- synchronized 是 Java 的一个关键字,它能够将代码块 (方法) 锁起来。
- synchronized 是 互斥锁,同一时间只能有一个线程进入被锁住的代码块(方法)。
- synchronized 通过监视器(Monitor)实现锁。java 一切皆对象,每个对象都有一个监视器(锁标记),而 synchronized 就是使用对象的监视器来将代码块 (方法) 锁定的!
为什么用 Synchronized ?
我们加锁的原因是为了线程安全,而线程安全最重要就是保证原子性和可见性。
- 被 Synchronized 修饰的代码块(方法),同一时间只能有一个线程执行,从而保证原子性。
- synchronized 通过使用监视器,来实现对变量的同步操作,保证了其他线程对变量的可见性。
怎么用 Synchronized ?
- 修饰普通同步方法:锁是当前实例对象
- 修饰静态同步方法:锁是当前类的 Class 对象
- 修饰同步代码块:
修饰普通同步方法
public class BigBigDog { // 修饰普通同步方法,普通方法属于实例对象 // 锁是当前实例对象 BigBigDog 的监视器 public synchronized void testCommon(){ // doSomething } }
多个实例对象调用不会阻塞,比如:
public class BigBigDog { // 修饰普通同步方法,普通方法属于实例对象 // 锁是当前实例对象 BigBigDog 的监视器 public synchronized void testCommon() { int i = 0; do { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Common function is locked " + i); } while (i++ < 10); } }
测试方法:
public class Main { public static void main(String[] args) { BigBigDog bigBigDog = new BigBigDog(); BigBigDog bigBigDog1 = new BigBigDog(); new Thread(bigBigDog::testCommon).start(); new Thread(bigBigDog1::testCommon).start(); } }
结果:异步运行,因为锁的是实例对象,也就是锁不同,所以并不会阻塞。
Common function is locked 0 Common function is locked 0 Common function is locked 1 Common function is locked 1 Common function is locked 2 Common function is locked 2 Common function is locked 3 Common function is locked 3 ···
修饰静态同步方法
public class BigBigDog { // 修饰静态同步方法,静态方法属于类(粒度比普通方法大) // 锁是类的锁(类的字节码文件对象:BigBigDog.class) public static synchronized void testStatic() { // doSomething } }
synchronized 修饰静态方法获取的是类锁 (类的字节码文件对象),synchronized 修饰普通方法获取的是对象锁。也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!测试下:
public class BigBigDog { // 修饰普通同步方法,普通方法属于实例对象 // 锁是当前实例对象 BigBigDog 的监视器 public synchronized void testCommon() { int i = 0; do { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Common function is locked " + i); } while (i++ < 10); } // 修饰静态同步方法,静态方法属于类(粒度比普通方法大) // 锁是类的锁(类的字节码文件对象:BigBigDog.class) public static synchronized void testStatic() { int i = 0; do { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Static function is locked " + i); } while (i++ < 10); } }
public class Main { public static void main(String[] args) { BigBigDog bigBigDog = new BigBigDog(); new Thread(bigBigDog::testCommon).start(); new Thread(BigBigDog::testStatic).start(); } }
结果:异步运行,并不冲突。
Common function is locked 0 Static function is locked 0 Common function is locked 1 Static function is locked 1 Common function is locked 2 Static function is locked 2 Common function is locked 3 Static function is locked 3
修饰同步代码块
public class BigBigDog { public void test3() { // 修饰代码块,锁是括号内的对象 // 这里的 this 是当前实例对象 BigBigDog 的监视器 synchronized (this) { // doSomething } } }
public class BigBigDog { // 使用 object 的监视器作为锁 private final Object object = new Object(); public void test4() { // 修饰代码块,锁是括号内的对象 // 这里是当前实例对象 object 的监视器 synchronized (object) { // doSomething } } }
除了第一种以 this 当前对象的监视器为锁的情况。对于同步代码块,Java 还支持它持有任意对象的锁,比如第二种的 object 。那么这两者有何区别?这两者并无本质区别,但是为了代码的可读性。还是更加建议用第一种(第二种,无缘无故定义一个对象)。
Synchronized 的原理
有以下代码:test 是静态同步方法,test1 是普通同步方法,test2 则是同步代码块。
public class SynchronizedTest { // 修饰静态方法 public static synchronized void test() { // doSomething } // 修饰方法 public synchronized void test1(){ } public void test2(){ // 修饰代码块 synchronized (this){ } } }
通过命令看下 synchronized 关键字到底做了什么事情:首先用 cd 命令切换到 SynchronizedTest.java 类所在的路径,然后执行 javac SynchronizedTest.java,于是就会产生一个名为 SynchronizedTest.class 的字节码文件,然后我们执行 javap -c SynchronizedTest.class,就可以看到对应的反汇编内容,如下:
Z:\IDEAProject\review\review_java\src\main\java\com\nasus\thread\lock>javac -encoding UTF-8 SynchronizedTest.java Z:\IDEAProject\review\review_java\src\main\java\com\nasus\thread\lock>javap -c SynchronizedTest.class Compiled from "SynchronizedTest.java" public class com.nasus.thread.lock.SynchronizedTest { public com.nasus.thread.lock.SynchronizedTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static synchronized void test(); Code: 0: return public synchronized void test1(); Code: 0: return public void test2(); Code: 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 Exception table: from to target type 4 6 9 any 9 12 9 any }
test2 同步代码块解析
主要看 test2 同步代码块的反编译内容,可以看出 synchronized 多了 monitorenter 和 monitorexit 指令。把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。
那这里为啥只有一次 monitorenter 却有两次 monitorexit ?
- JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁。
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
a. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。b. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。c. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
monitorexit
monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。
test1 普通同步方法
它并不是依靠 monitorenter 和 monitorexit 指令实现的,从上面的反编译内容可以看到,synchronized 方法和普通方法大部分是一样的,不同在于,这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。(在这看不出来需要看 JVM 底层实现)
当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。
PS:想要进一步深入了解 synchronized 就必须了解 monitor 对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。可以参考这篇博客:@chenssy 大神写的很好,建议拜读下。