线程A释放锁后,会将共享变更操作刷新到主内存中
线程B获取锁时,JMM会将该线程的本地内存置为无效,被监视器保护的临界区代码必须从主内存中读取共享变量
使用同步代码块的好处在于其他线程仍可以访问非synchronized(this)的同步代码块
/** * 先定义一个测试模板类 * 这里补充一个知识点:Thread.sleep(long)不会释放锁 * 读者可参见笔者的`并发番@Thread一文通` */ public class SynchronizedDemo { public static synchronized void staticMethod(){ System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问静态同步方法staticMethod"); } public static void staticMethod2(){ System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod2"); synchronized (SynchronizedDemo.class){ System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中获取了SynchronizedDemo.class"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void synMethod(){ System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod"); } public synchronized void synMethod2(){ System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod2"); } public void method(){ System.out.println(Thread.currentThread().getName() + "访问了普通方法method"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问普通方法method"); } private Object lock = new Object(); public void chunkMethod(){ System.out.println(Thread.currentThread().getName() + "访问了chunkMethod方法"); synchronized (lock){ System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中获取了lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void chunkMethod2(){ System.out.println(Thread.currentThread().getName() + "访问了chunkMethod2方法"); synchronized (lock){ System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中获取了lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void chunkMethod3(){ System.out.println(Thread.currentThread().getName() + "访问了chunkMethod3方法"); //同步代码块 synchronized (this){ System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中获取了this"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void stringMethod(String lock){ synchronized (lock){ while (true){ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } 复制代码
当一个线程进入同步方法时,其他线程可以正常访问其他非同步方法
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 复制代码
分析:通过结果可知,普通方法和同步方法是非阻塞执行的
当一个线程执行同步方法时,其他线程不能访问任何同步方法
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执行完毕之后才能继续执行
当同步代码块都是同一个锁时,方法可以被所有线程访问,但同一个锁的同步代码块同一时刻只能被一个线程访问
public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { //调用同步块方法 synDemo.chunkMethod(); synDemo.chunkMethod2(); }); Thread thread2 = new Thread(() -> { //调用同步块方法 synDemo.chunkMethod(); synDemo.synMethod2(); }); thread1.start(); thread2.start(); } 复制代码
输出:
Thread-0访问了chunkMethod方法 Thread-1访问了chunkMethod方法 Thread-0在chunkMethod方法中获取了lock ...停顿等待... Thread-1在chunkMethod方法中获取了lock ...停顿等待... Thread-0访问了chunkMethod2方法 Thread-0在chunkMethod2方法中获取了lock ...停顿等待... Thread-1访问了chunkMethod2方法 Thread-1在chunkMethod2方法中获取了lock 复制代码
分析可知:
线程间同时访问同一个锁多个同步代码的执行顺序不定,即使是使用同一个对象锁
public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { //调用同步块方法 synDemo.chunkMethod(); synDemo.chunkMethod2(); }); Thread thread2 = new Thread(() -> { //调用同步块方法 synDemo.chunkMethod2(); synDemo.chunkMethod(); }); thread1.start(); thread2.start(); } --------------------- //输出: Thread-0访问了chunkMethod方法 Thread-1访问了chunkMethod2方法 Thread-0在chunkMethod方法中获取了lock ...停顿等待... Thread-0访问了chunkMethod2方法 Thread-1在chunkMethod2方法中获取了lock ...停顿等待... Thread-1访问了chunkMethod方法 Thread-0在chunkMethod2方法中获取了lock ...停顿等待... Thread-1在chunkMethod方法中获取了lock //分析可知: //现象:对比20行、22行和24行、25行可知,虽然是同一个lock对象,但其不同代码块的访问是非阻塞的 //原因:根源在于锁的释放和重新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,依次类推到24行、25行时,Thread-0又重新获取到锁优先执行了 //注意:但有一点是必须的,对于同一个锁的同步代码块的访问一定是阻塞的 //补充:同步方法之所以会被全部阻塞,是因为synDemo对象一直被线程在内部把持住就没释放过 复制代码
由于三种使用方式的锁对象都不一样,因此相互之间不会有任何影响但有两种情况除外:
public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> synDemo.chunkMethod() ); Thread thread2 = new Thread(() -> synDemo.chunkMethod3()); Thread thread3 = new Thread(() -> staticMethod()); Thread thread4 = new Thread(() -> staticMethod2()); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } --------------------- //输出: Thread-1访问了chunkMethod3方法 Thread-1在chunkMethod3方法中获取了this Thread-2访问了静态同步方法staticMethod Thread-0访问了chunkMethod方法 Thread-0在chunkMethod方法中获取了lock Thread-3访问了静态同步方法staticMethod2 ...停顿等待... Thread-2结束访问静态同步方法staticMethod Thread-3在staticMethod2方法中获取了SynchronizedDemo.class //分析可知: //现象:虽然是同一个lock对象,但其不同代码块的访问是非阻塞的 //原因:根源在于锁的释放和重新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,,Thread-0又重新获取到锁优先执行了 复制代码
重入锁:当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功实现:一个线程得到一个对象锁后再次请求该对象锁,是允许的,每重入一次,monitor进入次数+1
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 //分析:在代码块中继续调用了当前实例对象的另外一个同步方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现 复制代码
隐患:由于在JVM中具有String常量池缓存的功能,因此相同字面量是同一个锁!!!注意:严重不推荐将String作为锁对象,而应该改用其他非缓存对象提示:对字面量有疑问的话请先回顾一下String的基础
public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> synDemo.stringMethod("sally")); Thread thread2 = new Thread(() -> synDemo.stringMethod("sally")); thread1.start(); thread2.start(); } --------------------- //输出: Thread-0 Thread-0 Thread-0 Thread-0 ...死循环... //分析:输出结果永远都是Thread-0的死循环,也就是说另一个线程,即Thread-1线程根本不会运行 //原因:同步块中的锁是同一个字面量 复制代码
隐患:当使用不可变类对象(finalClass)作为对象锁时,使用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作为对象锁时但还有计算操作就会出现并发问题 复制代码我们通过反编译发现执行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一个新的锁有木有!!! } 复制代码
死锁:当线程间需要相互等待对方已持有的锁时,就形成死锁,进而产生死循环
public static void main(String[] args) { Object lock = new Object(); Object lock2 = new Object(); Thread thread1 = new Thread(() -> { synchronized (lock){ System.out.println(Thread.currentThread().getName() + "获取到lock锁"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ System.out.println(Thread.currentThread().getName() + "获取到lock2锁"); } } }); Thread thread2 = new Thread(() -> { synchronized (lock2){ System.out.println(Thread.currentThread().getName() + "获取到lock2锁"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock){ System.out.println(Thread.currentThread().getName() + "获取到lock锁"); } } }); thread1.start(); thread2.start(); } --------------------- //输出: Thread-1获取到lock2锁 Thread-0获取到lock锁 ..... //分析:线程0获得lock锁,线程1获得lock2锁,但之后由于两个线程还要获取对方已持有的锁,但已持有的锁都不会被双方释放,线程"假死",无法往下执行,从而形成死循环,即死锁,之后一直在做无用的死循环,严重浪费系统资源 复制代码
我们用 jstack 查看一下这个任务的各个线程运行情况,可以发现两个线程都被阻塞 BLOCKED
我们很明显的发现,Java-level=deadlock,即死锁,两个线程相互等待对方的锁
为了能直观了解Synchronized的工作原理,我们通过反编译SynchronizedDeme类的class文件的方式看看都发生了什么
public class SynchronizedDemo { public static synchronized void staticMethod() throws InterruptedException { System.out.println("静态同步方法开始"); Thread.sleep(1000); System.out.println("静态同步方法结束"); } public synchronized void method() throws InterruptedException { System.out.println("实例同步方法开始"); Thread.sleep(1000); System.out.println("实例同步方法结束"); } public synchronized void method2() throws InterruptedException { System.out.println("实例同步方法2开始"); Thread.sleep(3000); System.out.println("实例同步方法2结束"); } public static void main(String[] args) { final SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { try { synDemo.method(); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread thread2 = new Thread(() -> { try { synDemo.method2(); } catch (InterruptedException e) { e.printStackTrace(); } }); thread1.start(); thread2.start(); } } 复制代码
javac -encoding UTF-8 SynchronizedDemo.java
最终我们将得到一个 .class 文件,即 SynchronizedDemo.class
javap -v SynchronizedDemo
复制代码通过反编译我们会得到常量池、同步方法、同步代码块的不同编译结果
常量池除了会包含基本类型和字符串及数组的常量值外,还包含以文本形式出现的符号引用:
类和接口的全限定名 字段的名称和描述符 方法和名称和描述符 复制代码
同步方法会包含一个ACC_SYNCHCRONIZED标记符
同步代码块会在代码中插入 monitorenter 和 monitorexist 指令
每个对象都有一个监视器。当该监视器被占用时即是锁定状态(或者说获取监视器即是获得同步锁)。线程执行monitorenter指令时会尝试获取监视器的所有权,过程如下:
执行monitorexit指令将遵循以下步骤:
由于 wait/notify 等方法底层实现是基于监视器,因此只有在同步方法(块)中才能调用wait/notify等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因
区别于同步代码块的监视器实现,同步方法通过使用 ACC_SYNCHRONIZED 标记符隐示的实现原理是通过方法调用指令检查该方法在常量池中是否包含 ACC_SYNCHRONIZED 标记符,如果有,JVM 要求线程在调用之前请求锁
Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是互斥和信号机制
在 Monitor Object 模式中,主要有四种类型的参与者:
同步方法的调用和串行化:
同步方法线程挂起:
监视条件通知:
同步方法线程恢复:
在JVM中,对象在内存中的布局分成三块区域:对象头、示例数据和对齐填充
对象头: 对象头主要存储 Mark Word(对象的hashCode、锁信息)、类型指针、数组长度(若是数组的话)等信息
示例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组长度,这部分内存按4字节对齐
填充数据:由于JVM要求对象起始地址必须是8字节的整数倍,当不满足8字节时会自动填充(因此填充数据并不是必须的,仅仅是为了字节对齐)
synchcronized的锁是存放在Java对象头中的
如果对象是数组类型,JVM用3个字宽(Word)存储对象头,否则是用2个子宽在32位虚拟机中,1字宽等于4个字节,即32bit;64位的话就是8个字节,即64bit
32位JVM的Mark Word的默认存储结构(无锁状态)
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化(32位)
64位JVM的Mark Word的默认存储结构(对于32位无锁状态,有25bit没有使用)
公平锁就是获得锁的顺序按照先到先得的原则,从实现上说,要求当一个线程竞争某个对象锁时,只要这个锁的等待队列非空,就必须把这个线程阻塞并塞入队尾(插入队尾一般通过一个CAS操作保持插入过程中没有锁释放)
相对的,非公平锁场景下,每个线程都先要竞争锁,在竞争失败或当前已被加锁的前提下才会被塞入等待队列,在这种实现下,后到的线程有可能无需进入等待队列直接竞争到锁(随机性)
/** * StringBuffer是线程安全的字符串处理类 * 每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁 */ StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("kira"); stringBuffer.append("sally"); stringBuffer.append("mengmeng"); } 复制代码
/** * 比如执行10000次字符串的拼接 */ public static void main(String[] args) { SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); for (int i = 0 ; i < 10000 ; i++){ synchronizedDemo.append("kira","sally"); } } public void append(String str1,String str2){ //由于StringBuffer对象被封装在方法内部,不可能存在共享资源竞争的情况 //因此JVM会认为该加锁是无意义的,会在编译期就删除相关的加锁操作 //还有一点特别要注明:明知道不会有线程安全问题,代码阶段就应该使用StringBuilder //否则在没有开启锁消除的情况下,StringBuffer不会被优化,性能可能只有StringBuilder的1/3 StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(str1).append(str2); }/** 复制代码
从JDK1.6开始,锁一共有四种状态:无锁状态、偏向锁状态、轻量锁状态、重量锁状态
锁的状态会随着竞争情况逐渐升级,锁允许升级但不允许降级
不允许降级的目的是提高获得锁和释放锁的效率
后面会通过倒序的方式,即重量级锁->轻量级锁->偏向锁进行讲解,因为通常后者是前者的优化
锁的升级过程
线程1和线程2同时争夺锁,并导致锁膨胀成重量级锁
隐患:对于轻量级锁有个使用前提是"没有多线程竞争环境",一旦越过这个前提,除了互斥开销外,还会增加额外的CAS操作的开销,在多线程竞争环境下,轻量级锁甚至比重量级锁还要慢
线程1演示了偏向锁的初始化过程,线程2演示了偏向锁的撤销锁过程
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的线程才会释放锁
偏向锁的撤销需要等待全局安全点(该时间点上没有字节码正在执行)
偏向锁的撤销需要遵循以下步骤: -
要么重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁)
要么恢复到无锁或者标记锁对象不适合作为偏向锁(此时锁会被升级为轻量级锁)
最后唤醒暂停的线程,被阻塞在安全点的线程继续往下执行同步代码块