synchronized的三种应用方式
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
public synchronized void method(){
// todo
}
- 修饰静态方法,作用于当前类加锁,进入同步代码前要获得当前类的锁。
public synchronized static void method(){
// todo
}
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象。
public void method(){
synchronized(this) {
// todo
}
}
synchronized的实现
对于同步方法和同步代码块,synchronized实现有所差别。
我们首先对同步方法进行反编译
public class SynchronizedTest {
private static int count = 0;
public void method() {
synchronized (this) {
count++;
}
}
public static synchronized void inc() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
inc();
}
}).start();
}
Thread.sleep(3000);
System.out.println("运行结果" + count);
}
}
反编译的结果如图所示
从反编译的结果中我们可以看到monitorenter和monitorexit两条指令。
对于monitorenter, 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
对于monitorexit, 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
对于同步方法反编译结果入下图
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
总结
Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。