写于:2020-02-03

《线程安全与数据同步-概念》中提到共享数据在线程间的问题。

针对该问题,JDK 提供了 synchronized 关键字来解决。

一、什么是 synchronized

synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见,那么对该对象的所有读或者写都将通过同步的方式进行,具体如下:

  • synchronized 关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现
  • synchronized 关键字保证线程对共享变量的更新操作马上刷入主内存中。
  • synchronized 遵守 hapens-before 规则。

    二、synchronized 使用

1、同步代码块的方式

案例如下

  1. public class SimpleThread{
  2. private final Object lock = new Object();
  3. public void sayHello(){
  4. synchronized(lock){
  5. .....
  6. }
  7. }
  8. }

2、同步方法的方式

直接在方法上加上 synchronized 关键字

案例如下:

  1. public class SimpleThread{
  2. public synchronized void sayHello(){
  3. .......
  4. }
  5. }

三、深入 synchronized 关键字

测试代码如下:

  1. public class SimpleThread {
  2. public static void main(String[] args) throws InterruptedException {
  3. SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
  4. // 三个线程:T1,T2,T3
  5. IntStream.rangeClosed(1,3).forEach(loopTimes ->{
  6. new Thread(synchronizedDemo::shareData,"T" + loopTimes).start();
  7. });
  8. }
  9. public static class SynchronizedDemo{
  10. private final static Object mutex = new Object();
  11. public void shareData(){
  12. synchronized (mutex){
  13. try {
  14. TimeUnit.MINUTES.sleep(10);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. }
  21. }

多个线程对使用 synchronized 的共享资源进行同时访问。

1、线程堆栈分析

使用 jstack 命令分析

step1、jps 获取线程号

$ jps
6032 JConsole
2980
11928 Jps
12268 Launcher
2716 SimpleThread

step2、jstack 查看 2716 堆栈信息

jstack 2716

查看对应的信息如下:
02.png
通过堆栈信息能够知道,等待和持有的是同一个锁对象。

2、JVM 指令分析

通过 javap 指令对代码的 class 文件进行反编译

javap 指令进行反编译

javap -c SimpleThread$SynchronizedDemo.class

反编译后的代码如下
03.png
代码中需要关注的就是两个 JVM 指令:monitorenter 和 monitorexit

monitorenter

每个对象都与一个 monitor 关联,一个 monitor 的 lock 的锁只能被一个线程在同一时间获得。

小贴士: monitor 存在计数器: 当计数器为0时,表示该 monitor 的 lock 还没被获取。 当计数器 >= 1,表示该 monitor 的 lock 被同一个线程多次获取。(锁重入)

线程获取 monitor 的可能存在几种情况:

a、monitor 计数器此时为 0 ,线程获取到 monitor 的 lock ,monitor 计数器 +1

b、同一个线程再一次获取到 monitor 的 lock,monitor 计数器累加

c、monitor 的 lock 被另一个线程持有,当前线程进入阻塞状态,知道 monitor 计数器变为 0,然后在尝试获取 monitor 的 Lock

monitorexit

monitorenter 是通过对 monitor 计数器的累加,来表示被某个线程持有了该 monitor 的 lock。

monitorexit 通过对 monitor 计数器的递减,来表示对该 monitor 的 lock 的释放。

四、 this monitor 和 class monitor

synchronized 是一种锁机制,同步代码块的方式可以任意指定锁对象,那么同步方法被锁的又是什么?

1、this : 当前对象实例

代码验证

普通的方法加 synchronized 被锁的是 this 当前对象实例

通过代码的方式进行验证,代码如下:

public class ThisLockValid {
    public static void main(String[] args) {
        ThisLock thisLock = new ThisLock();
        new Thread(()->{
            thisLock.m1();
        },"Thread-1").start();
        new Thread(()->{
            thisLock.m2();
        },"Thread-2").start();
        new Thread(()->{
            thisLock.m3();
        },"Thread-3").start();
    }

}
// 验证逻辑
// 默认为 This 锁,也就是 ThisLock 对象锁。获取同一个锁,方法调用需等待。
class ThisLock{
    public synchronized void m1(){
        System.out.println(Thread.currentThread().getName() + ",m1");
        try {
            TimeUnit.SECONDS.sleep(70);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void m2(){
        System.out.println(Thread.currentThread().getName() + ",m2");
        try {
            TimeUnit.SECONDS.sleep(70);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void m3(){
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + ",m3");
            try {
                TimeUnit.SECONDS.sleep(70);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

代码中三个方法 m1、m2、m3。

其中,m1,m2 直接在方法上加了 synchronized 关键字,而 m3 使用同步代码块的方式显示指定 this 对象为锁对象。

通过代码执行验证,当 m3 执行时,m1 和 m2 都需要进行等待,验证 普通方法加上 synchronized ,默认锁的是 this 对象。

通过堆栈信息验证

05.png
通过堆栈信息,发现 Thread1 和 Thread2 和 Thread3 持有的是同一个 monitor 的 lock。验证 普通方法加上 synchronized ,默认锁的是 this 对象。

2、 class:当前 class

静态方法加上 synchronized 被锁的是 class 。

代码验证

通过如下代码验证

public static class SynchronizedDemo{
        public synchronized static void m1(){
            System.out.println(Thread.currentThread().getName() + ":m1 开始执行");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public synchronized static void m2(){
            System.out.println(Thread.currentThread().getName() +":m2 开始执行");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public static void m3(){
            synchronized (SynchronizedDemo.class){
                System.out.println(Thread.currentThread().getName() +":m3 开始执行");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
}

代码中三个方法 m1、m2、m3。

其中 m1、m2 是加了 synchronized 关键字的静态方法,m3是通过同步代码块指定 class 为锁对象的方法。

通过如下代码执行验证

public class SimpleThread {

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        new Thread(()->{
            synchronizedDemo.m1();
        },"T1").start();
        new Thread(()->{
            synchronizedDemo.m2();
        },"T2").start();
        new Thread(()->{
            synchronizedDemo.m3();
        },"T3").start();
    }
}

通过运行能够知道,m1,m2,m3 都需要进行争抢锁来获取执行权。从而验证静态方法加上 synchronized 锁的 class 。

通过堆栈信息验证

06.png
通过堆栈信息,发现 T1 和 T1 和 T3 持有的是同一个 monitor 的 lock。从而验证静态方法加上 synchronized 锁的 class 。

五、使用 synchronized 需要注意的几个问题

1、与 monitor 关联的对象不能为空

例如:

// 错误案例
public static class SynchronizedDemo{
        private final static Object mutex = null;
        public void shareData(){
            synchronized (mutex){
                ......
            }
        }
    }

2、synchronized 作用域太大

synchronized 存在排他性,被 synchronized 包围的区域,线程只能串行的执行,如果 synchronized 作用域越大,运行效率越低。

譬如下面代码:

public class Task implements Runnable{
    @Override
    public synchronized void run() {

    }
}

上述代码中,Runnable 中 run 方法整块都在 synchronized 同步代码块中,此时即使创建在多线程也没用,引用多个线程的执行同时串行执行的。

synchronized 应该尽可能的只作用于共享资源的读写作用域。

3、不同对象对应的 monitor 是不一样的

例如如下代码

public class SimpleThread {

    public static void main(String[] args) throws InterruptedException {
        // 三个线程:T1,T2,T3
        IntStream.rangeClosed(1,3).forEach(loopTimes ->{
            new Thread(SynchronizedDemo::new,"T" + loopTimes).start();
        });
    }

    public static class Task implements Runnable{
        private final Object lock = new Object();
        @Override
        public void run() {
            synchronized(lock){

            }
        }
    }
}

代码中创建的三个线程中对应的 lock 对象是不同的,所以 monitor 也是不一样的。因此起不到互斥的作用。

4、多个锁的交叉导致死锁

案例代码如下

public class SimpleThread {

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        IntStream.rangeClosed(1,3).forEach(loopTimes ->{
            new Thread(synchronizedDemo::read,"T" + loopTimes).start();
        });
        IntStream.rangeClosed(4,6).forEach(loopTimes ->{
            new Thread(synchronizedDemo::write,"T" + loopTimes).start();
        });
    }

    public static class SynchronizedDemo{
        private final static Object mutex_read = new Object();
        private final Object mutex_write = new Object();
        public void read(){
            synchronized (mutex_read){
                synchronized (mutex_write){
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        public  void write(){
            synchronized (mutex_write){
                synchronized (mutex_read){
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

jconsole 查看死锁
04.png