synchronized的原理

import java.util.concurrent.TimeUnit; public class WaitNotify { static boolean flag = true; static Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread A = new Thread(new Wait(), "wait thread"); A.start(); TimeUnit.SECONDS.sleep(2); Thread B = new Thread(new Notify(), "notify thread"); B.start(); } static class Wait implements Runnable { @Override public void run() { synchronized (lock) { while (flag) { try { System.out.println(Thread.currentThread() + " flag is true"); lock.wait(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread() + " flag is false"); } } } static class Notify implements Runnable { @Override public void run() { synchronized (lock) { flag = false; lock.notifyAll(); try { TimeUnit.SECONDS.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } } }

其相关方法定义在java.lang.Object上,线程A在获取锁后调用了对象lock的wait方法进入了等待状态,线程B调用对象lock的notifyAll()方法,线程A收到通知后从wait方法处返回继续执行,线程B对共享变量flag的修改对线程A来说是可见的。

整个运行过程需要注意一下几点:

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁,调用wait()方法后会释放锁。
  2. 调用wait()方法之后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列中。
  3. notify()或notifyAll()方法调用后,等待线程不会立刻从wait()中返回,需要等该线程释放锁之后,才有机会获取锁之后从wait()返回。
  4. notify()方法将等待队列中的一个等待线程从等待队列中移动到同步队列中;notifyAll()方法则是把等待队列中的所有线程都移动到同步队列中;被移动的线程状态从WAITING变为BLOCKED。
  5. 从wait()方法返回的前提是,改线程获得了调用对象的锁。

synchronized保证三大特性

synchronized保证原子性的原理

对num++;增加同步代码块后,保证同一时间只有一个线程操作num++;。就不会出现安全问题。

synchronized保证可见性的原理

synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变 量的值。

synchronized保证有序性的原理

我们加synchronized后,依然会发生重排序,只不过我们有同步 代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性。

synchronized深入理解

monitor机制

Monitor 被翻译为监视器或管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

Monitor 结构如下
synchronized的原理
这里简单介绍一下

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。

在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
synchronized的原理
synchronized的原理

klass pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的 实例。

实例数据

就是类中定义的成员变量。

对齐填充

对齐填充并不是必然存在的,也没有什么特别的意义,它仅仅起着占位符的作用。

monitorenter

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获 取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应 的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为 monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直 到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

monitorenter小结: synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个 同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有 这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待

monitorexit

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出 monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个 monitor的所有权

monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

总结:synchronized在修饰代码块时,是通过monitorentermonitorexit来保证并发安全。

synchronized代码块底层原理

现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下

public class SyncCodeBlock {     public int i;     public void syncTask(){        //同步代码库        synchronized (this){            i++;        }    } } 

编译上述代码并使用javap反编译后得到字节码如下(这里我们省略一部分没有必要的信息):

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class   Last modified 2017-6-2; size 426 bytes   MD5 checksum c80bc322c87b312de760942820b4fed5   Compiled from "SyncCodeBlock.java" public class com.zejian.concurrencys.SyncCodeBlock   minor version: 0   major version: 52   flags: ACC_PUBLIC, ACC_SUPER Constant pool:   //........省略常量池中数据   //构造函数   public com.zejian.concurrencys.SyncCodeBlock();     descriptor: ()V     flags: ACC_PUBLIC     Code:       stack=1, locals=1, args_size=1          0: aload_0          1: invokespecial #1                  // Method java/lang/Object."<init>":()V          4: return       LineNumberTable:         line 7: 0   //===========主要看看syncTask方法实现================   public void syncTask();     descriptor: ()V     flags: ACC_PUBLIC     Code:       stack=3, locals=3, args_size=1          0: aload_0          1: dup          2: astore_1          3: monitorenter  //注意此处,进入同步方法          4: aload_0          5: dup          6: getfield      #2             // Field i:I          9: iconst_1         10: iadd         11: putfield      #2            // Field i:I         14: aload_1         15: monitorexit   //注意此处,退出同步方法         16: goto          24         19: astore_2         20: aload_1         21: monitorexit //注意此处,退出同步方法         22: aload_2         23: athrow         24: return       Exception table:       //省略其他字节码....... } SourceFile: "SyncCodeBlock.java" 

我们主要关注字节码中的如下代码

3: monitorenter  //进入同步方法 //..........省略其他   15: monitorexit   //退出同步方法 16: goto          24 //省略其他....... 21: monitorexit //退出同步方法 

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

synchronized方法底层原理

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

public class SyncMethod {     public int i;     public synchronized void syncTask(){            i++;    } } 

使用javap反编译后的字节码如下:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class   Last modified 2017-6-2; size 308 bytes   MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94   Compiled from "SyncMethod.java" public class com.zejian.concurrencys.SyncMethod   minor version: 0   major version: 52   flags: ACC_PUBLIC, ACC_SUPER Constant pool;     //省略没必要的字节码   //==================syncTask方法======================   public synchronized void syncTask();     descriptor: ()V     //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法     flags: ACC_PUBLIC, ACC_SYNCHRONIZED     Code:       stack=3, locals=1, args_size=1          0: aload_0          1: dup          2: getfield      #2                  // Field i:I          5: iconst_1          6: iadd          7: putfield      #2                  // Field i:I         10: return       LineNumberTable:         line 12: 0         line 13: 10 } SourceFile: "SyncMethod.java" 

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Javasynchronized的原理

synchronized的其他优化

锁消除

对不会逃逸到其他线程中的变量,执行加锁操作,可以删除加锁。

public void add (String s1, String s2) { 	StringBuffer sb = new StringBuffer(); 	sb.append(s1).append(s2); } 

StringBuffer是线程安全的,被synchronized修饰过的,是同步的。我们发现,sb这个引用只会在append方法中使用,不可能被其他线程引用(因为是局部变量,栈私有),因此,sb是不可能共享资源的,JVM会自动消除StringBuffer对象内部的锁。

锁粗化

多个 synchronized连续执行加锁、释放锁,导致线程发生多次重入,可以合并为一个。

public String test (String s) { 	int i = 0; 	StringBuffer sb = new StringBuffer(); 	while (i < 100) { 		sb.append(s); 		i++; 	} 	return sb.toString(); } 

JVM会检测到这样一连串的操作都对同一个对象加锁(while循环内100次执行append,没有锁粗化得话就要执行100次加锁/解锁),此时JVM就会将加锁的范围粗化到这一连串的操作的外部(比如while虚幻体外),使得这一连串操作只需要加一次锁即可。

版权声明:玥玥 发表于 2021-05-08 13:38:23。
转载请注明:synchronized的原理 | 女黑客导航