多线程编程中,有可能有很多线程同时访问一个共享、可变资源(临界资源)的情况。
- 共享:资源可以由多个线程同时访问
- 可变:资源可以在器生命周期内被修改
由于线程执行的过程是不可控的没所以需要采用同步机制来协同对象可变状态的访问,java中通过加锁来实现同步
加锁的目的
序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
Java锁体系
Java中锁也可以分为显式锁隐式锁,隐式锁就是使用 synchrinized 关键字。显示锁就是由开发通过定义的Lock对象,来手动进行加解锁。
显式锁 | 隐式锁 |
---|---|
ReentrantLock,实现juc里Lock | Synchronized加锁机制 |
实现是基于AQS实现 | Jvm内置锁 |
需要手动加解锁ReentrantLock lock(),unlock() | 不需要手动加锁与解锁,Jvm会自动加锁跟解锁 |
synchronized使用与原理
synchronized加锁方式
- 同步实例方法,锁是当前实例对象(this),当前bean由容器管理,则bean作用域必须是单例
- 同步类方法,锁是当前类对象
- 同步代码块,锁是括号里面的对象
synchronized的实现
synchronized通过JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。
如果需要跨方法加解锁、可以使用Unsafe类的monitorEnter和monitorExit方法手动进行加解锁;
Monitor:每个对象都会在创建之初维护一个对应的Monitor(监视器锁)对象
JVM加锁过程如下图:
- 线程需要竞争内部对象Monitor(监视器锁),没有竞争到Monitor对象的线程(阻塞的线程)会被放到一个waitSet缓存队列中;
- 在1中竞争到Monitor的线程执行monitorexit后,会唤醒waitSet缓存队列中的所有线程去竞争Monitor锁
JVM对锁的优化
- 锁消除
- 锁粗化
- 优化锁的升级过程
- 适应性自旋
锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
如下StringBuffer的 append 是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
public void add() {
StringBuffer sb = new StringBuffer();;
sb.append("1");
sb.append("2");
}
锁粗化
对于连续的基于同一个锁对象加锁的代码段,JIT在编译时会将多个同步代码合并成一个同步代码段。
如上的代码中,StringBuffer中的append方法是一个同步方法,连续两次的sppend方法的调用,相当于两个同步代码块,此时,JIT在编译时只会对两个方法整体加锁,而不是两个方法各自加锁。
JVM内置锁升级过程
JDK1.6版本之后对synchronized的实现进行了各种优化,如自旋锁、偏向锁和轻量级锁;并默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的 竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单 向的,也就是说只能从低到高升级,不会出现锁的降级。下图为锁的升级过程:
偏向锁
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从 而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种 称为轻量级锁的优化手段(1.6之后加入的)。此时Mark Word的结构也变为轻量级锁的结构。
轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”。
轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁
大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(一般不会太久,这也是称为自旋的原因),在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。
同步框架AbstractQueuedSynchronizer(AQS)
AQS是Java中多线程访问共享资源的同步框架,是对大多数同步其的基础行为的抽象。
AQS具备特性
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
目前juc(java.util.concurrent)包中同步器的实现都是基于AQS框架来实现的,一般是通过内部类Sync继承AbstractQueuedSynchronizer抽象类,将同步器的所有调用映射到Sync类对应的方法中
State三种访问方式
- getState()、setState()、compareAndSetState()
AQS定义两种资源共享方式
- Exclusive-独占,只有一个线程能执行,如ReentrantLock Share-共享
- 多个线程可以同时执行,如Semaphore/CountDownLatch
AQS定义两种队列
- 同步队列
CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
- 条件队列
Condition是一个多线程间协调通信的工具类,使得某个或者某些线程一起等待某个条件(Condition),只有当该条件具备时,这些等待线程才会被唤醒,从而重新争夺锁。
等待队列中的节点:
static final class Node {
/** 标记节点为共享模式 */
static final Node SHARED = new Node();
/** 不为空时表示节点为独占模式 */
static final Node EXCLUSIVE = null;
/** 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待 */
static final int CANCELLED = 1;
/** 此状态下,如果后继节点的线程处于等待状态,当前状态如果释放了同步状态或者被取消,将会通知后继节点,使后继节点得以运行 */
static final int SIGNAL = -1;
/** 节点在条件等待队列中,线程等待在Condition上,只有其他线程对Condition调用了sugnal()方法,该节点从等待队列中转移到同步等待队列中,必须时独占模式 */
static final int CONDITION = -2;
/**
* 表示下一次共享时,同步状态获取将会被无条件地传播下去
*/
static final int PROPAGATE = -3;
/**
* 标记当前节点的信号量状态,使用CAS更改状态,volatile保证线程可见性
*/
volatile int waitStatus;
/**
* 前继节点,同步队列中使用
*/
volatile Node prev;
/**
* 后继节点,同步队列中使用
*/
volatile Node next;
/**
* 绑定在当前节点的线程
*/
volatile Thread thread;
/**
* 下一个等待的节点,条件队列中使用
*/
Node nextWaiter;
/**
* 当前节点是在共享模式下等待时,返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回当前结点的前继节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
通过UnSafe类中的park()和unpark方法来阻塞线程和唤起线程