java锁的种类以及辨析(转载)

java锁的种类以及辨析(一):自旋锁

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。本系列文章将分析JAVA下常见的锁名称以及特性,为大家答疑解惑。

1、自旋锁

自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。如下

 

01public class SpinLock {
02 
03  private AtomicReference<Thread> sign =newAtomicReference<>();
04 
05  public void lock(){
06    Thread current = Thread.currentThread();
07    while(!sign .compareAndSet(null, current)){
08    }
09  }
10 
11  public void unlock (){
12    Thread current = Thread.currentThread();
13    sign .compareAndSet(current, null);
14  }
15}

使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。

当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

注:该例子为非公平锁,获得锁的先后顺序,不会按照进入lock的先后顺序进行。

Java锁的种类以及辨析(二):自旋锁的其他种类

《java锁的种类以及辨析(转载)》

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。本系列文章将分析JAVA下常见的锁名称以及特性,为大家答疑解惑。

 

2.自旋锁的其他种类

上篇我们讲到了自旋锁,在自旋锁中 另有三种常见的锁形式:TicketLock ,CLHlock 和MCSlock

Ticket锁主要解决的是访问顺序的问题,主要的问题是在多核cpu上

01package com.alipay.titan.dcc.dal.entity;
02 
03import java.util.concurrent.atomic.AtomicInteger;
04 
05public class TicketLock {
06    private AtomicInteger                     serviceNum = new AtomicInteger();
07    private AtomicInteger                     ticketNum  = new AtomicInteger();
08    private static final ThreadLocal<Integer> LOCAL      = new ThreadLocal<Integer>();
09 
10    public void lock() {
11        int myticket = ticketNum.getAndIncrement();
12        LOCAL.set(myticket);
13        while (myticket != serviceNum.get()) {
14        }
15 
16    }
17 
18    public void unlock() {
19        int myticket = LOCAL.get();
20        serviceNum.compareAndSet(myticket, myticket + 1);
21    }
22}

每次都要查询一个serviceNum 服务号,影响性能(必须要到主内存读取,并阻止其他cpu修改)。

CLHLock 和MCSLock 则是两种类型相似的公平锁,采用链表的形式进行排序,

01importjava.util.concurrent.atomic.AtomicReferenceFieldUpdater;
02 
03public class CLHLock {
04    public static class CLHNode {
05        private volatile boolean isLocked = true;
06    }
07 
08    @SuppressWarnings("unused")
09    private volatileCLHNode                                           tail;
10    private static finalThreadLocal<CLHNode>                          LOCAL   = new ThreadLocal<CLHNode>();
11    private static finalAtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,
12                                                                                   CLHNode.class,"tail");
13 
14    public void lock() {
15        CLHNode node = new CLHNode();
16        LOCAL.set(node);
17        CLHNode preNode = UPDATER.getAndSet(this, node);
18        if (preNode != null) {
19            while (preNode.isLocked) {
20            }
21            preNode = null;
22            LOCAL.set(node);
23        }
24    }
25 
26    public void unlock() {
27        CLHNode node = LOCAL.get();
28        if (!UPDATER.compareAndSet(this, node,null)) {
29            node.isLocked = false;
30        }
31        node = null;
32    }
33}

 

CLHlock是不停的查询前驱变量, 导致不适合在NUMA 架构下使用(在这种结构下,每个线程分布在不同的物理内存区域)

MCSLock则是对本地变量的节点进行循环。不存在CLHlock 的问题。

01importjava.util.concurrent.atomic.AtomicReferenceFieldUpdater;
02 
03public class MCSLock {
04    public static class MCSNode {
05        volatile MCSNode next;
06        volatile boolean isLocked = true;
07    }
08 
09    private static finalThreadLocal<MCSNode>                          NODE    = new ThreadLocal<MCSNode>();
10    @SuppressWarnings("unused")
11    private volatileMCSNode                                           queue;
12    private static finalAtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,
13                                                                                   MCSNode.class,"queue");
14 
15    public void lock() {
16        MCSNode currentNode = new MCSNode();
17        NODE.set(currentNode);
18        MCSNode preNode = UPDATER.getAndSet(this, currentNode);
19        if (preNode != null) {
20            preNode.next = currentNode;
21            while (currentNode.isLocked) {
22 
23            }
24        }
25    }
26 
27    public void unlock() {
28        MCSNode currentNode = NODE.get();
29        if (currentNode.next == null) {
30            if (UPDATER.compareAndSet(this, currentNode, null)) {
31 
32            else {
33                while (currentNode.next == null) {
34                }
35            }
36        else {
37            currentNode.next.isLocked = false;
38            currentNode.next = null;
39        }
40    }
41}

从代码上 看,CLH 要比 MCS 更简单,

CLH 的队列是隐式的队列,没有真实的后继结点属性。

MCS 的队列是显式的队列,有真实的后继结点属性。

JUC ReentrantLock 默认内部使用的锁 即是 CLH锁(有很多改进的地方,将自旋锁换成了阻塞锁等等)。

Java锁的种类以及辨析(三):阻塞锁

《java锁的种类以及辨析(转载)》

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。本系列文章将分析JAVA下常见的锁名称以及特性,为大家答疑解惑。

 

三、阻塞锁:

阻塞锁,与自旋锁不同,改变了线程的运行状态。
在JAVA环境中,线程Thread有如下几个状态:

1,新建状态

2,就绪状态

3,运行状态

4,阻塞状态

5,死亡状态

阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(j.u.c经常使用)

下面是一个JAVA 阻塞锁实例

01package lock;
02 
03importjava.util.concurrent.atomic.AtomicReferenceFieldUpdater;
04import java.util.concurrent.locks.LockSupport;
05 
06public class CLHLock1 {
07    public static class CLHNode {
08        private volatile Thread isLocked;
09    }
10 
11    @SuppressWarnings("unused")
12    private volatileCLHNode                                            tail;
13    private static finalThreadLocal<CLHNode>                           LOCAL   = new ThreadLocal<CLHNode>();
14    private static finalAtomicReferenceFieldUpdater<CLHLock1, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock1.class,
15                                                                                    CLHNode.class,"tail");
16 
17    public void lock() {
18        CLHNode node = new CLHNode();
19        LOCAL.set(node);
20        CLHNode preNode = UPDATER.getAndSet(this, node);
21        if (preNode != null) {
22            preNode.isLocked = Thread.currentThread();
23            LockSupport.park(this);
24            preNode = null;
25            LOCAL.set(node);
26        }
27    }
28 
29    public void unlock() {
30        CLHNode node = LOCAL.get();
31        if (!UPDATER.compareAndSet(this, node,null)) {
32            System.out.println("unlock\t" + node.isLocked.getName());
33            LockSupport.unpark(node.isLocked);
34        }
35        node = null;
36    }
37}

在这里我们使用了LockSupport.unpark()的阻塞锁。 该例子是将CLH锁修改而成。

阻塞锁的优势在于,阻塞的线程不会占用cpu时间, 不会导致 CPu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。

在竞争激烈的情况下 阻塞锁的性能要明显高于 自旋锁。

理想的情况则是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。

Java锁的种类以及辨析(四):可重入锁

《java锁的种类以及辨析(转载)》

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。本系列文章将分析JAVA下常见的锁名称以及特性,为大家答疑解惑。

 

四、可重入锁:

本文里面讲的是广义上的可重入锁,而不是单指JAVA下的ReentrantLock。

可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

下面是使用实例

01public class Test implements Runnable{
02 
03    public synchronized void get(){
04        System.out.println(Thread.currentThread().getId());
05        set();
06    }
07 
08    public synchronized void set(){
09        System.out.println(Thread.currentThread().getId());
10    }
11 
12    @Override
13    public void run() {
14        get();
15    }
16    public static void main(String[] args) {
17        Test ss=new Test();
18        new Thread(ss).start();
19        new Thread(ss).start();
20        new Thread(ss).start();
21    }
22}
01public class Test implements Runnable {
02    ReentrantLock lock = new ReentrantLock();
03 
04    public void get() {
05        lock.lock();
06        System.out.println(Thread.currentThread().getId());
07        set();
08        lock.unlock();
09    }
10 
11    public void set() {
12        lock.lock();
13        System.out.println(Thread.currentThread().getId());
14        lock.unlock();
15    }
16 
17    @Override
18    public void run() {
19        get();
20    }
21 
22    public static void main(String[] args) {
23        Test ss = new Test();
24        new Thread(ss).start();
25        new Thread(ss).start();
26        new Thread(ss).start();
27    }
28}

两个例子最后的结果都是正确的,即 同一个线程id被连续输出两次。

结果如下:

Threadid: 8
Threadid: 8
Threadid: 10
Threadid: 10
Threadid: 9
Threadid: 9

可重入锁最大的作用是避免死锁
我们以自旋锁作为例子,

01public class SpinLock {
02    private AtomicReference<Thread> owner =newAtomicReference<>();
03    public void lock(){
04        Thread current = Thread.currentThread();
05        while(!owner.compareAndSet(null, current)){
06        }
07    }
08    public void unlock (){
09        Thread current = Thread.currentThread();
10        owner.compareAndSet(current, null);
11    }
12}

对于自旋锁来说,
1、若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁
说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
2、若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了。实际上不应释放锁。
(采用计数次进行统计)
修改之后,如下:

01public class SpinLock1 {
02    private AtomicReference<Thread> owner =newAtomicReference<>();
03    private int count =0;
04    public void lock(){
05        Thread current = Thread.currentThread();
06        if(current==owner.get()) {
07            count++;
08            return ;
09        }
10 
11        while(!owner.compareAndSet(null, current)){
12 
13        }
14    }
15    public void unlock (){
16        Thread current = Thread.currentThread();
17        if(current==owner.get()){
18            if(count!=0){
19                count--;
20            }else{
21                owner.compareAndSet(current,null);
22            }
23 
24        }
25 
26    }
27}

该自旋锁即为可重入锁。

转载自并发编程网

    原文作者:JUC
    原文地址: http://www.cnblogs.com/chenying99/p/4307668.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞