你必须知道的Synchronized (前篇:底层实现)

1,072 阅读4分钟

关键字Synchronized的作用是实现线程间的同步,下面就简称sync。

sync要聊的东西太多了,本节先聊sync的底层实现和简单区别,后面还会写出Synchronized的锁升级过程和相关优化。

1.sync的使用

`

private static CountDownLatch latch = new CountDownLatch(100);    
public static void main(String[] args) throws Exception {        
    T t = new T();        
    for (int i = 0; i < 100; i++) {           
        new Thread(() -> {                
            for (int j = 0; j < 100; j++) {                    
             // 输出值 总是等于 10000
             // t.safeIncr();
             // t.safeIncrBlock();
             // 输出值总是小于 10000                    
                t.unSafeIncr();                
            }                
            latch.countDown();            
        }).start();        
    }        
    // 为了等待100个线程全都执行完成        
    latch.await();        
    System.out.println(t.count);    
}    
private static class T {       
    private int count;        
    public void unSafeIncr(){            
        count ++;        
    }  
    public void safeIncrBlock(){
        synchronized(this){
            count ++;
        }
    }
    public synchronized void safeIncr(){            
        count ++;        
    }    
}

`

上面的例子展示了sync的两种使用方式,即:

1、在方法签名加上sync关键字

2、使用sync代码块

两种方式都能达到 最后想要的结果,count=10000

2.sync的底层实现

这里分两个方面来讲:

1.从底层实现上来说:

同步代码块:采用monitorentermonitorexit两个指令来实现同步

同步方法: 采用ACC_SYNCHRONIZED标记符来实现同步

1.同步代码块

把上面代码 使用 javap -c -v '.\Sync_Method_01$T.class' 来反编译看一下字节码:

public void safeIncrBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0				// 局部变量表第0位this压栈
         1: dup					// 复制this引用,并入栈
         2: astore_1			// 把this弹出栈,存入第一个局部变量
         3: monitorenter		// 根据this引用进行加锁
         4: aload_0			
         5: dup
         6: getfield      #1                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #1                  // Field count:I
        14: aload_1
        15: monitorexit			// 释放锁,在前面4-14行已经完成了 count ++ 的操作
        16: goto          24	// 跳转到 24行,退出
        19: astore_2			// 如果在 4-16 行遇到任何的Exception,则会进入到19行处理
        20: aload_1				// 第一个局部变量入栈
        21: monitorexit			// 根据 栈顶的 this 退出临界区,释放锁	
        22: aload_2				// 第二个局部变量入栈
        23: athrow				// 抛出异常
        24: return

通过字节码,可以看到两个指令:monitorenter 和 monitorexit, 在JVM规范中也有相应的解释,有兴趣的可以看一下,大概意思就是,JVM会通过sync锁住的对象去找到对应的monitor(可以理解为一个监视计数器),然后根据monitor的状态进行加解锁的判断,如果一个线程尝试获取锁,并且对象的monitor为0,则当前线程被准许执行后续代码,然后将monitor做一次自增,此时为1,如果当前线程又一次获取对象锁(sync是可重入锁),monitor继续做自增,此时为2,当线程执行完成任务之后,需要使用monitorexit声明退出释放锁,则monitor自减,如果在这中间有其他线程也来尝试获取锁,但是monitor不为0,则其他线程需要进行等待;在JVM中,任何一个对象自身都会有一个监视器与其关联,用来判断当前对象是否被锁定,当监视器被持有后,当前对象就会处于锁定的状态。

2.同步方法:

同样还是看反编译之后的字节码:

public synchronized void safeIncr();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED		// 同步方法修饰符
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #1                  // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #1                  // Field count:I
        10: return
      LineNumberTable:
        line 42: 0
        line 43: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Latomicity/juejin/Sync_Method_01$T;

JVM对同步方法的处理是通过使用 ACC_SYNCHRONIZED来支持的,可以看到同步方法经过反编译之后的字节码跟普通的无同步代码没什么区别,并没有显示的使用monitorenter 和 monitorexit来进行同步,对于同步方法来说,当JVM通过方法的标识符来判断它是不是同步方法的时候,会自动的在调用的方法前面进行加锁操作,当同步方法执行完之后,不管是正常的退出还是抛异常,都会由JVM来释放锁,也就是说,ACC_SYNCHRONIZED意味着JVM会隐式的(使用方法调用和返回指令)使用monitorenter和monitorexit

JVM规范原文如下:

3.sync的作用区别

1.sync指定加锁对象,线程进入同步代码块前,必须要获取给定的对象的锁

2.作用在普通方法上,相当于对当前对象实例进行加锁,线程如果要执行同步方法,必须获得当前对象实例的锁

3.作用在静态方法上,相当于对当前类加锁,线程如果要执行同步方法,必须获取当前类的锁

1,2很简单大家都懂,这里针对第三点 静态方法 举例说明:

public class Sync_Method_02 {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            T2.t1();
        });
        t1.start();
      
        // 这里测试 静态同步方法 和 普通同步方法 之间是否有竞争情况
        Thread t3 = new Thread(() -> new T2().t3());
        t3.start();
        
        Thread t4 = new Thread(() -> T2.t4());
        t4.start();
        
        // 为了确保 t1线程会先执行
        TimeUnit.SECONDS.sleep(1);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> T2.t2()).start();
        }
    }
    private static class T2 {
        
        public static synchronized void t1() {
            try {
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end...");
        }
        
        public static synchronized void t2() {
            System.out.println("t2 start");
        }
        
        public synchronized void t3(){
            try {
                System.out.println("t3 start");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3 end");
        }
        
        public static void t4() {
            // 相当于 静态同步方法
            synchronized (T2.class){
                System.out.println("t4 ...");
            }
        }
    }
}

运行上面的代码,可以发现 t1或者t3方法总是先进行打印的,也就是说当t1线程在执行 T2.t1() 同步方法时,t1会首先获取T2的锁,一秒过后其他10个线程开始执行T2.t2(), 但是此时锁被t1线程持有,所以其他10个线程必须等t1线程释放T2的锁,才有机会去执行。说明所有的静态同步方法用的也是同一把锁——类对象本身。

而静态同步方法和非静态同步方法这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞争的。

补充:

在sync方法或者代码块中,有代码执行出现异常而没有catch处理,则会释放锁,要想不释放锁,则需要catch异常处理

PS:原理就先写这么多吧,很多细节后面还会聊,下节会聊锁升级的详细过程和JVM相关优化节,小白第一次写技术博客,没经验,有问题的话,希望各位大神指出,有错必改!