【JAVA中的锁】

【锁】

          • [1] 公平锁/非公平锁
          • [2] 可重入锁
          • [3] 独享锁/共享锁(互斥锁/读写锁)
          • [4] 乐观锁/悲观锁
          • [5] 分段锁
          • [6] 偏向锁/轻量级锁/重量级锁
          • [7] 自旋锁
          • [8] 可中断锁/不可中断锁/超时时间
          • [9] 显式锁/隐式锁
          • [10] 条件变量
          • [11] AQS

JAVA锁有哪些种类,以及区别(转)

  1. 实现上

Synchronized

ReentrantLock

CAS

Volatile

2 类型上

  • 公平锁/非公平锁 :
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释

[1] 公平锁/非公平锁
  • 介绍:

公平锁是指多个线程按照申请锁的顺序来获取锁,

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

  • 优缺点:

公平锁可以防止线程饥饿。

非公平锁的优点在于吞吐量比公平锁大,有可能会造成优先级反转或者饥饿现象。

  • 实现:

对于Java ReentrantLock而言,默认是非公平锁。,但是支持公平锁。

对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

1. 公平调度方式:

按照申请的先后顺序授予资源的独占权。

2. 非公平调度方式:

在该策略中,资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该段时间内,**新来的线程(活跃线程)**可以先被授予该资源的独占权。

如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。

公平调度和非公平调度方式优缺点分析

非公平调度策略:

  • 优点:吞吐率较高,单位时间内可以为更多的申请者调配资源
  • 缺点:资源申请者申请资源所需的时间偏差可能较大,并可能出现线程饥饿的现象

公平调度策略:

  • 优点:线程申请资源所需的时间偏差较小;不会出现线程饥饿的现象;适合在资源的持有线程占用资源的时间相对长或者资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用;
  • 缺点:吞吐率较小

接下来,我们一起来看看JVM对synchronized内部锁的调度方式吧。

JVM对synchronized内部锁的调度

JVM对内部锁的调度是一种非公平的调度方式,JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次申请锁的机会。被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁.

[2] 可重入锁
  • 介绍:

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

  • 优缺点:

可重入锁的一个好处是可一定程度避免死锁。

  • 实现:

对于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);
}
[3] 独享锁/共享锁(互斥锁/读写锁)
  • 介绍:

独享锁是指该锁一次只能被一个线程所持有。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
共享锁是指该锁可被多个线程所持有。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

  • 实现:

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLockSynchronized
读写锁在Java中的具体实现就是ReadWriteLock

对于Synchronized而言,当然是独享锁。

[4] 乐观锁/悲观锁
  • 介绍:

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。悲观锁适合写操作非常多的场景
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

  • 优缺点:

从悲观锁适合写操作非常多的场景

乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

  • 实现:

悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

[5] 分段锁
  • 介绍:

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

  • 实现:

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

[6] 偏向锁/轻量级锁/重量级锁
  • 介绍:

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

  • 实现:

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

[7] 自旋锁
  • 介绍:

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。

  • 优缺点

好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU造成ABA问题。

  • 实现:

典型的自旋锁实现的例子,可以参考自旋锁的实现

[8] 可中断锁/不可中断锁/超时时间

可中断锁:顾名思义,就是可以相应中断的锁。**不会无限制等待下去,是避免死锁的一种方式。**在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

超时时间:中断是被动的打断,而设置超时时间是主动的打断,可以避免死锁。

[9] 显式锁/隐式锁

synchronized加锁是隐式加锁,使用者不会看到其加锁解锁过程锁升级过程·。

ReentrantLock 加锁和解锁是显示的。如果 ReentrantLock 调用lock方法加锁, unlock 方法解锁,否则会造成死锁。

[10] 条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

synchronized 是那些不满足条件的线程都在一间休息室等消息

而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤

使用要点:

  • await 前需要获得锁

await 执行后,会释放锁,进入 conditionObject 等待

await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

竞争 lock 锁成功后,从 await 后继续执行

[11] AQS

AQS是AbustactQueuedSynchronizer(队列同步器)的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类

AQS中可重写的方法分为独占式与共享式的

img

可以直接调用的模板方法有

img

同步器提供的如下3个方法来访问或修改同步状态。 ·getState():获取当前同步状态。 ·setState(int newState):设置当前同步状态。 ·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

2.2 实现

2.2.1 同步队列

同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,获取锁失败那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用

同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和 后继节点

img

使用CAS将节点插入到尾部,并用tail指向该结点

2.2.2 独占锁的获取和释放流程

获取

  • 调用入口方法acquire(arg)
  • 调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步
  • 将当前线程构造成一个Node节点,并利用addWaiter(Node node) 将其加入到同步队列尾部
  • 调用acquireQueued(Node node,int arg)方法,使得该 节点以“死循环”的方式获取同步状态
  • 自旋时,首先判断其前驱节点为头节点且释放&是否成功获取同步状态,两个条件都成立,则将当前线程的节设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待前驱节点释放唤醒自己,之后继续判断。

释放

  • 调用入口方法release(arg)
  • 调用模版方法tryRelease(arg)释放同步状态
  • 利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第五步)

2.2.3 共享锁的获取和释放流程

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态

获取锁

  • 在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态
  • tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于0
  • 可以看到,在doAcquireShared(int arg)方法的自 旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。

释放锁

  • 调用releaseShared(arg)模版方法释放同步状态
  • 调用模版方法tryReleaseShard(arg)释放同步状态
  • 如果释放成功,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点
  • 与独占式区别在于线程安全释放,通过循环和CAS保证,因为释放同步状态的操作会同时来自多个线程

2.2.4 独占锁和共享锁在实现上的区别

  • 独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态
  • 共享锁的同步状态>1,取值由上层同步组件确定
  • 独占锁队列中头节点运行完成后释放它的直接后继节点
  • 共享锁队列中头节点运行完成后释放它后面的所有节点
  • 共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况

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;
         }
 

上述代码的处理流程已经非常清晰:

  • 获取同步状态,并从中分离出低16为的写锁状态
  • 如果同步状态不为0,说明存在读锁或写锁
  • 如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
  • 如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
  • 如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入) 将当前线程设置为写锁的获取线程

写锁的释放过程与独占锁基本相同:

     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时,写锁完全释放。

读锁的获取与释放

读锁是一个共享锁,获取读锁的步骤如下:

  • 获取当前同步状态
  • 计算高16为读锁状态+1后的值
  • 如果大于能够获取到的读锁的最大值,则抛出异常
  • 如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
  • 如果上述判断都通过,则利用CAS重新设置读锁的同步状态

读锁的释放步骤与写锁类似,即不断的释放写锁状态,直到为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));    
     } 
 }

流程:

  • 这个自定义类Mutex首先实现了Lock接口,
  • 内部静态类Sync继承了AQS抽象类,并重写了独占式的tryAcquire和tryRelease方法,
  • 接着Mutex实例化Sync内部类,
  • Mutex类重写Lock接口的方法,如lock、tryLock、unlock等方法,具体实现是通过调用Sync类中的重写的方法(tryAcquire)以及模板方法(acquire)等
  • 用户使用Mutex时调用Mutex提供的方法,在Mutex的实现中,调用同步器的模板方法acquire(int args)

你可能感兴趣的:(JAVA多线程8月份专题)