Java多线程二——Java锁分类

我们经常会听到比如:读写锁、公平锁、非公平锁、乐观锁、悲观锁、自旋锁、可重入锁……等等。

如下图所示:

78f1e63b027048bf9dcb176a86dbd3e0.png

以Java来说,关于锁的大分类,就只有:悲观锁、乐观锁这两种。其余说的各种锁都是基于这两大分类下的细节实现。

2.1 乐观锁/悲观锁

2.1.1 乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后比较跟上一次的版本号,如果一样则更新,如果不一样则要重复读-比较-写的操作。

java 中的乐观锁基本都是通过 CAS(Compare And Set)机制实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

2.1.1.1 Java中的乐观锁有哪些

Java没有提供可直接使用的乐观锁,不过内置了一些由底层由乐观锁实现的类。例如:java.util.concurrent.atomic下的几个原子类。

2127e1ab90ed425cb0358a8e3cde9c57.png

如果我们想自己实现乐观锁的话,可以参考上面那些原子类,使用 valotile+CAS 的方式实现。

2.1.1.2 valotile

valotile 跟 synchronized 一样,是Java内置的关键字。不过 valotile 只能修饰变量

valotile主要的作用是保证变量在内存中的可见性、有序性:

  • 可见性:valotile修饰的变量被修改后,对内存中是立即可见的。举个例子:有两个线程A、B。有一个valotile修饰的变量Y。当线程A对变量Y进行修改后,线程B能够立刻感知。感知到后会重新读取变量Y的最新值放到自己工作内存中进行操作。
  • 有序性:我们都知道,Java代码是一行一行根据编写顺序去运行的。看着是顺序执行没错。但是实际底层JVM在执行字节码文件指令时,有些指令并不会依照代码顺序一样执行。因为Java编译器会在编译成字节码的时候为了提高性能会对你的代码进行指令优化,这个优化叫做指令重排。这个指令重排在单线程环境下不会有问题,因为不管你怎么重排指令,最后的结果都是期望的。但是如果在多线程环境下,就会有线程安全问题。所以valotile帮我们禁止了指令重排的优化,保证了编译器是严格按照代码编写顺序去编译指令。

2.1.1.3 CAS

CAS可以理解为比较后赋值。举例:两个线程A、B。修改一个共享资源变量Y、根据Java内存模型(JMM)定义,两个线程分别会复制一份资源的副本到各自的工作内存中。AY1、BY1。两个线程修改完后会将AY1、BY1同步回主内存中。

然而,在CAS机制下,两个线程除了复制AY1、BY1到工作内存之外,还会另存一个资源副本AY2、BY2。当线程各自修改完AY1、BY1之后,同步主内存之前,会用AY2、BY2与主内存中的资源Y对比,如果对比一致,则立即更新主内存,如果不一致,则重复上面操作,重新从主内存获取资源、修改、同步。

由于CAS在Java底层是一个原子操作,所以可以保证同步数据回主内存时是线程安全的。这点可以参考 sun.misc.Unsafe 类。这个类提供了原生的CAS能力,直接调用 native 方法与系统底层交互。

e5d4c737b7c04ceaab307f3c48a70e22.png

【Java内存模型】:了解CAS之前先来了解一下Java内存模型。Java中的内存模型定义,将内存划分为:主内存和工作内存。Java线程在操作资源时,会将其用到的资源复制一份到线程的私有工作内存中,线程在自己的工作内存中对资源完成操作后,再把资源同步回主内存当中,完成一次资源的操作。

【ABA问题】 :A线程先读取共享内存数据值1,随后因某种原因,线程暂时挂起;同时B线程临时将共享内存数据值先改为2,随后又改回为1。随后挂起线程A恢复,并通过CAS比较,成功将共享内存数据值先改为2。

解决方案:AtomicStampedReference 版本号原子引用、AtomicMarkableReference 布尔标记原子引用。

2.1.1.4 valotile+CAS

上面分别介绍了 valotile 和 CAS 。了解到 valotile 是为了保证资源的可见性,任何一个线程修改了资源后。其他线程都能立刻感知并重新获取资源。CAS 是保证资源的安全性,由于是原子操作,任何一个线程在修改资源时,都是一体的。其他线程是不可操作的。所以Valotile的特性+CAS的机制就组成了一个完美的乐观锁,既保证了线程安全,对性能影响也不大。Valotile的特性+CAS的机制这种组合也可以叫做:Valotile+原子操作。

2.1.2 悲观锁

基本上我们理解的操作前对资源加锁,操作完后释放锁,说的都是悲观锁。悲观锁认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问。

2.1.2.1 Java中的悲观锁有哪些

  • synchronized 关键字
  • 基于Java同步器AQS的各种实现类(如 ReentrantLock)

1)synchronized

synchronized 有三种使用方式:

  • 修饰静态方法:锁住的是类,该类下创建的所有对象都被锁住
  • 修饰实例方法:锁住的是当前对象,当前类创建的其他对象不受影响
  • 修饰代码块(静态代码块、实例代码块):根据代码块所出区域来区别,如代码块在静态方法中,那锁的是整个类、如代码块在实例方法中,那锁住的是当前实例对象。

Java中的 synchronized 关键字、底层是由 JVM 实现的同步机制,通过两条监听器指令:MONITORENTER(进入)、MONITOREXIT(退出)来实现同步效果(代码编译成字节码文件后可看到指令)。每个对象都对应有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 MONITORENTER 和 MONITOREXIT 指令来实现的,方法加锁是通过一个标记位来判断的。

在JDK1.6之前,synchronized 属于重量级锁,是一个效率比较低下的锁,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,时间成本相对较高。

在JDK1.6后,JVM为了提高锁的获取与释放效率对 synchronized 进行了优化,引入了偏向锁和轻量级锁,所以锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),会随着竞争的激烈而逐渐升级。

2)基于AQS的实现类

AQS全称(AbstractQueuedSynchronizer)。基于Java程序实现的一种抽象队列同步器框架。AQS定义了一个 volatile 修饰的 int 类型变量 state 来控制是否同步,提供一个 unsafe 实现的原子方法来更新 state(也就是更新锁状态,是否上锁)。

# AbstractQueuedSynchronizer.class
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

基于AQS,Java本身实现了一些同步类,它们都位于java.util.concurrent包下。例如:

  • ReentrantLock(可重入锁,AQS体系下用户使用的最多的一个锁)
  • ReentrantReadWriteLock(基于ReentrantLock的读写锁,读锁之间共享资源、读写和写写之间互斥资源,读写锁相较于普通的互斥锁并发能力要稍微好些,但使用起来需要考虑锁的切入点)
  • StampedLock(基于读写锁优化,对读锁更加细化了一层,但同时使用也更加复杂,用的不多)
  • Semaphore(信号量,可用于限流)
  • CountDownLatch(可用于计数,一般用于在多线程环境下需要执行固定次数逻辑的地方)

AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock 源码如下:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * 基于AQS的同步锁,分为公平锁和非公平锁
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        //......
    }

    /**
     * 非公平锁
     */
    static final class NonfairSync extends Sync {
        /**
         * 先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    /**
     * 公平锁
     */
    static final class FairSync extends Sync {
        //......
    }

    /**
     * 默认构造非公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 根据传入参数fair确定构造公平锁还是非公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

2.2 共享锁/互斥锁

共享锁(读锁、S锁)是指该锁可被多个线程所持有;互斥锁(排它锁、独占锁、写锁、X锁)是指该锁一次只能被一个线程所持有。

  • 对于 synchronized 关键字而言,其是互斥锁。
  • 对于Java ReentrantLock 类而言,其是互斥锁。
  • 但是对于Lock的另一个实现类 ReentrantReadWriteLock ,其读锁是共享锁,其写锁是互斥锁。其中读读是共享的;读写、写读 、写写的过程是互斥的。ReentrantReadWriteLock 的共享锁和互斥锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。

2.3 公平锁/非公平锁

  • 公平锁 :多线程在进行数据请求的过程中,先去队列中申请锁,按照FIFO先进先出的原则拿到线程,然后占有锁。
  • 非公平锁 :线程尝试获取锁,如果获取不到,这时候采用公平锁的方式进行,与此同时,多个线程获取锁的顺序有一定的随机性,并非按照先到先得的方式进行。
    • 优点:性能上高于公平锁;
    • 缺点:存在线程饥饿问题,即某一个线程一直获取不到锁导致一直等待“饿死了”。

在java里面,synchronized 默认就是非公平锁,ReentrantLock 可以通过构造函数来设置该锁是公平的还是非公平的,默认是非公平的。

private final ReentrantLock.Sync sync;
public ReentrantLock() {        
    this.sync = new ReentrantLock.NonfairSync();    
}
public ReentrantLock(boolean fair) {        
    this.sync = (ReentrantLock.Sync)(fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());    
}

2.4 偏向锁/轻量级锁/重量级锁(简单介绍,后面细讲)

在JDK 1.6之前,内置锁 synchronized 在Java中被抽象为监视器锁(monitor),监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。

为了换取性能,JVM在内置锁 synchronized 上做了非常多的优化,引入偏向锁和轻量级锁,进行膨胀式的锁分配策略就是其一。

  • 偏向锁:指一段同步代码一直被一个线程所访问(不存在多线程竞争),那么该线程会自动获取锁,降低获取锁的代价。在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只依赖一次CAS原子指令。但在多线程竞争时,需要进行偏向锁撤销步骤,因此其撤销的开销必须小于节省下来的CAS开销,否则偏向锁并不能带来收益。
  • 轻量级锁:轻量级锁也就是自旋锁,利用CAS尝试获取锁。轻量级锁通过自旋来避免内核态与用户态的切换和线程阻塞造成的线程切换。如果你确定某个方法同一时间确实会有一堆线程访问,而且工作时间还挺长,那么建议直接用重量级锁,不要使用 synchronized,因为在CAS过程中,CPU是不会进行线程切换的,这就导致CAS失败的情况下他会浪费CPU的分片时间,都用来干这个事了。
  • 重量级锁:synchronized 就是一种重量级锁(ReentrantLock 也是重量级锁,它会先尝试CAS获取锁,获取不到则转重量级锁),它是通过内部一个叫做 Monitor Lock(监视器锁)来实现,而监视器锁本质上是依赖于系统的 Mutex Lock(互斥锁)来实现,当加锁的时候需要用用户态切换为核心态,这样的时间成本和性能开销非常高,因此这种依赖于操作系统 Mutex Lock 的锁称为重量级锁。在JDK1.6后,JVM为了提高锁的获取与释放效率对 synchronized 进行了优化,引入了偏向锁和轻量级锁,所以锁的状态就有了四种(无锁001、偏向锁101、轻量级锁000、重量级锁010),会随着竞争的激烈而逐渐升级。

2.5 可重入锁/不可重入锁

(1) 可重入锁

指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。可重入锁的意义之一在于防止死锁

synchronized ReentrantLock 都是可重入锁:

  • 对于 synchronized 来说,其实现可重入的原理是:
    • 处于偏向锁/轻量级锁状态时,通过往线程栈帧中添加 LockRecord 来实现重入。
    • 处于重量级锁状态时,通过在锁对象对应的 ObjectMonitor 中维护一个 「重入计数器_recursions」来实现重入:当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 ;如果同一个线程再次请求这个锁,计数器将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
  • 对于 ReentrantLock 来说,通过其内部类 Sync 的父类 AbstractQueuedSynchronizer 中维护的 state 字段来实现重入。

(2)不可重入锁

指的是某个线程已经获得某个锁,之后不可以再次获取该锁,会被阻塞。

public class MyLockTest {

    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    // 加锁
    public void myLock() {
        // 获取当前线程的引用,既代码段正在被哪一个线程调用
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> mylock");
        // 自旋锁 CAS实现
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    // 解锁
    public void myUnLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> myUnlock");
        atomicReference.compareAndSet(thread, null);
    }

    /**
     * 同一把锁
     * @param i
     * @param lock
     */
    private static void b(int i,MyLockTest lock){
        lock.myLock();
        try {
            i++;
            System.out.println(i);
            if (i == 10) {
                return;
            }
            b(i,lock);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.myUnLock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyLockTest myLockTest = new MyLockTest();
        b(0,myLockTest);
    }
}
//结果(无法打印1-10)
main==> myLock
1
main==> myLock

2.6 自旋锁

自旋,是指当有另一个线程想获取被其它线程持有的锁的时候,不会进入阻塞状态,而是使用空循环来进行自旋(CAS)。注意:自旋是会消耗cpu的,所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。

自旋锁的实现原理同样也是 CAS,AtomicInteger 原码中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

AtomicInteger.class
public final int getAndAddInt (Object var1, Long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while( !this.compareAndSwapInt(var1, var2, var5, var5 + var4));
     
    return var5;
}

自旋锁的一些问题 :

  • 如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在空循环,消耗cpu。
  • 本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁(线程饿死),此后再升级为重量级锁相比直接就是重量级锁更加浪费低效。

基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。JDK1.5中默认自旋次数为10次,用户可以通过 -XX:PreBlockSpin 来进行更改。

  • 适应性自旋锁(JDK1.6):所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
    • 线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。
    • 反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源

自旋锁早在JDK1.4 中就引入了,只是当时默认时关闭的,在JDK 1.6后默认为开启状态,同时在JDK 1.6中引入了自适应自旋锁。

JDK 1.6的轻量级锁使用的是普通自旋锁,且需要使用-XX:+UseSpinning选项手工开启。

JDK1.7后,轻量级锁使用自适应自旋锁,JVM启动时自动开启,且自旋时间由JVM自动控制。

同时JVM还针对当前CPU的负荷情况对适应性自旋锁做了较多的优化:

  • 如果平均负载小于CPUs(逻辑CPU数)则一直自旋
  • 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
  • 如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞
  • 如果CPU处于节电模式则停止自旋
  • 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
  • 自旋时会适当放弃线程优先级之间的差异

【物理cpu个数、核数、逻辑cpu数的概念】

1. 物理cpu数:主板上实际插入的cpu数量,可以数不重复的 physical id 有几个(physical id)

2. cpu核数:单块CPU上面能处理数据的芯片组的数量,如双核、四核等 (cpu cores)

3. 逻辑cpu数:一般情况下,逻辑cpu=物理CPU个数×每颗核数,如果不相等的话,则表示服务器的CPU支持超线程技术

(HT:简单来说,它可使处理器中的1 颗内核如2 颗内核那样在操作系统中发挥作用。这样一来,操作系统可使用的执行资源扩大了一倍,大幅提高了系统的整体性能,此时逻辑cpu=物理CPU个数×每颗核数x2)。有多少个逻辑cpu就能开多少线程。

2.6.1 自旋锁——TicketLock_票锁/号码锁

TicketLock 可以称为 票锁/号码锁/叫号锁,是一种自旋锁。TicketLock 主要解决的是公平性的问题。

ticket lock 就像是平时使用美味不用等一样,自己先通过公众号排队取号,然后不断的会刷新(自旋)当前叫到了第几号,如果叫到自己的号,就说明可以获取锁去吃饭啦。

实现代码:

public class TicketLockV2 {
    /**
     * 服务号
     */
    private AtomicInteger serviceNum = new AtomicInteger();
    /**
     * 排队号
     */
    private AtomicInteger ticketNum = new AtomicInteger();
    /**
     * 新增一个ThreadLocal,用于存储每个线程的排队号(解锁时无需传入排队号,避免了排队号被修改导致无法正常释放锁的风险)
     */
    private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>();
    public void lock() {
        int currentTicketNum = ticketNum.incrementAndGet();
        // 获取锁的时候,将当前线程的排队号保存起来
        ticketNumHolder.set(currentTicketNum);
        while (currentTicketNum != serviceNum.get()) {
            // Do nothing
        }
    }
    public void unlock() {
        // 释放锁,从ThreadLocal中获取当前线程的排队号
        Integer currentTickNum = ticketNumHolder.get();
        serviceNum.compareAndSet(currentTickNum, currentTickNum + 1);
    }
}

TicketLock存在的问题:

  • 多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量 serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

2.6.2 自旋锁——CLHLock

AQS 是CLH锁的变形实现,首先我们先了解下CLH锁。

CLH锁,是根据作者的名字简称命名,优点:无饥饿,先到先服务的公平性,下面的实现代码是最简单的一种实现方式。可以看到实现方式是基于隐式链表,没有真正的后续节点属性。

/**
 * CLH的发明人是:Craig,Landin and Hagersten
 * 代码来源:http://ifeve.com/java_lock_see2/
 */
public class CLHLock {
    /**
     * 定义一个节点,默认的lock状态为true
     */
    public static class CLHNode {
        private volatile boolean isLocked = true;
    }
    /**
     * 尾部节点,只用一个节点即可
     */
    private volatile CLHNode tail;
    private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>();
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class,"tail");
    public void lock() {
        // 新建节点并将节点与当前线程保存起来
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        // 将新建的节点设置为尾部节点,并返回旧的节点(原子操作),这里旧的节点实际上就是当前节点的前驱节点
        CLHNode preNode = UPDATER.getAndSet(this, node);
        if (preNode != null) {
            // 前驱节点不为null表示当锁被其他线程占用,通过不断轮询判断前驱节点的锁标志位等待前驱节点释放锁
            while (preNode.isLocked) {
            }
            preNode = null;
            LOCAL.set(node);
        }
        // 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁
    }
    public void unlock() {
        // 获取当前线程对应的节点
        CLHNode node = LOCAL.get();
        // 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态职位false,表示当前线程释放了锁
        if (!UPDATER.compareAndSet(this, node, null)) {
            node.isLocked = false;
        }
        node = null;
    }
}

2.6.3 自旋锁——MCSLock

MCSLock 原理与 CLHLock 原理差不多,不同的是 CLHLock 是基于隐式链表,没有真正的后续节点属性,MCSLock 是显示链表,有一个指向后续节点的属性,对本地变量的节点进行循环。将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了 TicketLock 多处理器缓存同步的问题。MCSLock 解决的同样是公平性问题,先到先服务。

/**
 * MCS:发明人名字John Mellor-Crummey和Michael Scott
 * 代码来源:http://ifeve.com/java_lock_see2/
 */
public class MCSLock {
    /**
     * 节点,记录当前节点的锁状态以及后驱节点
     */
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }
    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();
    // 队列
    @SuppressWarnings("unused")
    private volatile MCSNode queue;
    // queue更新器
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class,"queue");
    public void lock() {
        // 创建节点并保存到ThreadLocal中
        MCSNode currentNode = new MCSNode();
        NODE.set(currentNode);
        // 将queue设置为当前节点,并且返回之前的节点
        MCSNode preNode = UPDATER.getAndSet(this, currentNode);
        if (preNode != null) {
            // 如果之前节点不为null,表示锁已经被其他线程持有
            preNode.next = currentNode;
            // 循环判断,直到当前节点的锁标志位为false
            while (currentNode.isLocked) {
            }
        }
    }
    public void unlock() {
        MCSNode currentNode = NODE.get();
        // next为null表示没有正在等待获取锁的线程
        if (currentNode.next == null) {
            // 更新状态并设置queue为null
            if (UPDATER.compareAndSet(this, currentNode, null)) {
                // 如果成功了,表示queue==currentNode,即当前节点后面没有节点了
                return;
            } else {
                // 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待
                // 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法)
                while (currentNode.next == null) {
                }
            }
        } else {
            // 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为null
            currentNode.next.isLocked = false;
            currentNode.next = null;
        }
    }
}

2.7 分段锁

分段锁并不是具体的一个锁,其目的是细化锁的粒度。比如要保证数组中数据的线程安全,我们可以对其上锁,但是这样会影响效率,线程A在操作数组的时候,其他线程是不允许操作的。想一下如果线程A修改数组中下标0~9对应的元素,线程B要修改下标10~15的元素,这两个线程同时操作也不会出现线程安全问题,那可以对数组采用两把锁来控制,一把锁控制下标0~9的元素,另一把锁控制下标10~15的元素,这就是分段锁。相比于单个锁来说可以提高性能。jdk1.7中的 ConcurrentHashMap 就采用了分段锁。

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值