java并发(7)锁

栏目: Java · 发布时间: 6年前

内容简介:从前的日色变得慢,车,马,邮件都慢,一生只够爱一个人,从前的锁也好看 钥匙精美有样子 你锁了 人家就懂了。木心先生写的这首小诗很有情调,一般来说,你锁住了自己的家门,其他人就进不去了,本文的标题是锁,当然,这个“锁”说的不是锁住家门的锁,而是java中的锁,为了更好的理解java中的锁,先举个简单但不怎么优雅的栗子:

从前的日色变得慢,车,马,邮件都慢,一生只够爱一个人,

从前的锁也好看 钥匙精美有样子 你锁了 人家就懂了。

木心先生写的这首小诗很有情调,一般来说,你锁住了自己的家门,其他人就进不去了,本文的标题是锁,当然,这个“锁”说的不是锁住家门的锁,而是 java 中的锁,为了更好的理解java中的锁,先举个简单但不怎么优雅的栗子:

我们有个共享资源,这个资源是马桶,这个马桶可以被所有人使用,但是同一时刻只能被同一个人使用(毕竟要两个人同时使用一个马桶还是有些不雅),这个时候该怎么解决这个问题呢,很简单,将这个马桶围起来并加一道门和一把锁,这就是我们平常使用的卫生间,加了这个门之后有什么好处呢?我们来模拟一个场景,100个人同时想使用这个马桶,他们蜂拥而至到达卫生间门口,这时跑的最快的一个人将卫生间门打开,并且在里面将门反锁,开始使用马桶,接着剩下的99个人也来到了门口,他们要使用马桶必须将门打开,所以他们开始尝试打开门,但是门已经从里面反锁了,他们无奈打不开,只能等在门口,直到第一个人使用完这个共享马桶,将门打开,这时所有等待的人开始抢占卫生间,一个人抢到之后从里面将门反锁,其他人又只有等待,依次类推。

以上例子对应于java中的加锁解锁过程,而马桶就是共享资源,若等待的人排队等候就是公平锁,否者就是非公平锁,锁的概念非常大,java中有多种锁的实现,而锁又是并发编程中至关重要的一个概念,所以本文作为一个导读,从大体上描述一下锁的概念以及java中的锁,至于对锁的具体分析,将在后文慢慢补充。

锁的概念

先来说说锁的概念,锁从大类上分为 乐观锁悲观锁 ,j.u.c.a包中的类都是乐观锁,其他的ReentrantLock、ReentrantReadWriteLock、synchronized是基于悲观锁实现的,而StampedLock的读锁既有悲观锁实现也有乐观锁实现。

从小类上来分,锁又分为 可重入锁自旋锁独占锁(互斥锁)共享锁公平锁非公平锁 等,而我们常说的读写锁其实是两把锁,写锁是独占锁,读锁是共享锁,接下来阐述一下每种锁的含义

乐观锁

什么是乐观锁,顾名思义,乐观锁乐观的认为共享数据在大多数情况下不会发生冲突,只有少部分时候会发生冲突,在数据提交更新的时候会对冲突进行检查,当检查到冲突的时候,会更新失败,并且返回错误,由用户决定如何对该次失败进行补偿(通常会无限次失败重试),所以整个过程不会对共享数据上锁,称为无锁(lock-free)。

无锁的实现是依赖于底层硬件的,无锁就是指利用处理器的一些特殊的原子指令来避免传统的加锁,而java的乐观锁实现就是基于CAS的,CAS全称是CompareAndSwap,译为比较替换,CAS是无锁的一种实现,也是乐观锁的一种实现,java的Unsafe类有一系列CAS操作的方法,如compareAndSwapInt(Object var1, long var2, int var4, int var5)方法,该方法是一个native方法,该方法最终会通过JNI借助 C语言 来调用CPU底层指令cmpxchg,并且通过判断物理机是否是多核的来决定是否在cmpxchg指令前加上lock(lock cmpxchg),cmpxchg是cpu层面的一条cms指令,该指令保证比较和替换两个操作是原子性的,关于更多的CAS原理详解参考这篇文章 JAVA CAS原理深度分析

在理解了CAS的比较和替换两个操作是原子性的之后(这一点至关重要)再来看java是如何通过CAS来实现乐观锁的(这里强调一下,乐观锁是一种思想,而CAS是乐观锁的一种实现),CAS操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B),只用当预期原值,与内存位置中的值相等时,才会把该内存位置的值设置为新值,否则不做任何处理。什么意思呢,举个栗子:内存中有一个值v = 0, 线程A、B同时取得v的值为0,这时线程A要将v的值设置为1,线程A带上v的预期原值0和新值1,通过CAS,先比较预期原值0等于此时内存中v的值0,所以CAS成功,v的值变成了1;此时线程B也想将v的值设置为1,由于线程B在之前读取到的v的值为0,所以线程B期望此刻内存中v的值没有被其他线程改变,所以线程B对v的期望原值是0,线程B带上v的期望原值0和新值1,通过CAS,一比较发现期望原值0不等于此刻v在内存中的值1,所以设置新值失败,CAS失败,这就是怎个CAS过程。

由于本系列不打算对乐观锁做过多的探讨,所以关于java中使用乐观锁实现的Atomic的类,选一个AtomicInteger类在这里做个简单分析:

AtomicInteger的用法如下,我们知道普通int类型的i++是一个非原子性的操作,但是a.incrementAndGet方法是一个原子性的自增操作,其依赖于CAS。

AtomicInteger a = new AtomicInteger(0);
a.incrementAndGet();

AtomicInteger 持有一个volatile修饰的int类型的value字段,该字段就是用来保存int值的,除此之外还有一个valueOffset字段,该字段记录实例变量value在对象内存中的偏移量,简单来说,通过对象和valueOffset就能找到该对象中value字段的位置进而取得value的值,这主要是为了能在c++中获取到AtomicInteger对象的value值。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    ...
}

AtomicInteger.incrementAndGet()源码:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

可以看到虽然方法名是incrementAndGet,但实际上返回的是unsafe.getAndAddInt + 1,其实这里取了个巧,因为Unsafe类没有addAndGetInt这样的方法。接下来看看Unsafe.getAndAddInt()方法:

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

可以看到该方法有3个参数,第一个var1是object,我们传的this,第二个var2是valueOffset,正是传的上文计算出来的valueOffset,第三个参数是要增加多少,前面提到通过对象和valueOffset就可以拿到该对象的value值,所以var5就是通过getIntVloatile方法拿到了内存中的value值,并且保证拿到的是最新值,然后在while中通过compareAndSwapInt函数进行CAS操作,var1、var2可以确定内存中的value值,var5是期望的value旧值,var5+var4是要替换的value新值,通过上文讲到的CAS操作,如果失败,将会继续循环重试,直到CAS成功,此时内存中value值已经+1,跳出循环,返回旧值var5,然后上层函数返回旧值+1,从而完成了这一个自增操作。

CAS还有一个ABA问题,就是线程A取得变量a的值为0,此时线程B将a设置为1,然后又设置为0,虽然线程B对变量a进行了多次修改,但是在线程A执行CAS操作的时候发现变量a的值还是0没有改变(实际上a由0变成了1,然后又变成了0),就会CAS成功,为了解决这个问题,通常是变量前面加上版本号,版本号每次操作都会增加1,由于版本号只会增加不会减少,所以不会出现ABA问题,A-B-A就变成了1A-2B-3A。

关于乐观锁的介绍就写到这里,后面不再打算继续探讨乐观锁,主要是对几种悲观锁进行详细剖析。

悲观锁

与乐观锁相反,悲观锁悲观的认为共享数据在大部分时间下都会发生冲突,所以只能在线程访问共享数据的时候将其锁住,不让其他线程同时访问,所有线程对共享数据的访问都变成了线性访问,所以不会产生任何并发问题,在java中 synchronized、ReentrantLock、ReentrantReadWriteLock都是悲观锁的实现,关于这几个类会在后文详细分析。

公平锁/非公平锁

公平锁和非公平锁是指,线程获取锁阻塞的时候是否需要排队等候,若阻塞的线程按照请求锁的顺序获得锁,那么这把锁是一把公平锁,若阻塞的线程不按照请求锁的顺序获得锁,而是采用抢占式随机获得锁,那么这把锁就是一把非公平锁。

举个栗子,线程A获得了锁,此时线程BCD依次来请求这把锁,但是无奈锁被A持有了,所以BCD只能等待,这时候又两种策略,一种是BCD排队等候,因为B比C先来,C比D先来,所以按照BCD的顺序排好序,当A释放了锁的时候,排在最前面的B获得锁,B释放之后,C获得,依次类推,这种方式就是公平锁;另一种策略是BCD不排队,等A释放锁的时候,BCD去抢这把锁,谁先抢到谁就持有,这种锁就是非公平锁。

优缺点:

公平锁由于维护了线程请求顺序,保证了时间上的绝对顺序,但是在吞吐量上是远不如非公平锁的,非公平锁容易造成饥饿现象,因为非公平锁是抢占式的,所以某个线程可能长时间无法抢占到锁,而处于长时间阻塞状态

synchronized是一般非公平锁,ReentrantLock可通过构造函数 ReentrantLock(boolean fair) 来指定是否是公平锁,默认是非公平锁,ReentrantReadWriteLock同理

可重入锁

可重入锁是指,当线程A获得锁以后,如果线程A在此请求该锁,它能重新获得该锁,synchronized和ReentrantLock都是可重入锁,仔细想想如果synchronized和ReentrantLock是不可重入锁,那么当两个方法A、B都被synchronized修饰,A方法调用B方法的时候就会发生死锁,因为执行A方法的时候线程已经获取了this对象的锁,然后调用B方法的时候又会去请求一次this对象锁,如果该锁不可重入,则会等待这把锁释放,但是释放这把锁需要A方法执行完成,所以就形成了死锁

表现在代码上:

public class SynchronizedTest {

    public static void main(String[] args){
        new SynchronizedTest().methodA();
    }

    public synchronized void methodA() {
        System.out.println("A is execute ... ");
        B();
    }

    public synchronized void methodB() {
        System.out.println("B is execute ... ");
    }
}

//从结果上看,A()B()方法都得到了执行,没有发生死锁
== 输出 ==
A is execute ...
B is execute ...

自旋锁

自旋锁是指在获取锁失败之后不进入阻塞,而是通过在循环体里面进行不断的重试,前面乐观锁中提到,Unsafe类的CAS操作,在CAS失败的时候会在循环体里面不断重试,直到CAS成功才退出循环,这其实也是一种自旋的表现。

自旋锁由于不会让线程真正的挂起,而是不停的执行循环,所以少了线程状态的切换,清空cpu缓存及重新加载cpu缓存的操作,所以响应速度更快,但是由于自旋锁会占用cpu时间片,所以当线程数量多了之后性能会明显下降

在jdk8中对synchronized关键字进行了一系列优化,引入了轻量级锁、重量级锁、偏向锁、自适应自旋锁等手段,其中自适应自旋锁就是在获取锁失败之后开始自旋,自旋次数不固定,而是根据以前线程自旋期间成功获取到错的次数,也就是说,如果上一个线程通过自旋获取到了锁,那么认为这一个线程通过自旋获取锁的成功率会很高,所以当前线程的自旋次数会增加,相反,如果上一个线程达到最大自旋次数任然没有获取到锁,那么认为自旋获取锁的失败率会很高,所以当前线程的自旋次数会减少,甚至可能出现若是多个线程连续自旋获取锁失败,那么当前线程不再自旋,而是直接挂起。加入自适应自旋锁很好的解决了:如果自旋次数固定,由于任务的差异,导致每次的最佳自旋次数有差异,不好拿捏自旋次数的问题,而是通过“智能学习”的方式动态改变自旋次数

关于synchronized的优化手段还有很多,由于篇幅限制,将在后文单独写一篇来阐述。

独占锁/共享锁

独占锁和共享锁也是一个比较大的概念,如果一把锁同一时刻只能被一个线程持有,则这把锁是独占锁,如果一把锁同一时刻能被多个线程同时持有,则这把锁叫做共享锁。

java中的synchronized和ReentrantLock都是独占锁,而ReentrantReadWriteLock的写锁是一把独占锁,读锁是一把共享锁,关于读写锁ReentrantReadWriteLock和可重入锁ReentrantLock将在后文详细介绍。

后记

本文简单的阐述了几种锁的概念,以及java类中用到的几种锁,简单总结一下:

  1. atomic: 乐观锁、共享锁、无锁、非阻塞同步
  2. synchronized:悲观锁、独占锁、可重入锁、非公平锁、自旋锁
  3. ReentrantLock: 悲观锁、可重入锁、(公平锁|非公平锁)、独占锁
  4. ReentrantReadWriteLock.ReadLock: 悲观锁、可重入锁、(公平锁|非公平锁)、共享锁
  5. ReentrantReadWriteLock.WriteLock: 悲观锁、可重入锁、(公平锁|非公平锁)、独占锁

关于几种锁的基本概念暂时先了解到这里,现在有个显而易见的问题,我们说了这么久的锁,这把锁到底存放在什么地方,这把锁到低锁住的是什么,我们经常使用的同步代码块语法: synchronized(this){} 又是什么意思?这一系列问题以及上文提到的synchronized锁优化内容都将会在下一篇文章中详细介绍。

可以卑微如尘土,不可扭曲如蛆虫


以上所述就是小编给大家介绍的《java并发(7)锁》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Alone Together

Alone Together

Sherry Turkle / Basic Books / 2011-1-11 / USD 28.95

Consider Facebookit’s human contact, only easier to engage with and easier to avoid. Developing technology promises closeness. Sometimes it delivers, but much of our modern life leaves us less connect......一起来看看 《Alone Together》 这本书的介绍吧!

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具