JAVA锁有哪些种类,以及区别(转)
Synchronized
ReentrantLock
CAS
Volatile
2 类型上
上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释
公平锁是指多个线程按照申请锁的顺序来获取锁,
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
公平锁可以防止线程饥饿。
非公平锁的优点在于吞吐量比公平锁大,有可能会造成优先级反转或者饥饿现象。
对于Java ReentrantLock
而言,默认是非公平锁。,但是支持公平锁。
对于Synchronized
而言,也是一种非公平锁。由于其并不像ReentrantLock
是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
1. 公平调度方式:
按照申请的先后顺序授予资源的独占权。
2. 非公平调度方式:
在该策略中,资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该段时间内,**新来的线程(活跃线程)**可以先被授予该资源的独占权。
如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。
公平调度和非公平调度方式优缺点分析
非公平调度策略:
公平调度策略:
接下来,我们一起来看看JVM对synchronized内部锁的调度方式吧。
JVM对synchronized内部锁的调度
JVM对内部锁的调度是一种非公平的调度方式,JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次申请锁的机会。被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁.
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
可重入锁的一个好处是可一定程度避免死锁。
对于Java ReentrantLock
而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock
重新进入锁。
对于Synchronized
而言,也是一个可重入锁。
synchronized可重入锁的实现:
之前谈到过,每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
独享锁是指该锁一次只能被一个线程所持有。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
共享锁是指该锁可被多个线程所持有。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
和Synchronized
读写锁在Java中的具体实现就是ReadWriteLock
对于Synchronized
而言,当然是独享锁。
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。悲观锁适合写操作非常多的场景
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
从悲观锁适合写操作非常多的场景
乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap
来说一下分段锁的含义以及设计思想,ConcurrentHashMap
中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
这三种锁是指锁的状态,并且是针对Synchronized
。在Java 5通过引入锁升级的机制来实现高效Synchronized
。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU造成ABA问题。
典型的自旋锁实现的例子,可以参考自旋锁的实现
可中断锁:顾名思义,就是可以相应中断的锁。**不会无限制等待下去,是避免死锁的一种方式。**在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
超时时间:中断是被动的打断,而设置超时时间是主动的打断,可以避免死锁。
synchronized加锁是隐式加锁,使用者不会看到其加锁解锁过程锁升级过程·。
ReentrantLock 加锁和解锁是显示的。如果 ReentrantLock 调用lock方法加锁, unlock 方法解锁,否则会造成死锁。
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤
醒
使用要点:
await 执行后,会释放锁,进入 conditionObject 等待
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
竞争 lock 锁成功后,从 await 后继续执行
AQS是AbustactQueuedSynchronizer(队列同步器)的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。
AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
AQS中可重写的方法分为独占式与共享式的
可以直接调用的模板方法有
同步器提供的如下3个方法来访问或修改同步状态。 ·getState():获取当前同步状态。 ·setState(int newState):设置当前同步状态。 ·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
2.2 实现
2.2.1 同步队列
同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,获取锁失败那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和 后继节点
使用CAS将节点插入到尾部,并用tail指向该结点
2.2.2 独占锁的获取和释放流程
获取
释放
2.2.3 共享锁的获取和释放流程
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态
获取锁
释放锁
2.2.4 独占锁和共享锁在实现上的区别
2.2.5 重入锁
重入锁指的是当前线程成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中ReentrantLock和synchronized都是可重入锁,synchronized由JVM偏向锁实现可重入锁,ReentrantLock可重入性基于AQS实现。
重入锁的基本原理是判断上次获取锁的线程是否为当前线程(current == getExclusiveOwnerThread()
),如果是则可再次进入临界区,如果不是,则阻塞。
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//通过AQS获取同步状态
int c = getState();
//同步状态为0,说明临界区处于无锁状态,
if (c == 0) {
//修改同步状态,即加锁
if (compareAndSetState(0, acquires)) {
//将当前线程设置为锁的owner
setExclusiveOwnerThread(current);
return true;
}
}
//如果临界区处于锁定状态,且上次获取锁的线程为当前线程
else if (current == getExclusiveOwnerThread()) {
//则递增同步状态
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放 同步状态时减少同步状态值
2.2.6 公平锁和非公平锁
对于非公平锁,只要CAS设置 同步状态成功,则表示当前线程获取了锁,而公平锁则不同
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//此处为公平锁的核心,即判断同步队列中当前节点是否有前驱节点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该 方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
2.2.7 读写锁
Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock
,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。
写锁的获取与释放 写锁是一个独占锁,所以我们看一下ReentrantReadWriteLock中tryAcquire(arg)的实现:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
上述代码的处理流程已经非常清晰:
写锁的释放过程与独占锁基本相同:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
在释放的过程中,不断减少读锁同步状态,只为同步状态为0时,写锁完全释放。
读锁的获取与释放
读锁是一个共享锁,获取读锁的步骤如下:
读锁的释放步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。
三、使用AQS与Lock自定义一个锁
class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
protected boolean tryRelease(int releases) {
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
流程: