美文网首页
synchronized

synchronized

作者: 发光的老金 | 来源:发表于2020-03-15 03:53 被阅读0次

    synchronized简介

    官方解释

    同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。
    简单来说,就是保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

    
    public class synchronizedTest implements Runnable {
    
        static synchronizedTest s = new synchronizedTest();
        static int i = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(s);
            Thread thread2 = new Thread(s);
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println(i);
        }
    
        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                i++;
            }
        }
    
    }
    


    分别执行两次之后,发现并没有达到我们期望,两线程相加得到20000这个结果,并且得数也不相同。这就说明,这两个线程有同时操作i这个数据的情况,这样导则结果不同,这就是线程不安全的问题。

    synchronized的地位

    • synchronized是Java的关键字,被Java语言原生支持
    • 是最基本的互斥同步手段
    • 是并发编程中的元老级角色,是并发编程中的必学内容

    不使用并发手段会有什么后果

    多线程在同时处理一个数据的时候,会导致数据错误,这样就是线程不安全的情况。

    synchronized的两个用法

    对象锁

    包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)

    类锁

    指synchronized修饰静态的方法或指定锁为Class对象

    对象锁

    代码块形式:手动指定锁对象

    
    public class synchronizedclass implements Runnable {
        static synchronizedclass instance = new synchronizedclass();
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            while (t1.isAlive() || t2.isAlive()) {
    
            }
            System.out.println("finish");
        }
    
        @Override
        public void run() {
            synchronized (this) {
                System.err.println("我是对象的代码块形式。我叫" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "运行结束");
            }
        }
    }
    

    使用synchronized 关键字锁定指定的代码块,实现线程安全。
    括号里填写锁对象。但是如果有多段代码都需要同步而彼此之间并不用同步的话,那么选取不同的锁对象就可以实现这个需求。

    public class synchronizedclass implements Runnable {
        static synchronizedclass instance = new synchronizedclass();
        Object lock1 = new Object();
        Object lock2 = new Object();
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            while (t1.isAlive() || t2.isAlive()) {
    
            }
            System.out.println("finish");
        }
    
        @Override
        public void run() {
            synchronized (lock1) {
                System.out.println("我是lock1。我叫" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " lock1部分运行结束");
            }
    
            synchronized (lock2) {
                System.out.println("我是lock2。我叫" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " lock2部分运行结束");
            }
        }
    }
    

    从上面可以看出来,lock1锁住上面的代码的时候,另一个线程是无法执行这段代码的。当第一个线程释放锁的时候,第二个线程才可以拿到这个锁,并且无法获取下一把锁。这样就实现了不同的锁来锁住不同的代码块

    方法锁形式:synchronized修饰普通方法,锁对象默认为this

    直接用synchronized关键字修饰这个方法,被修饰的方法(不能是静态方法)

    public class synchronizedclass implements Runnable {
        static synchronizedclass instance = new synchronizedclass();
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            while (t1.isAlive() || t2.isAlive()) {
    
            }
            System.out.println("finish");
        }
    
        public synchronized void method() {
            System.out.println("我是对象锁的方法修饰符形式,我叫"+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(Thread.currentThread().getName()+"运行结束");
        }
        @Override
        public void run() {
            method();
        }
    }
    

    根据上面的内容可以看出来,当第一个线程的run()方法里面调用了被synchronized修饰的method()方法的时候,另一个线程是无法执行这个方法的代码的,这样就实现了线程安全的操作。

    类锁

    概念

    • 只有一个Class对象:java类可能有很多个对象,但只能有一个Class对象
    • 本质:所以所谓的类锁,不过是Class对象的锁而已。
    • 用法和效果:类锁只能在同一时刻被一个对象拥有。

    synchronized加在static方法上

    这么做的作用是可以做到在全局都实现线程同步

    public class synchronizedclass implements Runnable {
        static synchronizedclass instance1 = new synchronizedclass();
        static synchronizedclass instance2 = new synchronizedclass();
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance1);
            Thread t2 = new Thread(instance2);
            t1.start();
            t2.start();
            while (t1.isAlive() || t2.isAlive()) {
    
            }
            System.out.println("finish");
        }
    
        public static synchronized void method() {
            System.out.println("我是类锁的第一种形式:static形式,我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    
        @Override
        public void run() {
            method();
        }
    }
    

    从上面可以看出来,两个线程的参数不同并同时调用了method()方法,因为存在着被synchronized修饰的这个静态方法,所以依然可以实现线程同步。如果将synchronized 这个关键字拿掉的话,则线程的安全无法实现。

    synchronized(*.class)代码块

    public class synchronizedclass implements Runnable {
    
        static synchronizedclass s1 = new synchronizedclass();
        static synchronizedclass s2 = new synchronizedclass();
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(s1);
            Thread thread2 = new Thread(s2);
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
        }
    
        @Override
        public void run() {
            try {
                method();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private void method() throws InterruptedException {
            synchronized (synchronizedclass.class) {
                System.out.println("我是类锁的第二种形式:synchronized(*.class)。我叫" + Thread.currentThread().getName());
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + "运行结束");
            }
        }
    
    }
    

    这就说明了,无论是哪条线程,只要同步代码块种放了这个类的对象,最终大家共用对象,即使是不同的实例,也要逐个的执行代码,实现线程安全。

    多线程访问同步方法的7种情况

    1. 两个线程同时访问一个对象的同步方法

    先执行的线程至执行完代码之前,另一个线程无法执行

    2. 两个线程同时访问两个对象的同步方法

    synchronized是不起作用的,因为锁的是各自不同的实例,所以互相不干扰

    3. 两个线程访问的是synchronized的静态方法

    synchronized起作用,先执行的线程至执行完代码之前,另一个线程无法执行

    4. 同时访问同步方法与非同步方法

    synchronized关键字它只作用于指定的那个方法中,对于其他的没有加这个修饰符的方法,不会受到影响

    
    public class SynchronizedYesAndNo implements Runnable {
        static SynchronizedYesAndNo s1 = new SynchronizedYesAndNo();
        
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(s1);
            Thread thread2 = new Thread(s1);
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
        }
        @Override
        public void run() {
            if (Thread.currentThread().getName().equals("Thread-0")) {
                method1();
            }else {
                method2();
            }
        }
    
        private synchronized void method1() {
            System.out.println("我是加锁的方法。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    
        private void method2() {
            System.out.println("我是没加锁的方法。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }
    

    5. 访问同一个对象的不同的普通方法

    虽说代码中synchronized并没有指定使用的锁对象,但是本质上它背后的原理是指定了this这个对象作为它的锁。所以说对于同一个实例来说,他们所拿到的锁对象是一样的,所以这两个方法不会同时运行

    
    public class Synchronized1 implements Runnable{
        static Synchronized1 s1 = new Synchronized1();
        
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(s1);
            Thread thread2 = new Thread(s1);
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
        }
        @Override
        public void run() {
            if (Thread.currentThread().getName().equals("Thread-0")) {
                method1();
            }else {
                method2();
            }
        }
        private synchronized void method1() {
            System.out.println("我是加锁的方法method1。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    
        private synchronized void method2() {
            System.out.println("我是加锁的方法method2。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }
    
    1

    6. 同时访问静态synchronized和非静态synchronized方法

    静态synchronized方法加的锁是(*.class),而非静态synchronized方法加的锁是这个实例this。所以看似是一个类里的两个synchronized方法,但是却可以同时运行。

    
    public class Synchronized2 implements Runnable{
        static Synchronized2 s1 = new Synchronized2();
        
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(s1);
            Thread thread2 = new Thread(s1);
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
        }
        @Override
        public void run() {
            if (Thread.currentThread().getName().equals("Thread-0")) {
                method1();
            }else {
                method2();
            }
        }
        private static synchronized void method1() {
            System.out.println("我是静态加锁的方法method1。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    
        private synchronized void method2() {
            System.out.println("我是非静态加锁的方法method2。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }
    

    7. 方法抛异常后,会释放锁

    抛出异常之后,jvm会帮助代码释放锁,不需要手动释放锁。

    总结

    • 一把锁同时只能被一个线程获取,没有拿到锁的线程必须等待(对应第1,5种情况);
    • 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的静态方法的时候,所有对象共用同一把类锁(对应第2,3,4,6种情况);
    • 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)

    如果进入一个被synchronized修饰的方法,而在这个方法里面调用另外一个没有被synchronized修饰的方法,那它还是线程安全的吗?

    不是线程安全的,一旦出了本方法去了另一个方法,由于另一个方法没有被synchronized修饰,那么这个方法是可以被多个方法访问的。所以线程是不安全的;

    性质

    可重入

    什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁
    好处:避免死锁,提升封装性
    粒度:线程而非调用(用3种情况来说明和pthread的区别)

    粒度

    • 情况1:证明同一个方法是可重入的
    public class Synchronized4 {
    
        private int a = 0;
        public static void main(String[] args) {
            Synchronized4 s4 = new Synchronized4();
            s4.method();
        }
    
        private synchronized void method() {
            System.out.println("这是method,a = "+a);
            if(a == 0) {
                a++;
                method();
            }
            
        }
    }
    

    根据上面的代码可以看出,调用同一个被synchronized关键字修饰的方法是可以完成的

    • 情况2:证明可重入不要求是同一个方法
    public class Synchronized5 {
        
        public static void main(String[] args) {
            Synchronized5 s5 = new Synchronized5();
            s5.method1();
        }
        public synchronized void method1() {
            System.out.println("我是method1");
            method2();
        }
        
        public synchronized void method2() {
            System.out.println("我是method2");
        }
    }
    

    根据上面的代码,可以知道不是同一个方法也是可以重入的

    • 情况3:证明可重入不要求是同一个类中的
    public class Synchronized6 {
    
        public synchronized void method() {
            System.out.println("我是父类方法");
        }
    }
    class TestClass extends Synchronized6{
        public synchronized void method() {
            System.out.println("我是子类方法");
            super.method();
        }
        
        public static void main(String[] args) {
            TestClass t = new  TestClass();
            t.method();
        }
    }
    

    根据上面的代码可以知道,在子类继承父类的过程中,调用的main()函数已经和之前的不是同一个类了,但是依然实现了可重入,说明这个性质在不同的类中也是可以实现的

    根据上面的几个验证,说明了粒度范围是线程范围而非调用

    不可中断

    一旦这个锁已经被别的线程获得了,如果这个线程还想获得,那只能选择等待或者阻塞,知道别的线程释放这个锁。如果别的线程永远不释放锁,那么这个线程只能永远地等下去。
    相比之下,Lock类,可以拥有中断的能力,第一点,如果觉得等的时间太长了,有权中断现在已经获取锁的线程执行;第二点,如果觉得等待的时间太长了不想等了,也可以退出。

    原理

    1. 加锁和释放锁的原理:现象,时机,深入JVM看字节码

    加锁和释放锁的原理

    • 现象
      方法获取锁的时候其他方法无法获取这把锁,这有当这个方法执行完成或者抛出异常之后,其他方法才能获取锁
    • 获取和释放锁的时机:内置锁
    • 等价代码
    public class Synchronized7 {
    
        Lock lock = new ReentrantLock();
        public static void main(String[] args) {
            Synchronized7 s7 = new Synchronized7();
            s7.method1();
            s7.method2();
        }
        public synchronized void method1() {
            System.out.println("我是synchronized形式的锁");
        }
        
        public void method2() {
            lock.lock();
            try {
                System.out.println("我是Lock形式的锁");
            } finally {
                lock.unlock();
            }
        }
    }
    
    • 深入JVM看字节码:反编译,monitor指令
      概况:synchronized用的这把锁,是在java对象头里的一个字段,表明这个是被锁住了还是没锁住。它的进入锁和释放锁是基于monitor对象来实现同步方法和同步代码块的,
    1. 可重入原理:加锁次数计数器
    2. 保证可见性的原理:内存模型




      一旦代码块被synchronized关键字所修饰,那么他在执行完毕之后,被锁住的对象所做的任何修改都要在释放锁之前从线程内存写回到主内存中。在进入代码块得到锁之后,被锁定的对象的数据也是直接从主内存读取出来。因为之前的线程最后将数据写回到主内存中,所以这次从主内存中读取的数据一定是最新的。

    缺陷

    1. 效率低:锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程
      线程在获得synchronized锁并在执行这段代码的时候,其他线程无法得到这把锁,只能等待。也就只有两种情况才会释放锁:执行完毕代码和发生异常。其他情况都不会释放锁。如果这获得这把锁的线程中出现了耗时操作(io,sleep方法),其他线程只能等待,这样就非常影响执行效率。
      synchronized是不能设置超时的,也就是说出现了阻塞情况就会一直等待,这样也会影响效率。
      synchronized不能中断,这样也会影响代码的执行效率。
    2. 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
    3. 无法知道是否成果获取到锁

    synchronized相比于Lock类,可以对比它的不足

    public class LockExample {
    
        public static void main(String[] args) throws InterruptedException {
            ReentrantLock lock = new ReentrantLock(); 
            //加锁
            lock.lock();
            //解锁
            lock.unlock();
            //尝试获取到锁
            boolean tryLock = lock.tryLock();
            //尝试获取到锁,参数是超时时间
            boolean tryLock2 = lock.tryLock(10000, TimeUnit.SECONDS);
        }
    }
    

    一些问题

    1. synchronized关键字使用注意点:锁对象不能为空,作用域不宜过大,避免死锁
      锁对象不能为空就是说指定了一个对象作为我们得锁对象,它必须是一个实例对象,不能是一个空对象。因为锁的信息是保存在对象头中的。如果是一个空对象,也就没有对象头,所以这个锁是不能工作的。
      作用域不宜过大,它的作用域是指这个synchronized关键字所包裹的范围,如果包裹范围过大的话,很有可能将大部分线程变成串行,会降低出并发问题的可能性,这样就没法达到多线程编程的目的,这样会影响代码的执行效率。
      避免死锁,如果方法1用a锁修饰后,里面又调用了b锁,另一个方法2用b锁修饰后又调用了a锁,那么在执行过程中就容易出现死锁情况。
    2. 如何选择Lock和synchronized关键字?
      如果synchronized关键字在程序中适用,就优先使用这个,这样会减少编写的代码,也就减少了出错的几率;
      如果特别需要Lock独有的特性的时候(随时释放锁,设置等待时长等)。
    3. 多线程访问同步方法的各种具体情况
      上面的笔记有具体的7种情况,看懂熟记,明白原理。
    4. 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?什么算法?
    5. synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
      是优化使用范围,类似第一题的作用域不宜过大的原理。在符合要求的情况下尽可能的小。
      使用其他类型的Lock。
    6. 想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?
      自己实现一个Lock接口,这样就能代码控制这些操作了。
    7. 什么是锁的升级,降级?什么是JVM里的偏科锁,轻量级锁,重量级锁?

    总结

    • 一句话介绍synchronized
      JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程的安全,同时具有可重入和不可中断的的性质。

    相关文章

      网友评论

          本文标题:synchronized

          本文链接:https://www.haomeiwen.com/subject/sywodhtx.html