Sychronized in Java

使用多线程,避免不了要考虑线程安全的问题,常见解决线程安全的方式:是采用“序列化访问临界资源”的方案。
即在同一时刻,只能有一个线程访问临界资源,其他线程只能阻塞等待,这种方式也称作同步互斥访问。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)