java的锁分类

一、基础锁

java中的锁机制的实现基础有2种。第一种来自于JVM的原生支持,这一类通过java虚拟机来直接实现,在语言层面的体现就是synchronized关键字。第二种的是在JUC(即java.util.concurrent)中通过LockSupport类来实现的。

文中的代码地址:https://gitee.com/fireXXXXXX/study

1.1、JVM中的锁

1、synchronized关键字

synchronized是java中一个重要的关键字,用于限制多线程互斥访问临界资源,synchronized的实现依赖于java中的实例对象结构,如图为一个典型的32位java虚拟机中java实例对象结构。

clipboard.png

mark word为synchronized起作用的关键。mark word的主要分类为4种:

  • 无锁状态

无锁状态很好理解,就是不适用任何会阻塞线程执行的代码来协调执行顺序。

public static void main(String[] args) {
    Object head = new Object();
    System.out.println(JavaHeadParser.parse(head).toJson());
}

由于JVM默认开启偏向锁,在使用JVM参数-XX:-UseBiasedLocking关闭偏向锁后,可以看到此时打印的结果为

{"markWord": {"hashCode": 0,"age": 0,"lockType": "none"},"instanceSize": 0,"spaceLoss": 0}
  • 偏向锁

偏向锁是JDK1.6之后引入来优化synchronized性能的一个方法,它默认当前属于单线程场景或多线程之间是交替使用资源,不存在锁竞争,因此同一线程二次进入同步代码块无需获取锁。使用偏向锁需要加JVM参数-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

public static void main(String[] args) throws JsonProcessingException {
    Object head = new Object();

    System.out.println(JavaHeadParser.parse(head).toJson());
    synchronized (head) {
        System.out.println("加锁");
        System.out.println(JavaHeadParser.parse(head).toJson());
    }

    System.out.println(JavaHeadParser.parse(head).toJson());
}

此时的打印结果为

{"markWord":{"threadId":0,"epoch":0,"age":0,"lockType":"biasedLock"},"instanceSize":0,"spaceLoss":0}
加锁
{"markWord":{"threadId":55386,"epoch":1,"age":0,"lockType":"biasedLock"},"instanceSize":0,"spaceLoss":0}
{"markWord":{"threadId":55386,"epoch":1,"age":0,"lockType":"biasedLock"},"instanceSize":0,"spaceLoss":0}

可以看到,在进入同步代码块后,对象头的threadId变为了当前线程的id(这个id是操作系统中的线程id)

  • 轻量级锁

轻量级锁是JDK1.6之后引入来优化synchronized性能的一个方法,它默认当前锁竞争很少,就算出现竞争自旋一下也就拿到锁了。

想要看到轻量级锁出现很简单,只要JVM禁止使用偏向锁就可以,因此JVM参数添加-XX:-UseBiasedLocking,代码和偏向锁一样。

此时打印的结果为

{"markWord":{"hashCode":0,"age":0,"lockType":"none"},"instanceSize":0,"spaceLoss":0}
加锁
{"markWord":{"lockPoint":11402638,"lockType":"light"},"instanceSize":0,"spaceLoss":0}
{"markWord":{"hashCode":0,"age":1,"lockType":"none"},"instanceSize":0,"spaceLoss":0}

偏向锁和轻量级锁很相似,偏向锁通过指向线程id来标识线程,轻量级锁通过指向当前线程堆栈锁记录来表示线程。

  • 重量级锁

根据上面的对象头结构可以看出,重量级锁是通过objectmonitor来实现的,在JDK1.5之前,是直接通过字节码指令来实现的。

public class Test11 {

    public synchronized void testFunction() {
        int a = 1;
    }

    public void testArg() {
        Object a = new Object();
        synchronized (a) {
            int b = 1;
        }
    }

}

javap -v Test11.class反编译后结果可以看到

image-20201220162133215.png

image-20201220162105343.png

对于方法同步和代码块同步分别通过monitorenter/monitorexit以及ACC_SYNCHRONIZED来在JVM层创建重量级锁,但是一般会通过偏向锁和轻量级锁来尝试替代掉重量锁。

测试代码为2个线程并发调用:

public class TestSyncAndLockPerformance {

    private static Map<String, IFixedTimeConsumeTask> tasks = new HashMap<>();
    private static IFixedTimeConsumeTask syncTask = new SyncConsumeTask();

    static {
        tasks.put("sync", syncTask);
    }

    private static void test(int thread, int sleepTime, int sample, ConsumeTaskType consumeTaskType) throws InterruptedException, IllegalAccessException, InstantiationException {
        ConsumeTimeArg arg = new ConsumeTimeArg(sleepTime, consumeTaskType);
        nullTask.init(arg);
        syncTask.init(arg);
        lockTask.init(arg);
        atomicTask.init(arg);

        String sceneName = thread + "-" + sleepTime + "-" + consumeTaskType.toString();
        FixedTimeScene fixedTimeScene = new FixedTimeScene();
        IResult result = fixedTimeScene.startSync(sceneName, thread, sample, tasks, MultiResult.class);
        result.show(sceneName);
    }

    public static void main(String[] args) throws InterruptedException, InstantiationException, IllegalAccessException, JsonProcessingException {
        test(2, 16, 10, ConsumeTaskType.io);
    }

}

此时打印的结果为:

{"markWord":{"monitorPoint":102419874,"lockType":"heavy"},"instanceSize":0,"spaceLoss":0}

2、锁的优化

JVM中针对锁的优化操作有很多,包括锁粗化(加大锁的范围避免频繁释放获取锁)、锁消除(通过逃逸分析去除调不存在竞争的临界区的锁)、锁膨胀(消耗资源低的锁会随着场景的不同逐渐膨胀为消耗资源高的锁)。

这里着重介绍一下锁膨胀。锁的膨胀顺序为:无锁->偏向锁->轻量级锁->重量级锁。具体膨胀的顺序见下图。

2bcc8161c52eb100d2c7c4c96c70d3c5823.jpg

1.2、JUC中的锁

1、unsafe&LockSupport

JVM原生支持的锁是通过java的实例对象头结构来实现的,对于java类库中的锁实现方式截然不同,主要是通过JNI来调用操作系统底层的CAS和线程操作的API来实现的,其中实现这个操作最基础的一个类就是unsafe类。LockSupport只是做了unsafe调用的简单封装,这里着重接招一下unsafe类。

public final class Unsafe {
    ......
    
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
        
    public native void unpark(Object var1);

    public native void park(boolean var1, long var2);
    
    ......
}

compareAndSwap***是CAS的操作,park会消费许可,unpark会生产许可,调用park如果没有许可会当前线程会阻塞状态。unpark需要指定线程来发送许可,多次生产也只会有1个许可,因此可以把许可看作一个0-1信号量。

其中还实现了一些自旋锁的操作:

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

2、AQS

AQS是JDK1.5引入的线程同步器框架,实现一个线程同步器最基本的三要要素:线程阻塞队列、共享资源的互斥访问、线程的阻塞和环境。

本文的主题是介绍锁的实现,因此这里着重介绍一下AQS中共享资源的互斥访问,AQS中需要互斥访问的资源为线程的阻塞队列和共享资源。

  • 共享资源

AQS的共享资源使用一个整型的state变量来表示,不过对于共享资源的争用基本都是在实现类实现的,简单的拿CountDownLatch来看:

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

getState()获取共享资源的数量,对于共享资源的修改是通过乐观锁+自旋来实现的,如上图代码,首先取得初始state的值放到变量c,然后修改之后通过CAS来设置回state,修改不成功就重复尝试。

  • 线程的阻塞队列

AQS的阻塞队列用的是一个先进先出的链式队列,队列的每个Node都是一个等待资源的线程。修改队列的过程都是通过CAS操作来完成的,如下为AQS入队API的源码(head、tail为队列的头尾):

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
img

图3

二、衍生锁

Java基础锁中的synchronized在JVM实现,因此衍生锁都是基于JUC来做的,主要是通过JUC的AQS来实现,AQS的实现主要包括两大部分:同步器和锁。同步器主要包括Semaphore、CountDownLatch和LimitLatch,锁的实现主要包括ReentrantLock和ReentrantReadWriteLock,ReentrantReadWriteLock平时使用较少,这里着重介绍可重入锁ReentrantLock的实现。

2.1、ReentrantLock

可重入锁是通过类似0-1同步信号量来实现的,想要lock的线程会去消费信号量,unlock的线程会去生产信号量。

这里面采取的同步信号量包括公平争用和不公平争用两种方式。

  • 非公平争用

资源获取

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            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;
}

核心逻辑在于acquire函数,它表达的意思很简单,申请获取资源(tryAcquire),要是获取不到资源就把自己插入等待队列(acquireQueued),获取完之后等待线程会唤醒(selfInterrupt)。

资源释放

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

资源的释放的同时会唤醒一个阻塞的线程,因此前面调用lock没获取到资源被阻塞的线程会被唤醒。

  • 公平争用

和非公平争用类似,只是这里会进入acquire后直接调用hasQueuedPredecessors去排队,源码差异如下:

final void lock() {
    acquire(1);
}

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

综合以上:

非公平锁在获取锁的时候是先直接尝试获取锁,如果成功就不进行排队操作

公平锁在获取锁的时候通过hasQueuedPredecessors() 方法判断是否有其它线程等待获取锁的时间超过当前线程,如果有则表明同步队列中已有线程先行进行等待获取锁,就会进入队列排队。

2.3、原子锁

Java中的原子锁对应着不同的数据类型,主要分为三类:

1、基本数据类型

AtomicBoolean、AtomicInteger、AtomicLong

2、数据类型

AtomicIntegerArray、AtomicLongArray

3、引用类型

AtomicMarkableReference、AtomicReference、AtomicReferenceArray、AtomicStampedReference

原子类型的实现很简单,都是基于unsafe类的CAS操作来实现的,这里以AtomicBoolean的getAndSet为例,采用的是CAS+自旋

public final boolean getAndSet(boolean newValue) {
    boolean prev;
    do {
        prev = get();
    } while (!compareAndSet(prev, newValue));
    return prev;
}

2.4、自旋锁

自旋锁实际和前面列举的不属于一个分类,它只是一种应对临界资源等待时间很短的时的一种处理手段,主要为了避免频繁线程阻塞导致系统调用过多影响性能,操作手法就是通过循环判断来代替直接调用线程阻塞的API。

三、性能测试

3.1、测试方案

1、task每次完成业务操作(通过休眠来模拟IO密集型的操作,自旋来模拟CPU密集型的操作)都会计数

2、任务分为sync、lock、atomic三种

3、最终根据任务计数的总量来确定那种类型的任务计数最高,以此来确定性能差异

例如test(1, 16, 10, ConsumeTaskType.io);表示1个线程业务操作时间16ms,IO密集型操作,运行10s

public class TestSyncAndLockPerformance {

    private static Map<String, IFixedTimeConsumeTask> tasks = new HashMap<>();
    private static IFixedTimeConsumeTask syncTask = new SyncConsumeTask();
    private static IFixedTimeConsumeTask lockTask = new LockConsumeTask();
    private static IFixedTimeConsumeTask atomicTask = new AtomicConsumeTask();

    static {
        tasks.put("sync", syncTask);
        tasks.put("lock", lockTask);
        tasks.put("atomic", atomicTask);
    }

    private static void test(int thread, int sleepTime, int sample, ConsumeTaskType consumeTaskType) throws InterruptedException, IllegalAccessException, InstantiationException {
        ConsumeTimeArg arg = new ConsumeTimeArg(sleepTime, consumeTaskType);
        syncTask.init(arg);
        lockTask.init(arg);
        atomicTask.init(arg);

        String sceneName = thread + "-" + sleepTime + "-" + consumeTaskType.toString();
        FixedTimeScene fixedTimeScene = new FixedTimeScene();
        IResult result = fixedTimeScene.startSync(sceneName, thread, sample, tasks, MultiResult.class);
        result.show(sceneName);
    }

    public static void main(String[] args) throws InterruptedException, InstantiationException, IllegalAccessException, JsonProcessingException {
        //1,2,5,10,20个线程休眠16ms
        test(1, 16, 10, ConsumeTaskType.io);
        test(2, 16, 10, ConsumeTaskType.io);
        test(5, 16, 10, ConsumeTaskType.io);
        test(10, 16, 10, ConsumeTaskType.io);
        test(20, 16, 10, ConsumeTaskType.io);
        //1,2,5,10,20个线程休眠128ms
        test(1, 128, 10, ConsumeTaskType.io);
        test(2, 128, 10, ConsumeTaskType.io);
        test(5, 128, 10, ConsumeTaskType.io);
        test(10, 128, 10, ConsumeTaskType.io);
        test(20, 128, 10, ConsumeTaskType.io);

        //1,2,5,10,20个线程休眠16ms
        test(1, 16, 10, ConsumeTaskType.cpu);
        test(2, 16, 10, ConsumeTaskType.cpu);
        test(5, 16, 10, ConsumeTaskType.cpu);
        test(10, 16, 10, ConsumeTaskType.cpu);
        test(20, 16, 10, ConsumeTaskType.cpu);
        //1,2,5,10,20个线程休眠128ms
        test(1, 128, 10, ConsumeTaskType.cpu);
        test(2, 128, 10, ConsumeTaskType.cpu);
        test(5, 128, 10, ConsumeTaskType.cpu);
        test(10, 128, 10, ConsumeTaskType.cpu);
        test(20, 128, 10, ConsumeTaskType.cpu);
    }

}

3.2、测试结果

线程数 业务耗时 业务类型 任务完成计数(sync/lock/aotmic)
1 16ms IO 598/598/597
2 16ms IO 595/1188/1202
5 16ms IO 594/2745/2980
10 16ms IO 597/4745/5990
20 16ms IO 596/10239/11980
1 128ms IO 77/77/77
2 128ms IO 77/154/154
5 128ms IO 77/347/385
10 128ms IO 77/552/770
20 128ms IO 77/1094/1540
1 16ms CPU 625/603/625
2 16ms CPU 605/1147/1166
5 16ms CPU 625/1730/3035
10 16ms CPU 513/3683/4567
20 16ms CPU 536/2248/4172
1 128ms CPU 76/76/76
2 128ms CPU 75/142/152
5 128ms CPU 68/278/375
10 128ms CPU 74/655/719
20 128ms CPU 76/902/800

1、synchronized在多线程性能效果很差,具体原因可以打印观察锁文件头得知,在一开始发生资源竞争时,锁就已经膨胀成重量级锁了,而JDK1.5之后锁的优化手段适用的场景很小。

2、提升争用的线程无法提升synchronized的性能。

3、对于低耗时(16ms以下)的业务操作,lock和atomic性能差异越发明显。

四、总结

4.1、锁机制总览图

java中的锁.png

图4

4.2、不同锁的适用范围

1、synchronized代码简单,但是对多线程并发提升很小,建议尽量少用。

2、JUC的可重入锁适用于资源竞争激烈的条件下使用,而且性能比synchronized优异很多,随着并发线程的提高,性能差异变得越发明显。

3、对于单变量资源共享的场景尽量使用原子操作,原子操作性能会明显优于lock,特别是在并发高的场景。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容