写于:2020-02-03

[【THEORY]-[06]-线程安全与数据同步-概念中提到共享数据在线程间的问题。

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

# 一、什么是 synchronized

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

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

# 二、synchronized 使用

# 1、同步代码块的方式

案例如下

public class SimpleThread{
	private final Object lock = new Object();
	public void sayHello(){
		synchronized(lock){
			.....
		}
	}
}

# 2、同步方法的方式

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

案例如下:

public class SimpleThread{
	public synchronized void sayHello(){
		.......
	}
}

# 三、深入 synchronized 关键字

测试代码如下:

public class SimpleThread {

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

    public static class SynchronizedDemo{
        private final static Object mutex = new Object();
        public void shareData(){
            synchronized (mutex){
                try {
                    TimeUnit.MINUTES.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

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

# 1、线程堆栈分析

使用 jstack 命令分析

# step1、jps 获取线程号

$ jps
6032 JConsole
2980
11928 Jps
12268 Launcher
2716 SimpleThread

# step2、jstack 查看 2716 堆栈信息

jstack 2716

查看对应的信息如下:

通过堆栈信息能够知道,等待和持有的是同一个锁对象。

# 2、JVM 指令分析

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

# javap 指令进行反编译

javap -c SimpleThread$SynchronizedDemo.class

反编译后的代码如下

代码中需要关注的就是两个 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 对象。

# 通过堆栈信息验证

通过堆栈信息,发现 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 。

# 通过堆栈信息验证

通过堆栈信息,发现 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 查看死锁

精彩内容推送,请关注公众号!
最近更新时间: 4/16/2020, 8:37:50 PM