synchronized是并发编程中重要的使用工具之一,我们必须学会使用并且掌握它的原理。
JVM自带的关键字,可在需要线程安全的业务场景中使用,来保证线程安全。
按照锁的对象区分可以分为 对象锁 和 类锁 按照在代码中的位置区分可以分为 方法形式 和 代码块形式
锁对象为当前 this 或者说是 当前类 的实例对象
public void synchronized method() { System.out.println("我是普通方法形式的对象锁"); } public void method() { synchronized(this) { System.out.println("我是代码块形式的对象锁"); } } 复制代码
锁的是当前类或者指定类的Class对象。一个类可能有多个实例对象,但它只可能有一个Class对象。
public static void synchronized method() { System.out.println("我是静态方法形式的类锁"); } public void method() { synchronized(*.class) { System.out.println("我是代码块形式的类锁"); } } 复制代码
[参考] www.imooc.com/learn/1086 【慕课网,Java高并发之魂:synchronized深度解析】
最基本的用法在上一个标题用法中已将伪代码列出,这里列举在以上基础上稍微变化一些的用法
public class SimpleExample implements Runnable { static SimpleExample instance1 = new SimpleExample(); static SimpleExample instance2 = new SimpleExample(); @Override public void run() { method1(); method2(); method3(); method4(); } public synchronized void method1() { common(); } public static synchronized void method2() { commonStatic(); } public void method3() { synchronized(this) { common(); } } public void method4() { synchronized(MultiInstance.class) { common(); } } public void method5() { common(); } public void method6() { commonStatic(); } public void common() { System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行"); try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println("线程 " + Thread.currentThread().getName() + " 执行完毕"); } public static void commonStatic() { System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行"); try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println("线程 " + Thread.currentThread().getName() + " 执行完毕"); } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance1); Thread t2 = new Thread(instance2); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("finished"); } } method1()、method3()结果为: 线程 Thread-0 正在执行 线程 Thread-1 正在执行 线程 Thread-0 执行完毕 线程 Thread-1 执行完毕 finished method2()、method4()执行结果为: 线程 Thread-0 正在执行 线程 Thread-0 执行完毕 线程 Thread-1 正在执行 线程 Thread-1 执行完毕 finished 复制代码
// 将run方法改为 @Override public void run() { if("Thread-0".equals(Thread.currentThread().getName())) { method1(); } else { method2(); } } // 将main方法改为 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance1); Thread t2 = new Thread(instance1); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("finished"); } 结果为: 线程 Thread-0 正在执行 线程 Thread-1 正在执行 线程 Thread-1 执行完毕 线程 Thread-0 执行完毕 finished 复制代码
3.对象锁和无锁得普通方法,普通方法不需要持有锁,所以异步执行
// 将run方法改为 @Override public void run() { if("Thread-0".equals(Thread.currentThread().getName())) { method1(); } else { method5(); } } // main方法同 2 结果为: 线程 Thread-0 正在执行 线程 Thread-1 正在执行 线程 Thread-0 执行完毕 线程 Thread-1 执行完毕 finished 复制代码
// 将run方法改为 @Override public void run() { if("Thread-0".equals(Thread.currentThread().getName())) { method1(); } else { method6(); } } // main方法同 2 结果为: 线程 Thread-0 正在执行 线程 Thread-1 正在执行 线程 Thread-0 执行完毕 线程 Thread-1 执行完毕 finished 复制代码
// run方法改为 @Override public void run() { if ("Thread-0".equals(Thread.currentThread().getName())) { method7(); } else { method8(); } } public synchronized void method7() { try { ... throw new Exception(); } catch (Exception e) { e.printStackTrace(); } } public synchronized void method8() { common(); } public static void main(String[] args) throws InterruptedException { // 同 2 } 结果为: 线程 Thread-0 正在执行 java.lang.Exception at com.marksman.theory2practicehighconcurrency.synchronizedtest.blog.SynchronizedException.method7(SynchronizedException.java:26) at com.marksman.theory2practicehighconcurrency.synchronizedtest.blog.SynchronizedException.run(SynchronizedException.java:15) at java.lang.Thread.run(Thread.java:748) 线程 Thread-0 执行结束 线程 Thread-1 正在执行 线程 Thread-1 执行结束 finished // 这说明抛出异常后持有对象锁的method7()方法释放了锁,这样method8()才能获取到锁并执行。 复制代码
public class SynchronizedRecursion { int a = 0; int b = 0; private void method1() { System.out.println("method1正在执行,a = " + a); if (a == 0) { a ++; method1(); } System.out.println("method1执行结束,a = " + a); } private synchronized void method2() { System.out.println("method2正在执行,b = " + b); if (b == 0) { b ++; method2(); } System.out.println("method2执行结束,b = " + b); } public static void main(String[] args) { SynchronizedRecursion synchronizedRecursion = new SynchronizedRecursion(); synchronizedRecursion.method1(); synchronizedRecursion.method2(); } } 结果为: method1正在执行,a = 0 method1正在执行,a = 1 method1执行结束,a = 1 method1执行结束,a = 1 method2正在执行,b = 0 method2正在执行,b = 1 method2执行结束,b = 1 method2执行结束,b = 1 复制代码
总结一下
将下面两段代码分别用 javac *.java编译成.class文件,再反编译 javap -verbose *.class文件
public class SynchronizedThis { public void method() { synchronized(this) {} } } // 反编译结果 public void method(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_1 5: monitorexit 6: goto 14 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow 14: return 复制代码
public class SynchronizedMethod { public synchronized void method() {} } // 反编译结果 public synchronized void method(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 2: 0 复制代码
可以看到:
[参考] www.jianshu.com/p/3d38cba67… 【简书,Java对象头详解】
上面我们提到monitor,这是什么鬼? 其实,对象在内存是这样存储的,包括 对象头 、 实例数据 和 对齐填充padding ,其中对象头包括Mark Word和类型指针。
Mark Word用于存储对象自身的运行时数据,如哈希码(identity_hashcode)、GC分代年龄(age)、锁状态标志(lock)、线程持有的锁、偏向线程ID(thread)、偏向时间戳(epoch)等等,占用内存大小与虚拟机位长一致。
Mark Word (32 bits) | State 锁状态 |
---|---|
identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal 无锁 |
thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased 偏向锁 |
ptr_to_lock_record:30 | lock:2 | Lightweight Locked 轻量级锁 |
ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked 重量级锁 |
| lock:2 | Marked for GC GC标记 |
Mark Word (64 bits) | State 锁状态 |
---|---|
unused:25|identity_hashcode:31|unused:1|age:4|biased_lock:1|lock:2 | Normal 无锁 |
thread:54 |epoch:2|unused:1|age:4|biased_lock:1|lock:2 | Biased 偏向锁 |
ptr_to_lock_record:62 | lock:2 | Lightweight Locked 轻量级锁 |
ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked 重量级锁 |
| lock:2 | Marked for GC GC标记 |
可以看到,monitor就存在Mark Word中。
类型指针指向对象的类元数据 metadata ,虚拟机通过这个指针确定该对象是哪个类的实例。
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
jdk1.6之前synchronized是很重的,所以并不被开发者偏爱,随着后续版本jdk对synchronized的优化使其越来越轻量,它还是很好用的,甚至ConcurrentHashMap在jdk的put方法都在jdk1.8时从ReetrantLock.tryLock()改为用synchronized来实现同步。 并且还引入了偏向锁,轻量级锁等概念,下面是偏向锁和轻量级锁的获取流程
![偏向锁和轻量级锁的获取流程] www.processon.com/diagraming/… 【ProncessOn 公开克隆】
[参考] 链接: pan.baidu.com/s/1gA_URint… 提取码:s6vx 【咕泡学院公开课】
如果一个线程获取了偏向锁,那么如果在接下来的一段时间里,如果没有其他线程来抢占锁,那么获取锁的线程在下一次进入方法时不需要重新获取锁。
[参考] time.geekbang.org/column/arti… 【极客时间,Java核心技术36讲专栏】
区别 | synchronized | ReentrantLock |
---|---|---|
灵活性 | 代码简单,自动获取、释放锁 | 相对繁琐,需要手动获取、释放锁 |
是否可重入 | 是 | 是 |
作用位置 | 可作用在方法和代码块 | 只能用在代码块 |
获取、释放锁的方式 | monitorenter、monitorexit、ACC_SYNCHRONIZED | 尝试非阻塞获取锁tryLock()、超时获取锁tryLock(long timeout,TimeUnit unit)、unlock() |
获取锁的结果 | 不知道 | 可知,tryLock()返回boolean |
使用注意事项 | 1、锁对象不能为空(锁保存在对象头中,null没有对象头)2、作用域不宜过大 | 1、切记要在finally中unlock(),否则会形成死锁 2、不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。 |