Home Tags Posts tagged with "CAS"

CAS

一、volatile的应用

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。(还包括64位原子操作、禁止指令重排序)

可见性:当一个线程修改一个共享变量时,另外一个线程能够读到这个共享变量值。

如果volatile变量修饰符使用恰当 的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

1.1 volatile定义与原理

volatile定义

如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的

注:有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码(添加 lock),通过查IA-32架 构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。

1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

1.2 volatile的两条实现原则

1)Lock前缀指令会引起处理器缓存回写到内存(从总线锁定变为缓存锁定,使用缓存一致性机制保着修改的原子性)

缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效

2.1 volatile优化使用

著名的Java并发编程大师Doug lea在JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性 能。

为什么追加64字节能够提高并发编程的效率呢?

因为对于英特尔酷睿i7、酷睿、Atom和 NetBurst,以及Core Solo和Pentium M处理器的L1、L2或L3缓存的高速缓存行是64个字节宽,不 支持部分填充缓存行,这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将
它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一
个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致
其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头
节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea使 用追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存
行,使头、尾节点在修改时不会互相锁定。

那么是不是在使用volatile变量时都应该追加到64字节呢?不是的。在两种场景下不应该
使用这种方式。
·缓存行非64字节宽的处理器。如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个
字节宽。
·共享变量不会被频繁地写。因为使用追加字节的方式需要处理器读取更多的字节到高速
缓冲区,这本身就会带来一定的性能消耗,如果共享变量不被频繁写的话,锁的几率也非常
小,就没必要通过追加字节的方式来避免相互锁定。
不过这种追加字节的方式在Java 7下可能不生效,因为Java 7变得更加智慧,它会淘汰或 重新排列无用字段,需要使用其他追加字节的方式。

2.2 synchronized的实现原理与应用

随着Java SE 1.6对sychronized进行了各种优化之后,有些情况下它就并不是重量级锁了。本节将介绍Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

sychronized实现同步的基础:Java中的每一个对象都可以作为锁。

·对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁

代码块同步是使用monitorenter 和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有
详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结 束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有 一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁

2.2.1 Java对象头

synchronized用的锁是存在Java对象头里的

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位,在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变

2.2.2 锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

1.偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问同步块并 获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需 要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则 使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

(1)偏向锁的撤销(看书)

(2)关闭偏向锁(如果确定应用程序里所有的锁通常情况下处于竞争状态,设置JVM参数关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如 有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程 序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

2.轻量级锁

(1)轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并 将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

(2)轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头如果成 功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了)一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

2.3 原子操作的实现原理

2.处理器如何实现原子操作?(处理器层面)

处理器提供总线锁定缓存锁定两个机制来保证复杂内存操作的原子性。

总线锁定:

处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

缓存锁定:

所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声 言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处 理器回写已被锁定的缓存行的数据时,会使缓存行无效,在如图2-3所示的例子中,当CPU1修 改缓存行中的i时使用了缓存锁定,那么CPU2就不能同时缓存i的缓存行。

但是有两种情况下处理器不会使用缓存锁定。

第一种情况是:(不在处理器内部)当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。
第二种情况是:(处理器不支持)有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定

3.Java如何实现原子操作?(Java层面) 建立在处理器层面之上

在Java中可以通过循环CAS的方式来实现原子操作。

(1)使用循环CAS实现原子操作

JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本 思路就是循环进行CAS操作直到成功为止

(2)CAS实现原子操作的三大问题

1)ABA问题

2)循环时间开销大

3)只能保证一个共享变量的原子操作(取巧—将多个共享变量合并为一个共享变量来操作,JDK1.5提高AtomicReference)

(3)使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现的锁机制有偏向锁、轻量级锁、互斥锁,除了偏向锁其他的实现方式都使用了循环CAS,当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时 候使用循环CAS释放锁。

使用同步代码块,效率低下,因此改为乐观锁

 

StockMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.southwind.mmall002.mapper.StockMapper">


    <!--根据商品 ID 扣除库存-->
    <update id="updateSale" parameterType="com.southwind.mmall002.entity.kill.Stock">
        update stock
        set
            saled=saled + 1,
            version = version + 1
        where
            id = #{id}
            and
            version = #{version}
    </update>
</mapper>

Service层代码
    public  Integer kill(Integer id, HttpSession session) {
        //根据 商品 ID 校验库
        Stock stock = checkStock(id);
        LOGGER.info("商品的名称为:" + stock.getName());
        LOGGER.info("商品的已售为:" + stock.getSaled());
        LOGGER.info("商品的库存为:" + stock.getCount());
        //更新库存
        updateSale(stock);
        //创建订单
        return createOrder(stock,session);
    }

    //校验库存
    private Stock checkStock(Integer id){
        Stock stock = stockMapper.selectById(id);
        if(stock.getSaled().equals(stock.getCount())){
            throw new RuntimeException("库存不足!!!");
        }
        return stock;
    }

    //扣除库存
    private void updateSale(Stock stock){
        LOGGER.info("准备更新库存...");
        //在 SQL 层面完成销量的+1 和 版本号的+1 并且根据商品 ID 和版本号同时查询更新的商品
        stock.setSaled(stock.getSaled() + 1);
        int updateRows = stockMapper.updateSale(stock);//返回更新的条数
        if(updateRows == 0 ){
            throw new RuntimeException("抢购失败,请重试!!!");
        }
    }

    //创建订单
    private Integer createOrder(Stock stock,HttpSession session){
        User user= (User) session.getAttribute("user");
        Orders orders = new Orders();
        orders.setUserId(user.getId());
        orders.setLoginName(user.getLoginName());
        orders.setSerialnumber(stock.getName());
        orderMapper.insert(orders);
        return orders.getId();
    }

}

结果:实现了线程同步

0 109

CAS及ABA的定义和实现

CAS思想(算法):对于内存中的某一个值data,提供一个旧值A一个新值B。如果提供的旧值V和A相等就把B写入V,这个过程是原子性的。

CAS执行结果要么成功要么失败,对于失败的情形下一般采用不断重试,或者放弃。

举例说明:

public class CAS{
    public static void main(String[] args) {
        /*乐观锁的实现CAS;*/
        String data = "123";//共享数据

        /*更新数据的线程会进行如下操作*/
        boolean flag=true;
        while(flag){
            oldValue = date;//保存原始数据
            newValue = doSomething(oldValue);
            //下面的部分为CAS操作,尝试更新出data值
            if(date == oldValue){//比较在更新之前原值有无更改,如没有
                //则更新:
                data = newValue;
                falg = false;//跳出循环
            }
            else{
                //什么也不做,循环重试
            }
        }
    }
}
/*
   很明显,这样的代码根本不是原子性的,
   因为真正的CAS利用了CPU指令,
   这里只是为了展示执行流程,本意是一样的。
*/

ABA问题

CAS实现的过程是先取出内存中某时刻的数据在下一时刻比较并替换,那么在这个时间差会导致数据的变化,此时就有可能出现ABA问题;

什么是ABA问题?

如果另一个线程修改V值,假设原来是A,先修改成B,再修改会A。当前线程的CAS操作无法分辨当前V值是否发生过变化。

举例说明:

在你非常渴的情况下发现一个盛满水的被子,你一饮而尽,之后再给杯子里面重新倒满水,然后你离开,当杯子的真正主人(当前执行CAS操作的线程)看到杯子还是盛满水的,他当然不知道是否被人喝完重新倒满。解决这个问题的方案的一个策略是 每一次倒水假设有一个自动记录仪记录下(Version版本号),这样主人回来就可以分辨在他离开后是否发生过重新倒满的情况。这也是解决ABA问题目前采用的策略。

解决办法:

1.加版本号

2.用AtomicStampedReference/AtomicMarkableReference解决ABA问题

 

补充:

洪山区最新新开了一家自助餐店,为迎接5-1劳动节,老板决定为每位消费卡余额低于20的用户卡里赠送20元,该活动每位顾客只可享受一次;

很简单就用cas技术,先去用户卡里的余额,然后包装成 AtomicInteger,写一个判断,开启10个线程,然后判断小于20的,一律加20,然后就很开心的交差了。可是过了一段时间,发现账面亏损的厉害,老板起先的预支是2000块,因为店里的会员总共也就100多个,就算每人都符合条件,最多也就2000啊,怎么预支了这么多。小王一下就懵逼了,赶紧debug,tail -f一下日志,这不看不知道,一看吓一跳,有个客户被充值了10次!

解释:

用户要给储值卡充值20元,用户卡内余额15元,A线程首先获取余额是15,然后准备加上20,A线程因为某种原因block超时,系统超时重试提交,B线程成功将余额加上20并且成功提交,此时余额为35。但是紧接着用户又消费了20,所以余额还是15,终于A线程获取到了时间片,它比对之后发现余额还是15,所以A线程就执行了。ABA的问题核心在于一个线程在提交的时候,如果只是根据要修改的值和之前是否一样,这样是无法证明这个值没有被其他线程改过因为在这段时间它的值可能被改为其他值,然后又改原来的,实际上如果避免重复提交就能避免ABA问题,而版本号控制可以避免重复提交

0 66

一个锁可以同时是悲观锁可重入锁公平锁可中断锁等等,就像一个人可以是男人、医生、健身爱好者、并不矛盾;

 

synchronized与Lock

Java加锁的方式有两种:

1.使用synchronized关键字

2.用Lock接口的实现类

synchronized关键字是自动挡,可以满足一切日常驾驶需求;但是如果我们想玩飘逸或其他骚操作(刀片超车),就需要手动挡—各种Lock的实现类

所以如果我们只是想要简单加个锁,对性能也没什么特别的要求,用synchronized关键字就够了;自Java 5之后,才在java.util.concurrent.locks包下有了另外一种方式来实现锁,那就是Lock;也就是说,synchronized是Java语言内置的关键字,而Lock是一个接口,这个接口的实现类在代码层面实现了锁的功能;

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

ReentrantLockReadLockWriteLockLock接口最重要的三个实现类对应“可重入锁”“读锁”“写锁”,后面会讲其用途;

ReadWriteLock其实是一个工厂接口,而ReentranReadWriteLock是ReadWriterLock的实现类,它包含两个静态内部类,即ReadLock和WriteLock,这两个静态内部类又实现了Lock接口;

 

各种锁分类的概念、synchronized、各种Lock实现类之间的区别与联系;

 

一、悲观锁和乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略

悲观锁(Pessimistic Lock),很悲观,往坏的方向想,即每次去拿数据的时候都认为别人会修改,因此在每次拿数据的时候都会上锁,这样别人想拿数据就会被挡住,直到悲观锁被释放;

乐观锁(Optimistic Lock),很乐观,往好的方向想,每次去拿数据的时候都认为别人不会修改,所以不会上锁!不会上锁!不会上锁!但是如果想要更新数据,会在更新前检查在读取至更新这段时间别人有没有修改过这个数据,如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(也允许更新失败的线程放弃操作)

乐观锁阻塞事务,悲观锁回滚重试,它们各有优缺点,不能固执的认为一种一定好于另一种;乐观锁适用于写操作比较少的情况下即冲突真的很少发生的时候,这样可以省去锁的开销,加到了系统的整个吞吐量;但如果经常发生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下使用悲观锁就比较合适;

 

二、乐观锁的基础—CAS

说到乐观锁,就必须提到一个概念:CAS

什么是CAS?Compare-and-Swap,即比较并替换,也叫Compare-and-Set的,比较并设置。

1.比较:读取了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动);

2.设置:如果是,将A更新为B,结束。如果不是,则什么也不做;

上面的两步操作具有原子性那个,理解为瞬间完成,在CPU看来就是一步操作;

通过CAS实现一个乐观锁;

data = 123; // 共享数据

/* 更新数据的线程会进行如下操作 */
flag = true;
while (flag) {
    oldValue = data; // 保存原始数据
    newValue = doSomething(oldValue); 

    // 下面的部分为CAS操作,尝试更新data的值
    if (data == oldValue) { // 比较
        data = newValue; // 设置
        flag = false; // 结束
    } else {
	// 啥也不干,循环重试
    }
}
/* 
   很明显,这样的代码根本不是原子性的,
   因为真正的CAS利用了CPU指令,
   这里只是为了展示执行流程,本意是一样的。
*/

这是一个简单直观的乐观锁实现,它允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。

Java中真正的CAS操作调用的native方法

因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!

三、自旋锁

有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。

刚刚的乐观锁就有类似的无限循环操作,那么它是自旋锁吗?

感谢评论区养猫的虾的指正。

不是。尽管自旋与 while(true) 的操作是一样的,但还是应该将这两个术语分开。“自旋”这两个字,特指自旋锁的自旋

然而在JDK中并没有自旋锁(SpinLock)这个类,那什么才是自旋锁呢?读完下个小节就知道了。

四、synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利没有发生阻塞,那么就不存在锁竞争只有当某线程尝试获取锁的时候,发现该锁已经被占用只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。(补充:可以降级,但是条件比较苛刻)

感谢评论区酷帅俊靓美的问题:
偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
还有人对此有疑惑,我之前确实没有描述清楚,但如果要展开讲,涉及到太多新概念,可以新开一篇了。更何况有些太底层的东西,我没读过源码,没有自信说自己一定是对的。其实在升级为轻量级锁之前,虚拟机会让线程A尽快在安全点挂起,然后在它的栈中“伪造”一些信息,让线程A在被唤醒之后,认为自己一直持有的是轻量级锁。如果线程A之前正在同步代码块中,那么线程B自旋等待即可。如果线程A之前不在同步代码块中,它会在被唤醒后检查到这一情况并立即释放锁,让线程B可以拿到。这部分内容我之前也没有深入研究过,如果有说的不对的,请多多指教啊!

五、可重入锁(递归锁)

可重入锁的字面意思是“可以重新进入的锁,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)

Java里只要以Reetrant开头命名的锁都是可重入锁,而且JDK提供的所有线程的Lock实现类,包括synochronized关键字锁都是可重入的如果你需要不可重入锁只能自己去实现。

六、公平锁、非公平锁

如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。

对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。

ReentrantLock构造器可以指定为公平或非公平

对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁

七、可中断锁

可中断锁,字面意思是“可以响应中断的锁”。

这里的关键是理解什么是中断Java没有提供任何直接中断某线程的方法只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。[2]

回到锁的话题上来,如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁

在Java中,synchronized就是不可中断锁Lock的实现类都是可中断锁可以简单看下Lock接口。

/* Lock接口 */
public interface Lock {

    void lock(); // 拿不到锁就一直等,拿到马上返回。

    void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

    boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

    void unlock();

    Condition newCondition();
}

八、读写锁、共享锁、互斥锁

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。

看下Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。

记得之前的乐观锁策略吗?所有线程随时都可以读,仅在写之前判断值有没有被更改。

读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。

虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略因为读写锁并没有更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁乐观锁特指无锁编程,如果仍有疑惑可以再回到第一、二小节,看一下什么是“乐观锁”。

JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock提供了一些便于外界监控其内部工作状态的方法,这里就不一一展开。

九、回到悲观锁和乐观锁

这篇文章经历过一次修改,我之前认为偏向锁和轻量级锁是乐观锁,重量级锁和Lock实现类为悲观锁,网上很多资料对这些概念的表述也很模糊,各执一词。

先抛出我的结论:

我们在Java里使用的各种锁几乎全都是悲观锁synchronized偏向锁、轻量级锁重量级锁全是悲观锁JDK提供的Lock实现类全是悲观锁其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。

那JDK并发包里到底有没有乐观锁呢?

有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。

原子类AtomicInteger的自增方法为乐观锁策略

为什么网上有些资料认为偏向锁、轻量级锁是乐观锁?理由是它们底层用到了CAS?或者是把“乐观/悲观”与“轻量/重量”搞混了?其实,线程在抢占这些锁的时候,确实是循环+CAS的操作,感觉好像是乐观锁。但问题的关键是,我们说一个锁是悲观锁还是乐观锁,总是应该站在应用层看它们是如何锁住应用数据的,而不是站在底层看抢占锁的过程。如果一个线程尝试获取锁时发现已经被占用它是否继续读取数据,等后续要更新时再决定要不要重试?对于偏向锁轻量级锁来说,显然答案是否定的。无论是挂起还是忙等,对应用数据的读取操作都被“挡住”了。从这个角度看,它们确实是悲观锁。

退一步讲,也没有必要在这些术语上狠钻牛角尖,最重要的是理解它们的运行机制。想写得尽量简单一些,却发现洋洋洒洒近万字,只讲了个皮毛。深知自己水平有限,不敢保证完全正确,只能说路漫漫其修远兮,望指正。

参考

  1. ^这里存在一个问题,就是一个值从A变为B,又从B变回了A。这种情况下,CAS可能会认为值没有发生过变化,但实际上是有变化的。对此,并发包下有AtomicStampedReference提供根据版本号判断的实现。ABA问题。
  2. ^Java中断机制: https://www.cnblogs.com/jiangzhaowei/p/7209949.html