【读源码】Synchronized原理分析-Java线程互斥同步

文章目录

  • 一、Synchronized的使用
      • 1.1对象锁
      • 1.2类锁
  • 二、Synchronized原理分析
      • 2.1加锁和释放锁的原理
      • 2.2可重入原理:加锁次数计数器
      • 2.3保证可见性的原理:内存模型和happens-before规则:

一、Synchronized的使用

使用synchronized关键字要注意以下几点:

  1. 一把锁只能同时被一个线程获取,没有锁只能等待
  2. 每个实例都有自己的一把锁synchronized(this){},不同实例互不影响,同一对象不同实例不影响,
    不同对象的实例更不影响。
    但如果锁对象是synchronized(A.class){}的方式去加锁的,以及synchronized修饰的是static方法的时候,
    所有同一对象不同实例会共用同一把锁。
  3. synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁

1.1对象锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)		
  • 方法锁形式:synchronized修饰普通方法,锁对象默认为this
import lombok.SneakyThrows;

public class SynchronizedLock implement Runnable{
	static SynchronizedLock instance = new SynchronizedLock();
	
	@SneakyThrows
	@Override
	public void run(){
		method();
	}
	
	private void synchronized method(){
		System.out.print(Thread.current.getName() + ": 工作中...")
	}
}
  • 代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁
    • this
    import lombok.SneakyThrows;
    
    public class SynchronizedLock implement Runnable{
    	static SynchronizedLock instance = new SynchronizedLock();
    	
    	@SneakyThrows
    	@Override
    	public void run(){
    		synchronized(this){
    			System.out.print(Thread.current.getName() + ": 工作中...")
    		}
    	}
    }
    
    • 自定义的锁
    import lombok.SneakyThrows;
    
    public class SynchronizedLock implement Runnable{
    	static SynchronizedLock instance = new SynchronizedLock();
    	Object block = new Object();
    	
    	@SneakyThrows
    	@Override
    	public void run(){
    		synchronized(block){
    			System.out.print(Thread.current.getName() + ": 工作中...")
    		}
    	}
    }
    

1.2类锁

指synchronize修饰静态的方法或指定锁对象为Class对象
  • synchronize修饰静态方法
import lombok.SneakyThrows;
	
public class SynchronizedLock implement Runnable{

    @Override
    public void run() {
        method();
    }

    // synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把
    public static synchronized void method() {
        System.out.println("我是线程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }
}
  • synchronized指定锁对象为Class对象
public class SynchronizedLock implements Runnable {

    @Override
    public void run() {
        // 所有线程需要的锁都是同一把
        synchronized(SynchronizedLock.class){
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    }
}

二、Synchronized原理分析

2.1加锁和释放锁的原理

现象、时机(内置锁this)、深入JVM看字节码(反编译看monitor指令)

深入JVM看字节码,创建如下的代码:

public class SynchronizedDemo {

    Object objectA = new Object();
    Object objectB = new Object();

    int i = 0;

    public void method1() {
        synchronized (objectA) {
            i++;
        }
        synchronized (objectB) {
            i++;
        }
    }
}

这是一个带有两个不同对象锁的同步代码块的方法method1()的类,我们主要关注method1()的字节码实现,因为synchronized是JVM实现的,所以当java经过jvm编译后就会表现出底层控制锁的逻辑

接下来到该SynchronizedDemo.java目录下,打开命令窗口,
第一步执行javac SynchronizedDemo.java 编译java文件为.class 文件
运行无报错,则执行第二步执行javap -verbose SynchronizedDemo.class 会在idea控制台打印如下图:

Classfile /X:/文件路径信息脱敏/SynchronizedDemo.class
  Last modified 2024-1-8; size 619 bytes
  MD5 checksum e5e4bc0f1de27ff725b9e76ca5117e21
  Compiled from "SynchronizedDemo.java"
public class com.health.im.lock.SynchronizedDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#23         // java/lang/Object."":()V
   #2 = Class              #24            // java/lang/Object
   #3 = Fieldref           #6.#25         // com/health/im/lock/SynchronizedDemo.objectA:Ljava/lang/Object;
   #4 = Fieldref           #6.#26         // com/health/im/lock/SynchronizedDemo.objectB:Ljava/lang/Object;
   #5 = Fieldref           #6.#27         // com/health/im/lock/SynchronizedDemo.i:I
   #6 = Class              #28            // com/health/im/lock/SynchronizedDemo
   #7 = Utf8               objectA
   #8 = Utf8               Ljava/lang/Object;
   #9 = Utf8               objectB
  #10 = Utf8               i
  #11 = Utf8               I
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               method1
  #17 = Utf8               StackMapTable
  #18 = Class              #28            // 文件路径信息脱敏/SynchronizedDemo
  #19 = Class              #24            // java/lang/Object
  #20 = Class              #29            // java/lang/Throwable
  #21 = Utf8               SourceFile
  #22 = Utf8               SynchronizedDemo.java
  #23 = NameAndType        #12:#13        // "":()V
  #24 = Utf8               java/lang/Object
  #25 = NameAndType        #7:#8          // objectA:Ljava/lang/Object;
  #26 = NameAndType        #9:#8          // objectB:Ljava/lang/Object;
  #27 = NameAndType        #10:#11        // i:I
  #28 = Utf8               com/health/im/lock/SynchronizedDemo
  #29 = Utf8               java/lang/Throwable
{
  java.lang.Object objectA;
    descriptor: Ljava/lang/Object;
    flags:

  java.lang.Object objectB;
    descriptor: Ljava/lang/Object;
    flags:

  int i;
    descriptor: I
    flags:

  public com.health.im.lock.SynchronizedDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."":()V
        12: putfield      #3                  // Field objectA:Ljava/lang/Object;
        15: aload_0
        16: new           #2                  // class java/lang/Object
        19: dup
        20: invokespecial #1                  // Method java/lang/Object."":()V
        23: putfield      #4                  // Field objectB:Ljava/lang/Object;
        26: aload_0
        27: iconst_0
        28: putfield      #5                  // Field i:I
        31: return
      LineNumberTable:
        line 3: 0
        line 5: 4
        line 6: 15
        line 8: 26

  public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: aload_0
         1: getfield      #3                  // Field objectA:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: aload_0
         8: dup
         9: getfield      #5                  // Field i:I
        12: iconst_1
        13: iadd
        14: putfield      #5                  // Field i:I
        17: aload_1
        18: monitorexit
        19: goto          27
        22: astore_2
        23: aload_1
        24: monitorexit
        25: aload_2
        26: athrow
        27: aload_0
        28: getfield      #4                  // Field objectB:Ljava/lang/Object;
        31: dup
        32: astore_1
        33: monitorenter
        34: aload_0
        35: dup
        36: getfield      #5                  // Field i:I
        39: iconst_1
        40: iadd
        41: putfield      #5                  // Field i:I
        44: aload_1
        45: monitorexit
        46: goto          54
        49: astore_3
        50: aload_1
        51: monitorexit
        52: aload_3
        53: athrow
        54: return
      Exception table:
         from    to  target type
             7    19    22   any
            22    25    22   any
            34    46    49   any
            49    52    49   any
      LineNumberTable:
        line 11: 0
        line 12: 7
        line 13: 17
        line 14: 27
        line 15: 34
        line 16: 44
        line 17: 54
      StackMapTable: number_of_entries = 4
        frame_type = 255 /* full_frame */
          offset_delta = 22
          locals = [ class 文件路径信息脱敏/SynchronizedDemo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
        frame_type = 255 /* full_frame */
          offset_delta = 21
          locals = [ class 文件路径信息脱敏/SynchronizedDemo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "SynchronizedDemo.java"

这一长串代码不用慌,可以直接找method1() 方法

public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: aload_0
         1: getfield      #3                  // Field objectA:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: aload_0
         8: dup
         9: getfield      #5                  // Field i:I
        12: iconst_1
        13: iadd
        14: putfield      #5                  // Field i:I
        17: aload_1
        18: monitorexit
        19: goto          27
        22: astore_2
        23: aload_1
        24: monitorexit
        25: aload_2
        26: athrow
        27: aload_0
        28: getfield      #4                  // Field objectB:Ljava/lang/Object;
        31: dup
        32: astore_1
        33: monitorenter
        34: aload_0
        35: dup
        36: getfield      #5                  // Field i:I
        39: iconst_1
        40: iadd
        41: putfield      #5                  // Field i:I
        44: aload_1
        45: monitorexit
        46: goto          54
        49: astore_3
        50: aload_1
        51: monitorexit
        52: aload_3
        53: athrow
        54: return

其中有,2个monitorenter指令和4个monitorexit指令。我们来辨认第一个同步代码块:

 0: aload_0
         1: getfield      #3                  // Field objectA:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: aload_0
         8: dup
         9: getfield      #5                  // Field i:I
        12: iconst_1
        13: iadd
        14: putfield      #5                  // Field i:I
        17: aload_1
        18: monitorexit
        19: goto          27
        22: astore_2
        23: aload_1
        24: monitorexit
        25: aload_2
        26: athrow

第0行: 0: aload_0 加载当前帧的局部变量0(即this)到栈顶。
第1行:1: getfield #3 获取类实例的object字段。
第4行:4: dup 复制栈顶的对象引用。
第5行:5: astore_1 将栈顶的对象引用存储到局部变量1中。
第6行:6: monitorenter 进入同步块,使用object作为锁,即代码中的objectA
第7行:7: aload_1 加载局部变量1到栈顶。
第8行:8: monitorexit 正常退出同步块。
第9行:9: goto 17无条件跳转到标记17的代码位置,直接跳转到下一个代码块
这里,如果在同步块中抛出异常,JVM将确保监视器被正确释放,这是通过在异常处理路径中放置monitorexit指令来实现的。
第12行:12: astore_2 将栈顶的对象引用存储到局部变量2中。
第13行:13: aload_1 加载局部变量1到栈顶。
第14行:14: monitorexit 退出同步块
第15行:15: aload_2 加载局部变量2到栈顶。
第16行:16: athrow 抛出栈顶的异常。
这是第一个同步块的异常处理路径,确保在异常抛出时,锁被释放,这实现了synchronized自动释放锁。

其中monitorEntermonitorexit会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

  1. monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  2. 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
  3. 这把锁已经被别的线程获取了,等待锁释放。

下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
【读源码】Synchronized原理分析-Java线程互斥同步_第1张图片
该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

2.2可重入原理:加锁次数计数器

可重入:即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。看如下的例子


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SynchronizedDemo1 {
    public static void main(String[] args) {
        SynchronizedDemo1 demo =  new SynchronizedDemo1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(() -> {
            try {
                demo.method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        executorService.submit(() -> {
            try {
                demo.method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        executorService.shutdown();
    }

    private synchronized void method1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ": method1()");
        Thread.sleep(1000);
        method2();
    }

    private synchronized void method2() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ": method2()");
        Thread.sleep(1000);
        method3();
    }

    private synchronized void method3() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ": method3()");
        Thread.sleep(8000);
    }
}

运行结果为:

pool-1-thread-1: method1()
pool-1-thread-1: method2()
pool-1-thread-1: method3()
pool-1-thread-2: method2()
pool-1-thread-2: method3()

就是说在递归调用的情况下,第一个执行的demo.method1() 时获取了锁,之后线程1就一直重用一把锁。
这就是Synchronized的重入性,即在同一锁程中,每个对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会加一,释放锁后就会将monitor计数器减一,线程不需要再次获取同一把锁。

2.3保证可见性的原理:内存模型和happens-before规则:

Happens-Before规则是JMM中定义的一种偏序关系,它指定了一些规则,使得我们可以在多线程环境中确定内存操作的顺序。
如果一个操作A happens-before另一个操作B,那么A的结果对于B是可见的,并且A在B之前发生。

Happens-Before规则包括:

  1. 程序次序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile字段的写操作,happens-before于任意后续对这个volatile字段的读操作。
  4. 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C。
  5. 线程启动规则:Thread对象的start()方法 happens-before于此线程的每个动作。
  6. 线程终止规则:线程中的任何操作都happens-before于其他线程检测到这个线程已经终止的动作,或者从Thread.join()方法成功返回。
  7. 线程中断规则:对线程interrupt()的调用,happens-before于被中断线程检测到中断事件的发生。
  8. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happens-before于它的finalize()方法的开始。

如何保证可见性:
JMM通过Happens-Before规则来保证可见性。当一个线程修改了一个共享变量,并且这个新值被写回主内存后,其他线程可以通过以下方式看到这个修改:

  1. 通过synchronized关键字:当一个线程进入一个synchronized块并获取锁时,它会清空工作内存中的共享变量值,从主内存中重新读取。当线程退出synchronized块时,它会将工作内存中的共享变量的最新值刷新回主内存
  2. 通过volatile关键字:当一个线程读取一个volatile变量时,它会直接从主内存中读取这个变量的值,而不是从工作内存中的缓存读取。同样,当一个线程写入一个volatile变量时,它会直接写入主内存,而不是写入工作内存中的缓存。
  3. 通过Happens-Before规则:如果一个写操作A happens-before于一个读操作B,那么B能够看到A的结果。
    通过这些机制,JMM确保了在多线程环境中,共享变量的修改能够被其他线程正确地看到,从而保证了可见性。

你可能感兴趣的:(Java源码阅读,java,开发语言,jvm)