源码角度理解Synchronized
- Synchronized加到方法上的用法
public class AccountingSync implements Runnable {
// 共享资源: 这里是静态资源才可以被多个线程操作
static int i = 0;
public synchronized void increase(){
++i;
}
public static synchronized void increase(){
++i;
}
@Override
public void run() {
for (int i = 0; i < 100000; ++i) {
increase();
}
}
public static void main(String[] args) {
AccountingSync async = new AccountingSync();
AccountingSync async1 = new AccountingSync();
Thread t1 = new Thread(async);
Thread t2 = new Thread(async1);
try{
t1.start(); // 当调用线程的t1.run()方法的时候其实并没有启动线程的操作,仅仅是函数调用而已。
t2.start();
t1.join();
t2.join();
} catch (InterruptedException e){
//
}
System.out.println(i);
}
}
当传入不同对象的时候,并不能起到加锁的目的。synchronized作用于非静态方法时,锁住的是实例,此时此实例的其它synchronized方法也不能被其他线程访问,但是非synchronized 方法可以被其他线程访问。当synchronized作用于静态方法时锁住的是类。
- synchronized加到代码块上的用法:这个时候要注意,锁住的对象是静态的,还是非静态,一般类中静态变量所有实例共享,但是非静态变量每个实例单独拥有。
public class AccountingSync implements Runnable {
static Object instance = new Object();
// Object instance = new Object(); 这样写会有问题
// 共享资源: 这里是静态资源才可以被多个线程操作
static int i = 0;
public static synchronized void staticIncrease(){
++i;
}
@Override
public void run() {
synchronized (instance) {
for (int j = 0; j < 100000; ++j) {
++i;
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new AccountingSync());
Thread t2 = new Thread(new AccountingSync());
try{
t1.start(); // 当调用线程的t1.run()方法的时候其实并没有启动线程的操作,仅仅是函数调用而已。
t2.start();
t1.join();
t2.join();
} catch (InterruptedException e){
//
}
System.out.println(i);
}
}
- synchronized底层语义原理
-
synchronized同步代码块的底层原理
image.png
-
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。
- synchronized同步方法原理
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。
- Java虚拟机对synchronized的优化
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
- 偏向锁:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
- 轻量级锁
- 自旋锁: 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁。
- 锁消除:Java虚拟机在JIT编译时,通过扫描去除不可能存在共享资源竞争的锁,从而消除锁,减少开销。
- 等待唤醒机制与synchronized
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。同时,线程sleep()方法会导致线程进入阻塞态。
参考:
1 synchronized的三种应用方式