前言
公司加班太狠了,都没啥时间充电,这周终于结束了。这次整理了Java并发编程里面的synchronized关键字,又称为隐式锁,与JUC包中的Lock显示锁相对应;这个关键字从Java诞生开始就有,称之为重量级锁,自从JDK1.6之后官方对该关键字进行优化,引入了轻量级锁和偏向锁,于是就有了锁升级的概念。
使用
在代码中使用这个关键字总共有以下三种:
private static Object object = new Object();private synchronized void function1() {//锁住当前实例对象}private static synchronized void function2() {//锁住当前class对象,可以认为是锁住当前Class文件}public static void main( String[] args ) {synchronized (object) {//锁住object对象}}
1:普通方法同步;
2:静态方法同步;
3:同步代码块括号中的对象;
使用synchronized关键字进行同步,则锁是储存于Java对象头里面的Mark Word。
Java对象头里面的Mark Word里面主要是存储对象的hashCode、分代年龄(用于判断为老年代还是年轻代,在垃圾回收器里面用得到)以及锁标记位。
锁的升级
在JDK1.6之后,引入了引入了偏向锁和轻量级锁的状态,目的是为了提升锁的释放和获取效率,减少性能的开销,所以synchronized就有四种级别锁的状态,级别从低到高分别是无锁状态、偏向锁状态、轻量级锁以及重量级锁状态;四种状态实质上是对象头储存的锁标记位不一致,使用CAS更改对象头的标记位进行锁状态位;并且在记录锁的标记位的同时,也会在Mark Word里面记录锁线程的ID
无锁
既在对象头的Mark Word没有标记锁状态的时候就是无锁状态
偏向锁
单个线程进行访问或调用带synchronized的同步代码块或方法时,会在先判断对象头里面锁标志位是否有线程ID,如果没有线程ID的话,将当前线程ID写入进去,并且也会在栈帧中的锁记录里面进行记录。此时,锁的状态位为偏向锁,通俗来说可以说是只要单线程访问同步代码块,从无锁状态就会便成为偏向锁状态;如果在对象头里面存在线程ID的话,如果当前线程ID是与对象头里面记录的线程ID的话,那么就可以直接访问,不需要使用CAS去进行竞争锁了;不一致的话,那么就会使用CAS进行竞争锁。
当其他线程开始竞争偏向锁的时候,那么持有偏向锁的线程就去会释放偏向锁,供其他线程使用;这时候就出现一种偏向锁的撤销概念
偏向锁的撤销
偏向锁的撤销就是,会先暂停持有偏向锁的线程,然后去对象头里面判断记录线程ID的线程是否还在处于活动状态,如果处于非活动状态,那么就会将Mark Word里面的锁标志位设置为无锁状态;如果处于活动状态的话,会先遍历偏向对象的锁记录、栈里面锁记录以及对象头里面Mark Word里面的锁标记位,并且将对象头中锁标志位设置为无锁状态或者升级成轻量锁状态的,然后唤醒持有偏向锁的的线程,继续执行;
轻量级锁
加锁
当执行同步代码块升级为轻量级锁的时候,会在栈帧中创建一块用于储存锁记录的空间,并且将对象头里面Mark Word复制到记录中(Displaced Mark Word),线程开始会使用CAS将Mark Word替换为指向锁记录的指针,如果获取成功,那么当前线程获取锁,如果失败,采用自旋来获取锁,如果自旋获取锁失败,那么会膨胀为重量级锁。
解锁
轻量级锁解锁会使用CAS将Displaced Mark Word替换回到对象头中,如果成功了,表示没有锁竞争;如果失败了表示有锁在竞争,那么就会膨胀成重量级锁,那么在自旋的获取锁的线程就会进行线程阻塞;
由于自旋会大量消耗CPU资源,所以一旦升级成为了重量级锁之后,那么就不会进行降级了。
重量级锁
这个就是线程阻塞了,基本上可以和Lock表现一致了,一旦有线程获取锁,其他获取锁的线程将会阻塞,释放锁之后将会唤醒阻塞线程去竞争锁
锁 | 优点 | 缺点 | 使用场景 |
偏向锁 | 加锁和解锁不需要额外的资源消耗,性能快 | 如果线程之间存在锁竞争,那么会出现锁撤销,暂停线程,比较消耗资源 | 适用于单线程使用场景 |
轻量级锁 | 线程不会阻塞,提高响应程度 | 自旋消耗CPU性能,容易升级为重量级锁 | 适用于同步代码块执行非常快的 |
重量级锁 | 线程不会自旋,避免过多消耗CPU资源 | 线程阻塞,响应时间缓慢 | 提高吞吐量,同步代码块执行较长 |
等待/通知机制
这个之前是有篇讲过线程之间的共享协作:线程协作 这个里面提到过如何使用
现在看看底层是如何运行的执行,我们先看一段代码
public class SynchronziedDemo {public static Object object = new Object();public static void main(String[] args) {synchronized (object) {}m();}public static synchronized void m() {}
}
将这段代码编译后使用javap -v命令进行反编译
会得到class文件的编译后的c代码:
同步代码块使用的事monitorenter(获取锁)和monitorexit(释放锁)指令,同步放上是使用ACC_SYNCRONIZED来完成的。
无论是哪个本质上其实是个monitor监视器进行完成的,每个对象都会有一个监视器,线程需先获取到monitor监视器才能访问同步代码块或者方法,而没有获取到的线程就会进入自旋,升级为重量级锁,然后会进入到阻塞状态,这时候会有一个同步队列(SynchronizedQueue),阻塞的线程会加入到这个队列里面,等待获取到监视器的线程调用monitorexit指定后,会唤醒同步队列里面的等待线程。
等待/通知机制里面对了一个WaitQueue即等待队列,案例:
public class WaitNotify {public static Object object = new Object();public static void main(String[] args) {new Thread(new WaitClass()).start();new Thread(new NotifyClass()).start();}static class NotifyClass implements Runnable {@Overridepublic void run() {synchronized (object) {try {TimeUnit.SECONDS.sleep(2L);System.out.println(Thread.currentThread() + "notify start...");object.notifyAll();System.out.println(Thread.currentThread() + "notify end...");} catch (InterruptedException e) {e.printStackTrace();}}}}static class WaitClass implements Runnable {@Overridepublic void run() {synchronized (object) {System.out.println(Thread.currentThread() + "wait start....");try {object.wait();System.out.println(Thread.currentThread() + "wait end....");} catch (InterruptedException e) {e.printStackTrace();}}}}
}
打印日志如下:
Thread[Thread-0,5,main]wait start....
Thread[Thread-1,5,main]notify start...
Thread[Thread-1,5,main]notify end...
Thread[Thread-0,5,main]wait end....
对于wait线程获取到object对象的监视器之后,调用wait方法后进入等待队列(WaitQueue),然后释放监视器。
对于notify线程来说,先获取到object对象监视器之后,然后调用notifyAll方法,将WaitQueue里面的所有等待线程同步到同步队列中,(如果是调用notify方法就会值同步一个线程并非所有线程),然后释放监视器,就会唤醒同步列队中的线程。
对于wait线程,在同步队列呗唤醒后,会重新获取监视器,然后继续执行wait方法后面的代码。
这样就完成一个等待通知的机制了。