JAVA 锁之 Synchronied
■ Java 锁
1. 锁的内存语义
- 锁可以让临界区互斥执行,还可以让释放锁的线程向同一个锁的线程发送消息
- 锁的释放要遵循 Happens-before 原则(锁规则:解锁必然发生在随后的加锁之前)
- 锁在Java中的具体表现是 Synchronized 和 Lock
2. 锁的释放与获取
- 线程A 释放锁后,会将共享变更操作刷新到主内存中
- 线程B 获取锁时,JMM会将该线程的本地内存置为无效,被监视器保护的临界区代码必须从主内存中读取共享变量
- 线程A释放一个锁,实质是线程A告知下一个获取到该锁的某个线程其已变更该共享变量
- 线程B获取一个锁,实质是线程B得到了线程A告知其(在释放锁之前)变更共享变量的消息
- 线程A释放锁,随后线程B竞争到该锁,实质是线程A通过主内存向线程B发消息告知其变更了共享变量
■ Synchronized 的综述
- 同步机制: synchronized是Java同步机制的一种实现,即互斥锁机制,它所获得的锁叫做互斥锁
- 互斥锁: 指的是每个对象的锁一次只能分配给一个线程,同一时间只能由一个线程占用
- 作用: synchronized 用于保证同一时刻只能由一个线程进入到临界区,同时保证共享变量的可见性、原子性和有序性
- 使用: 当一个线程试图访问同步代码方法(块)时,它首先必须得到锁,退出或抛出异常时必须释放锁,其他线程可以访问非 syn块
■ Synchronized 的使用规则
* 测试代码 - 证明锁的一些特性
1 /** 2 * 先定义一个测试模板类 3 * 这里补充一个知识点:Thread.sleep(long)不会释放锁 4 * 读者可参见笔者的`多线程 - 让程序更高效的运行` 5 */ 6 public class SynchronizedDemo { 7 public static synchronized void staticMethod(){ 8 System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod"); 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 System.out.println(Thread.currentThread().getName() + "结束访问静态同步方法staticMethod"); 15 } 16 public static void staticMethod2(){ 17 System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod2"); 18 synchronized (SynchronizedDemo.class){ 19 System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中获取了SynchronizedDemo.class"); 20 try { 21 Thread.sleep(1000); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 } 26 } 27 public synchronized void synMethod(){ 28 System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod"); 29 try { 30 Thread.sleep(1000); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } 34 System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod"); 35 } 36 public synchronized void synMethod2(){ 37 System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod2"); 38 try { 39 Thread.sleep(1000); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod2"); 44 } 45 public void method(){ 46 System.out.println(Thread.currentThread().getName() + "访问了普通方法method"); 47 try { 48 Thread.sleep(1000); 49 } catch (InterruptedException e) { 50 e.printStackTrace(); 51 } 52 System.out.println(Thread.currentThread().getName() + "结束访问普通方法method"); 53 } 54 private Object lock = new Object(); 55 public void chunkMethod(){ 56 System.out.println(Thread.currentThread().getName() + "访问了chunkMethod方法"); 57 synchronized (lock){ 58 System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中获取了lock"); 59 try { 60 Thread.sleep(1000); 61 } catch (InterruptedException e) { 62 e.printStackTrace(); 63 } 64 } 65 } 66 public void chunkMethod2(){ 67 System.out.println(Thread.currentThread().getName() + "访问了chunkMethod2方法"); 68 synchronized (lock){ 69 System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中获取了lock"); 70 try { 71 Thread.sleep(1000); 72 } catch (InterruptedException e) { 73 e.printStackTrace(); 74 } 75 } 76 } 77 public void chunkMethod3(){ 78 System.out.println(Thread.currentThread().getName() + "访问了chunkMethod3方法"); 79 //同步代码块 80 synchronized (this){ 81 System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中获取了this"); 82 try { 83 Thread.sleep(1000); 84 } catch (InterruptedException e) { 85 e.printStackTrace(); 86 } 87 } 88 } 89 public void stringMethod(String lock){ 90 synchronized (lock){ 91 while (true){ 92 System.out.println(Thread.currentThread().getName()); 93 try { 94 Thread.sleep(1000); 95 } catch (InterruptedException e) { 96 e.printStackTrace(); 97 } 98 } 99 } 100 } 101 }
1. 普通方法与同步方法调用互不关联 - 当一个线程进入同步方法时,其他线程可以正常访问其他非同步方法
public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { //调用普通方法 synDemo.method(); }); Thread thread2 = new Thread(() -> { //调用同步方法 synDemo.synMethod(); }); thread1.start(); thread2.start(); } --------------------- //输出: Thread-1访问了同步方法synMethod Thread-0访问了普通方法method Thread-0结束访问普通方法method Thread-1结束访问同步方法synMethod //分析:通过结果可知,普通方法和同步方法是非阻塞执行的
2. 所有同步方法只能被一个线程访问 - 当一个线程访问同步方法时,其他线程不能访问任何同步方法
public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { synDemo.synMethod(); synDemo.synMethod2(); }); Thread thread2 = new Thread(() -> { synDemo.synMethod2(); synDemo.synMethod(); }); thread1.start(); thread2.start(); } --------------------- //输出: Thread-0访问了同步方法synMethod Thread-0结束访问同步方法synMethod Thread-0访问了同步方法synMethod2 Thread-0结束访问同步方法synMethod2 Thread-1访问了同步方法synMethod2 Thread-1结束访问同步方法synMethod2 Thread-1访问了同步方法synMethod Thread-1结束访问同步方法synMethod //分析:通过结果可知,任务的执行是阻塞的,显然Thread-1必须等待Thread-0执行完毕之后才能继续执行
3. 当同步代码块都是同一个锁时,方法可以被所有线程访问,但同一个锁的同步代码块同一时刻只能被一个线程访问
1 public static void main(String[] args) { 2 SynchronizedDemo synDemo = new SynchronizedDemo(); 3 Thread thread1 = new Thread(() -> { 4 //调用同步块方法 5 synDemo.chunkMethod(); 6 synDemo.chunkMethod2(); 7 }); 8 Thread thread2 = new Thread(() -> { 9 //调用同步块方法 10 synDemo.chunkMethod(); 11 synDemo.synMethod2(); 12 }); 13 thread1.start(); 14 thread2.start(); 15 } 16 --------------------- 17 //输出: 18 Thread-0访问了chunkMethod方法 19 Thread-1访问了chunkMethod方法 20 Thread-0在chunkMethod方法中获取了lock 21 ...停顿等待... 22 Thread-1在chunkMethod方法中获取了lock 23 ...停顿等待... 24 Thread-0访问了chunkMethod2方法 25 Thread-0在chunkMethod2方法中获取了lock 26 ...停顿等待... 27 Thread-1访问了chunkMethod2方法 28 Thread-1在chunkMethod2方法中获取了lock 29 //分析可知: 30 //1.对比18行和19行可知,即使普通方法有同步代码块,但方法的访问是非阻塞的,任何线程都可以自由进入 31 //2.对比20行、22行以及25行和27行可知,对于同一个锁的同步代码块的访问一定是阻塞的
4. 线程间同时访问同一个锁多个同步代码的执行顺序不定,即使是使用同一个对象锁,这点跟同步方法有很大差异
1 public static void main(String[] args) { 2 SynchronizedDemo synDemo = new SynchronizedDemo(); 3 Thread thread1 = new Thread(() -> { 4 //调用同步块方法 5 synDemo.chunkMethod(); 6 synDemo.chunkMethod2(); 7 }); 8 Thread thread2 = new Thread(() -> { 9 //调用同步块方法 10 synDemo.chunkMethod2(); 11 synDemo.chunkMethod(); 12 }); 13 thread1.start(); 14 thread2.start(); 15 } 16 --------------------- 17 //输出: 18 Thread-0访问了chunkMethod方法 19 Thread-1访问了chunkMethod2方法 20 Thread-0在chunkMethod方法中获取了lock 21 ...停顿等待... 22 Thread-0访问了chunkMethod2方法 23 Thread-1在chunkMethod2方法中获取了lock 24 ...停顿等待... 25 Thread-1访问了chunkMethod方法 26 Thread-0在chunkMethod2方法中获取了lock 27 ...停顿等待... 28 Thread-1在chunkMethod方法中获取了lock 29 //分析可知: 30 //现象:对比20行、22行和24行、25行可知,虽然是同一个lock对象,但其不同代码块的访问是非阻塞的 31 //原因:根源在于锁的释放和重新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,依次类推到24行、25行时,Thread-0又重新获取到锁优先执行了 32 //注意:但有一点是必须的,对于同一个锁的同步代码块的访问一定是阻塞的 33 //补充:同步方法之所有会被全部阻塞,是因为synDemo对象一直被线程在内部把持住就没释放过,论把持住的重要性!
5. Synchronized 与重入性
- 重入锁:当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功
- 实现:一个线程得到一个对象锁后再次请求该对象锁,是允许的,每重入一次,monitor进入次数+1
1 public static void main(String[] args) { 2 SynchronizedDemo synDemo = new SynchronizedDemo(); 3 Thread thread1 = new Thread(() -> { 4 synDemo.synMethod(); 5 synDemo.synMethod2(); 6 }); 7 Thread thread2 = new Thread(() -> { 8 synDemo.synMethod2(); 9 synDemo.synMethod(); 10 }); 11 thread1.start(); 12 thread2.start(); 13 } 14 --------------------- 15 //输出: 16 Thread-0访问了同步方法synMethod 17 Thread-0结束访问同步方法synMethod 18 Thread-0访问了同步方法synMethod2 19 Thread-0结束访问同步方法synMethod2 20 Thread-1访问了同步方法synMethod2 21 Thread-1结束访问同步方法synMethod2 22 Thread-1访问了同步方法synMethod 23 Thread-1结束访问同步方法synMethod 24 //分析:对比16行和18行可知,在代码块中继续调用了当前实例对象的另外一个同步方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现
6. Synchronized 与 String 锁
- 隐患:由于在JVM中具有String常量池缓存的功能,因此相同字面量是同一个锁!!!
- 注意:严重不推荐将String作为锁对象,而应该改用其他非缓存对象
- 提示:对字面量有疑问的话请先回顾一下String的基础,这里不加以解释
1 public static void main(String[] args) { 2 SynchronizedDemo synDemo = new SynchronizedDemo(); 3 Thread thread1 = new Thread(() -> synDemo.stringMethod("sally")); 4 Thread thread2 = new Thread(() -> synDemo.stringMethod("sally")); 5 thread1.start(); 6 thread2.start(); 7 } 8 --------------------- 9 //输出: 10 Thread-0 11 Thread-0 12 Thread-0 13 Thread-0 14 ...死循环... 15 //分析:输出结果永远都是Thread-0的死循环,也就是说另一个线程,即Thread-1线程根本不会运行 16 //原因:同步块中的锁是同一个字面量
7. Synchronized 与 不可变锁
- 隐患:当使用不可变类对象(final Class)作为对象锁时,使用synchronized同样会有并发问题
- 原因:由于不可变特性,当作为锁但同步块内部仍然有计算操作,会生成一个新的锁对象
- 注意:严重不推荐将final Class作为锁对象时仍对其有计算操作
- 补充:虽然String也是final Class,但它的原因却是字面量常量池
public class SynchronizedDemo { static Integer i = 0; //Integer是final Class public static void main(String[] args) throws InterruptedException { Runnable runnable = new Runnable() { @Override public void run() { for (int j = 0;j<10000;j++){ synchronized (i){ i++; } } } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(i); } } --------------------- //输出: 14134 //分析:跟预想中的20000不一致,当使用Integer作为对象锁时但还有计算操作就会出现并发问题
手动预编译:
javac SynchronizedDemoTest.java
- 注意:由于笔者OS的默认编码方式是UTF-8,因此可能出现以下错误
javac -encoding UTF-8 SynchronizedDemoTest.java
- 最终,我们得到一个class 文件,即 SynchronizedDemo.class
- 我们通过反编译发现执行i++操作相当于执行了i = Integer.valueOf(i.intValue()+1)
- 通过查看Integer的valueOf方法实现可知,其每次都new了一个新的Integer对象,锁变了有木有!!!
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); //每次都new一个新的锁有木有!!! }
8. Synchronized 与 死锁
- 死锁:当线程间需要相互等待对方已持有的锁时,就形成死锁,进而产生死循环
- 注意: 代码中严禁出现死锁!!!
1 public static void main(String[] args) { 2 Object lock = new Object(); 3 Object lock2 = new Object(); 4 Thread thread1 = new Thread(() -> { 5 synchronized (lock){ 6 System.out.println(Thread.currentThread().getName() + "获取到lock锁"); 7 try { 8 Thread.sleep(2000); 9 } catch (InterruptedException e) { 10 e.printStackTrace(); 11 } 12 synchronized (lock2){ 13 System.out.println(Thread.currentThread().getName() + "获取到lock2锁"); 14 } 15 } 16 }); 17 Thread thread2 = new Thread(() -> { 18 synchronized (lock2){ 19 System.out.println(Thread.currentThread().getName() + "获取到lock2锁"); 20 try { 21 Thread.sleep(2000); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 synchronized (lock){ 26 System.out.println(Thread.currentThread().getName() + "获取到lock锁"); 27 } 28 } 29 }); 30 thread1.start(); 31 thread2.start(); 32 } 33 --------------------- 34 //输出: 35 Thread-1获取到lock2锁 36 Thread-0获取到lock锁 37 ..... 38 //分析:线程0获得lock锁,线程1获得lock2锁,但之后由于两个线程还要获取对方已持有的锁,但已持有的锁都不会被双方释放,线程"假死",无法往下执行,从而形成死循环,即死锁,之后一直在做无用的死循环,严重浪费系统资源
- 我们用 jstack 查看一下这个任务的各个线程运行情况,可以发现两个线程都被阻塞 BLOCKED
- 我们很明显的发现,Java-level=deadlock,即死锁,两个线程相互等待对方的锁
■ Synchronization 实现原理
/* Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions). For code written in the Java programming language, perhaps the most common form of synchronization is the synchronized method.
A synchronized method is not normally implemented using monitorenter and monitorexit.
Rather, it is simply distinguished in the run-time constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions (§2.11.10). */
- 本段摘自 The Java® Virtual Machine Specification 3.14. Synchronization
- 在JVM中,同步的实现是通过监视器锁的进入和退出实现的,要么显示得通过 monitorenter 和 monitorexit 指令实现,要么隐示地通过方法调用和返回指令实现
- 对于Java代码来说,或许最常用的同步实现就是同步方法。其中同步代码块是通过使用 monitorenter 和 monitorexit 实现的,而同步方法却是使用 ACC_SYNCHRONIZED 标记符隐示的实现,原理是通过方法调用指令检查该方法在常量池中是否包含 ACC_SYNCHRONIZED 标记符
- 本篇不会针对 Synchronized 的字节码实现进行分析,只是点到为止,有兴趣的读者可参见 JVM源码分析之 synchronized 实现 (当然,若有机会开JVM番的话,笔者会重新分析的)
我们可以反编译测试类,看看 jstack 如何展示:
- 常量池图示
- 常量池除了会包含基本类型和字符串及数组的常量值外,还包含以文本形式出现的符号引用:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法和名称和描述符
- 同步方法图示
- 同步方法会包含一个 ACC_SYNCHCRONIZED 标记符
- 同步代码块会在代码中插入 monitorenter 和 monitorexist 指令
PS:爱问的读者可能会追问下去,什么是监视器模式?什么是 Monitor Object? 关于进阶的一些知识以后有空会整理分享!
再次感谢好基友 Kira 的友情赞助,谢谢~