美文网首页
Sychronized in Java

Sychronized in Java

作者: Shmily鱼 | 来源:发表于2018-01-22 15:10 被阅读0次

    使用多线程,避免不了要考虑线程安全的问题,常见解决线程安全的方式:是采用“序列化访问临界资源”的方案。
    即在同一时刻,只能有一个线程访问临界资源,其他线程只能阻塞等待,这种方式也称作同步互斥访问。synchronized同步锁就能实现这种效果,解决线程安全的问题。

    ① synchronized同步锁

    解决资源共享的问题:给共享的资源加锁,让线程一个个通过,以确保每次线程读取的数据是正确的。
    当synchronized用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
    春运已至,看看大家最关心的火车票:

        private int ticker = 1000;
        class SubTickerCount implements Runnable{
            @Override
            public void run() {
                // 这个循环只是为了能够一直卖票而已
                for (int index = 0; index < 1100; index++) {
                    if (ticker > 0) {
                        try {
                            // 为了避免一个线程执行到底
                            Thread.sleep(10);
                        }catch (Exception e){}
                        System.out.println(Thread.currentThread().getName() +
                                                       "号窗口卖出:" + ticker-- + "号票");
                    }
                }
            }
        }
    ...
       // 开十个线程,售1000张票
        private void saleTicker(){
            SubTickerCount  runnable = new SubTickerCount();
            threadCount1 = new Thread(runnable,"SubCount1");
            threadCount2 = new Thread(runnable,"SubCount2");
            ... 
            threadCount10 = new Thread(runnable,"SubCount10");
    
            threadCount1.start();
            threadCount2.start();
            ...
            threadCount10.start();
        }
    
    打印结果: image.png

    十个并发线程访问同一个对象时,最终出现卖出负数的错误。
    修改代码,添加synchronized:

      synchronized (this) {
          if (ticker > 0) {
              System.out.println(Thread.currentThread().getName() + "号窗口卖出:" + ticker-- + "号票");
          }
    
    打印结果: image.png

    总结:当多个并发线程访问同一个对象中的synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。其他必须等待,直到当前线程执行完同步代码块之后才能执行该代码块。

    那当一个线程访问object的一个synchronized(this)同步代码块时,其他线程可以访问该object中的非同步代码块吗?

        public void testSynchronized2_1() {
            synchronized (this) {
                for (int index = 0; index < 5; index++) {
                    Log.v(TAG, Thread.currentThread().getName() +
                            " synchronized loop " + index);
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                    }
                }
            }
        }
    
        public void testSynchronized2_2() {
            for (int index = 0; index < 5; index++) {
                Log.v(TAG, Thread.currentThread().getName() +
                        " no synchronized loop " + index);
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                }
            }
        }
    
        private void testSynchronizedOrNot(){
            final SynchronizedOrNot synchro = new SynchronizedOrNot();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchro.testSynchronized2_1();
                }
            }, "Thread1");
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchro.testSynchronized2_2();
                }
            }, "Thread2");
    
            thread1.start();
            thread2.start();
        }
    
    打印结果: image.png

    总结:当一个线程访问object的一个synchronized(this)同步代码块时,其他线程仍然可以访问该object中的非同步代码块。

    来一首诗歌吧
        public synchronized void printBefore() {
            delayPrint("我打江南走过");
            delayPrint("那等在季节里的容颜如莲花的开落");
            delayPrint("东风不来,三月的柳絮不飞");
            delayPrint("你底心如小小的寂寞的城");
        }
    
        public synchronized void printAfter(){
                delayPrint("恰若青石的街道向晚");
                delayPrint("跫音不响,三月的春帷不揭");
                delayPrint("我达达的马蹄是美丽的错误");
                delayPrint("我不是归人,是个过客……");
        }
    
        public synchronized void printTitle(){
            delayPrint("~~~《错误》 郑愁予");
        }
          //线程1先打印诗歌前半段,再打印后半段和标题
        Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    print.printBefore();
                    print.printAfter();
                    print.printTitle();
                }
            }, "Thread1");
         //线程2先打印诗歌后半段,再打印前半段和标题
         Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    print.printAfter();
                    print.printTitle();
                    print.printBefore();
                }
            }, "Thread2");
    
            thread1.start();
            thread2.start();
        }
    
    打印结果: image.png

    总结:当一个线程访问object的同步代码块或同步方法时,其他线程对object中所有同步代码块或方法的访问将被阻塞。
    因为对象锁就这么一个,一个线程获得这个锁,其他线程对该对object所有同步代码区域的访问都被暂时阻塞。

    通俗的例子

    假设我去餐厅吃饭,我占用了一个或多个餐桌,(我自己吃饭或我帮一起来吃饭的小伙伴占位)就好比我给这些餐桌加了锁,那么其他人想要用这个餐桌,只能等我们用餐结束后,离开这个餐桌(释放了锁),其他人才可以使用这个餐桌。但是如果存在无人占用的餐桌(未加锁的餐桌)其他人还是可以使用的。

    ② 锁的重入性:
        public synchronized void method1(){
            Log.v(TAG,"----method1----");
            method2();
        }
    
        public synchronized void method2(){
            Log.v(TAG,"----method2----");
            method3();
        }
    
        public synchronized void method3(){
            Log.v(TAG,"----method3----");
        }
       
         打印结果
        ----method1----
        ----method2----
        ----method3----
    

    总结:当一个线程已经持有一个对象锁后,再次请求该锁对象是可以得到锁的。这种方式称为锁的可重入性,它是线程安全的一种,自己可以获取自己的内部锁。

    ③ 对象锁
    • synchronized修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{ }括起来的代码,作用的对象是synchronized(object)中的object对象。
    • synchronized修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    • sychronized(this)锁住的只是对象本身,同一个类的不同对象调用sychronized方法并不会被锁住,不会产生互斥;
      对象锁作用于:调用这个方法或代码块的对象
    ④ 类锁
    • static synchronized 修饰一个静态的方法,其作用的范围是整个静态方法;
    • synchronized修饰一个类,其作用的范围是synchronized后面括号括起来的部分。
      类锁作用于:这个类的所有对象

    对象锁:修饰方法或对象或代码块,相同对象,即是同一个对象锁,可以实现不同线程的互斥效果。但如果是不同对象,就会有不同的对象锁,不能实现互斥。类锁:实现了全局锁的功能,这个类的所有对象,调用被类锁修饰的方法,都受到锁的影响

    So同一个时间段,只可能有一个线程获取类锁,从而执行这段代码。全局锁,单例就是很好的例子。

    public static CommonDialog getInstance(){
        if (null == instance) {
            instance = new CommonDialog();
        }
        return instance;
    }
    

    假设两个线程:
    线程①执行到代码 if (null == instance) 还未执行instance = new CommonDialog();
    线程②执行到代码 if (null == instance) 此时线程①的instance 还new未出来,仍然为null。
    而接下来,线程① instance创建一次,之后线程②instance再被创建一次,instance被重复创建了。
    加上synchronized,单例方式一

    public static synchronized  CommonDialog getInstance(){
        if (null == instance) {
            instance = new CommonDialog();
        }
        return instance;
    }
    

    这样写instance的确不会被重复创建,但是锁(代码块)的粒度太大,我们只关心instance创建部分,没有必要给整个方法加锁。如果多个线程频繁调用getInstance()方法,synchronized导致性能开销较大,程序执行性能也就下降了。
    可以采用synchronized(className.class)的方式。
    所以上文可以写成:单例方式二

    public static CommonDialog getInstance(){
        if (null == instance) {
            //类锁,这个类的所有对象调用此方法,都会受到锁的影响
                synchronized (CommonDialog.class) {
                instance = new CommonDialog();
            }
        }
        return instance;
    }
    

    这样写貌似没有什么问题, 但是我们考虑一种情况:
    当instance为null,线程①与线程②都进入if语句,步骤如下:
    1.线程①获得了锁资源,线程②等待锁资源
    2.线程①执行完代码块instance = new CommonDialog(); instance 被创建,释放锁资源
    3.线程②获得锁资源,执行代码块instance = new CommonDialog();instance 被创建,释放锁资源。instance又被创建了两次。
    基于java内存模型与synchronized实现了内存可见性,此类情况很可能出现
    于是我们再次修改代码:

    public static CommonDialog getInstance() {
          if (null == instance) {
              //这个类的所有对象调用此方法,都会受到锁的影响
              synchronized (CommonDialog。class) {
              //其他线程可能获取过锁,并且实例化了instance 而当前线程一直被阻塞到此处           
                        if(null == instance) {
                               instance = new CommonDialog();
                    }
              }
          }
        return instance;
    }
    

    这种方式叫做双重检查上锁的单例模式,此类方式是基于synchronized实现了可见性与原子性的特性。
    这两种方式,作用的对象都是这个类的所有对象,作用的范围都是整个方法,区别在锁的位置。
    锁的粒度: 我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。(synchronized使用起来非常简单,却是牺牲性能换取的代码的可读性。所以锁的使用原则:锁的范围越小越好)

    注意,以上的示例是为了描述synchronized的用法,但这并不是最好的单例,因涉及到java内存模型,所以我们单开一篇文章来说java内存模型

    目前最推崇的单例模式如下:

    public class Singleton {
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
        private Singleton () { }
        public static final Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本,完美!

    ⑤ 死锁
        Person a = new Person() ;
        Person b = new Person() ;
        synchronized(a) {
            ...
        }
        synchronized(b) {
            ...
        }
    
        private void method1(){
            synchronized(a) {
                synchronized(b)  {
                }
            }
        }
    
        private void method2(){
            synchronized(b) {
                synchronized(a)  {
                }
            }
        }
    

    假设线程①进入method1,获得锁a,执行代码... 同时线程②进入method2,获得锁b,执行代码... 此时线程①未释放锁a,等待锁b,而线程②未释放锁b,等待锁a,两者都再等待对方释放锁,以便自己获得锁。这种情况就是死锁。
    有一张很经典的图例:

    image.png
    避免死锁的关键在于,尽量保持一致的锁的获取顺序
    还是以几个面试题结尾

    ①当对一个方法加锁的时候,锁的是谁,描述这个过程?
    当对一个方法加锁时,实际是对调用此方法的对象加锁。
    使用synchronized关键字来标记一个方法/代码块,当某个线程调用该对象的synchronized方法/代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法/代码块,只有等待这个方法/代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能获取锁资源,执行这个方法/代码块。
    ②类锁与对象锁互斥吗?
    类锁与对象锁,不是一个锁,所以不存在互斥。
    如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为调用static synchronized方法占用的是类锁,而调用非static synchronized方法占用的是对象锁,不是同一个锁。
    ③当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
    答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。既然线程已进入同步方法A,说明此线程已获得对象锁并且未释放,那么试图进入B方法的线程就只能在等锁池中等待对象的锁。
    ④synchronized锁的可重入性
    可重入锁:当一个线程已经持有一个对象锁后,再次请求该锁对象是可以得到锁的。
    这种方式称为锁的可重入性,它是线程安全的一种,自己可以获取自己的内部锁。
    这种方式也是必须的:否则在一个synchrnoized方法内部就无法调用该对象的另外一个synchrnoized方法了。
    工作原理:锁的重入性,是设置一个计数器,关联占有它的线程,当计数器为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM会记录锁的占有者,并将计数器设置为1。 如果同一个线程再次请求该锁,计数器会递增,每次占有的线程退出同步代码块时,计数器会递减,直至减为0时,锁才会被释放。 重入性原理参考为文章:http://www.cnblogs.com/pureEve/p/6421273.html
    ⑤synchronized的缺点
    synchronized可以轻松的解决线程同步的存在的隐患,但却不够灵活,主要是通过牺牲性能换来语法上的简洁与可读。
    ⑥synchronized出现异常时,会怎样?
    锁自动释放:当一个线程执行的代码出现异常的时候,其所持有的锁会自动释放,So还是比较安全的。

    本文先用几个例子,介绍了synchronized的概念与用法,以及对象锁与类锁的区别。然后用几个面试题,再次回顾synchronized的特性,将这些知识点串联在一起。
    喜欢学习,乐于分享,不麻烦的话,给个❤鼓励下吧!

    相关文章

      网友评论

          本文标题:Sychronized in Java

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