synchronized 简介
synchronized 是 Java 中的一种同步机制,用于防止多个线程同时访问共享资源。它可以用于方法和代码块,确保同一时间只有一个线程可以执行被 synchronized 保护的代码,同时还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。
synchronized 的特性
原子性:synchronized 保证了被保护的代码块在一个线程执行期间不会被其他线程中断。
可见性:synchronized 确保在锁释放前对共享变量的修改对其他线程可见。
有序性:synchronized保证同一时刻只能有一个线程进行操作,也就保证了有序性,有效解决指令重排对程序的影响。
重入性:同一个线程可以多次获取同一把锁而不会被阻塞。
synchronized的使用
同步实例方法
1 | public class ThreadTest implements Runnable{ |
修饰静态方法
1 | public class ThreadTest { |
修饰代码块
1 | public class ThreadTest implements Runnable{ |
synchronized修饰的是实例方法时,获得该实例对象的内置锁。synchronized修饰的时静态方法时,获得类锁,即当前类的Class对象的内置锁。synchronized修饰的是代码块时,根据括号中的对象或者类,获得相应的对象内置锁或者类锁。- 每个类都有一个类锁,类的每个对象都有一个内置锁,它们是互不干扰的,也就是说一个线程可以同时获得类锁和该类实例化对象的内置锁。
synchronized的实现原理
synchronized 的底层实现依赖于 JVM 中的对象头和 Monitor对象。
对象头:每个 Java 对象都有一个对象头,包含了锁标志位和指向
Monitor对象的指针。Monitor对象:
Monitor是 JVM 内部的一个同步工具,用于管理线程的同步。
锁的状态
无锁状态:对象未被锁定。
偏向锁:锁偏向于第一个获取它的线程,降低了无竞争情况下的加锁和解锁成本。
轻量级锁:多个线程竞争锁时,锁会膨胀为轻量级锁。
重量级锁:竞争激烈时,锁会膨胀为重量级锁,使用操作系统的
Mutex进行管理。
锁的强度会随着多线程竞争的激烈而逐渐升级。
synchronized的字节码实现
通过下面的测试代码来分析:
1
2
3
4
5
6
7
8
9
10
11public class SynchronizedDemo {
private int i=1;
public void test1(){
synchronized (this){
i++;
}
}
public synchronized void test2(){
i++;
}
}执行
javap -verbose SynchronizedDemo.class反编译
其中test1()方法的字节码如下:
1 | public void test1(); |
test1()使用了synchronized代码块,字节码中通过monitorenter指令获取锁,通过monitorexit指令释放锁。注意到其中只有1个monitorenter但是有2个monitorexit,因为synchronized是JVM层面的锁,如果发生异常了JVM会自动帮我们释放锁,而Lock异常则需要我们手动捕获并释放。
关于这两条指令的作用,我们直接参考JVM规范中的描述:
monitorenter
1 | Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: |
monitorexit
1 | The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. |
通过这两段描述,我们应该能很清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成,其实wait()/notify()等方法也依赖于monitor对象,这就是为什么只有在同步块或者方法中才能调用wait()/notify()等方法,否则会抛出java.lang.IllegalMonitorStateException异常的原因。
test2()方法的字节码如下:
1 | public synchronized void test2(); |
test2()使用了synchronized方法,虽然在字节码中并没有看到monitorenter和monitorexit指令,但在字节码常量池中多了ACC_SYNCHRONIZED标志符,JVM就是根据该标示符来实现方法同步的。当方法调用时,调用指令会检查该标志符是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现。
总结
synchronized是Java并发编程中最常用的用于保证线程安全的方式,使用起来非常简单和方便。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助于我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。