java中几种锁

文章内容来自并发编程网的文章阅读,部分示例代码已修改。

1. 自旋锁

 自旋锁就是它的名字一样,让当前线程不停的在一个循环体内执行,当循环的条件被其他线程改变时才能进入临界区。自旋锁的示例代码(仅以非公平锁为例)如下:

public class SpinLock {
    /**
     * 临界区的owner
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        // CAS原子操作预测原来的值为空,在循环体内空循环
        while (!owner.compareAndSet(null, current)) {
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        // CAS原子操作预测原来的值为当前线程
        owner.compareAndSet(current, null);
    }
}

当第一个线程调用lock()时,临界区的owner为空,没有线程占用,原值为空,CAS将owner置为当前线程A,此时第二个线程再来调用lock()时,由于临界区的owner此时是第一个线程A,不为空,因此while()中的条件owner.compareAndSet(null, current)由于预测(null)和实际(线程A)不符不会设置新的owner,返回false(取反为true),所以一直为在那里空循环。直到第一个线程A调用unlock()方法将临界区的owner置为null,第二个线程会退出空循环成为临界区的新owner。

注:自旋锁只是将当前线程不停的执行空循环,不进行线程状态的改变,所以响应速度很快,但当线程数不断增加时,性能会明显下降,因为每个线程都要自旋,占用CPU时间。若线程竞争不激烈,并保持锁的时间段,比较适合使用自旋锁。

1.1 常见3类自旋锁

 自旋锁中有3种常见的锁形式:TicketLock,CLHlock 和 MCSlock。TicketLock主要解决的访问顺序的问题;CLHlock和MCSlock则是两类类似的公平锁,以链表形式进行排序。其中CLHlock会不停的查询前驱变量(隐式队列),所以它不适合在NUMA架构(在该结构下,每个线程分布在不同的物理内存区域)下使用,CLHLock的全称为Craig, Landin, and Hagersten lock,属于自旋锁的一种,它可以确保无饥饿,提供先入先出的公平按序服务,基于链表实现。MCSLock则是对本地变量的节点进行循环(显式队列),不存在CLHlock的问题。重入锁JUC ReentrantLock默认就是使用的CLH锁。3种锁的示例代码如下:

/**
 * @author liuwg-a
 * @date 2019/10/16 9:11
 * @description 自旋锁的一种 由于每次都要查询ServiceNum服务号会影响性能(必须要到主存读取并阻止其他cpu修改)
 */
public class TicketLock {
    private AtomicInteger serviceNum = new AtomicInteger();
    private AtomicInteger ticketNum = new AtomicInteger();
    private static final ThreadLocal<Integer> LOCAL = new ThreadLocal<>();

    public void lock() {
        int myTicket = ticketNum.getAndIncrement();
        LOCAL.set(myTicket);
        while (myTicket != serviceNum.get()) {
        }
    }

    public void unlock() {
        int myTicket = LOCAL.get();
        serviceNum.compareAndSet(myTicket, myTicket + 1);
    }
}

/**
 * @author liuwg-a
 * @date 2019/10/16 19:54
 * @description CLHLock 会不停查询前驱变量
 */
public class CLHLock {
    /**
    * 在创建节点时,初始化行为就将标识符置为 true,表示该节点需要锁,
    * 只要新创建节点就默认表示它需要同步锁
    */
    public static class CLHNode {
        private volatile boolean isLocked = true;
    }

    private volatile CLHNode tail;
    private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>();
    // 采用链表的形式
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");

    public void lock() {
        // 1. 创建一个CLHNode
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        // 2. 线程对tail调用 getAndSet 方法使自己成为 tail,同时获取它前驱节点的引用 preNode
        CLHNode preNode = UPDATER.getAndSet(this, node);
        // 3. 在前驱节点 preNode 上进行旋转,直至前驱节点释放锁
        if (preNode != null) {
            while (preNode.isLocked) {
            }
            // 4. 将前驱节点置为 null
            preNode = null;
            LOCAL.set(node);
        }
    }

    public void unlock() {
        CLHNode node = LOCAL.get();
        // 释放锁时,将当前节点的 isLocked 标识符置为false,表示此时该线程不需要锁
        if (!UPDATER.compareAndSet(this, node, null))){
            node.isLocked = false;
        }
        // 将 node 节点置为 null
        node = null;
    }
}

/**
 * @author liuwg-a
 * @date 2019/10/17 10:46
 * @description MCSLock是对本地变量的节点进行循环
 */
public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }

    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<>();
    @SuppressWarnings("unused")
    private volatile MCSNode queue;
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock() {
        MCSNode current = new MCSNode();
        NODE.set(current);
        MCSNode preNode = UPDATER.getAndSet(this, current);
        if (preNode != null) {
            preNode.next = current;
            while (current.isLocked) {

            }
        }
    }

    public void unlock() {
        MCSNode currentNode = NODE.get();
        if (currentNode.next == null) {
            if (UPDATER.compareAndSet(this, currentNode, null)) {
            } else {
                while (currentNode.next == null) {
                }
            }
        } else {
            currentNode.next.isLocked = false;
            currentNode.next = null;
        }
    }
}

2. 阻塞锁

 阻塞锁会让线程进入阻塞状态,当获得响应的信号时才能进入线程的准备就绪状态,能够和阻塞相关的关键字和方法有:sychronizedReentrantLockObject.wait()以及LockSupport.park()/unpark。阻塞锁的代码示例:

public class BlockLock {
    private static class BlockNode {
        private volatile Thread isLocked;
    }

    private volatile BlockNode tail;
    private static final ThreadLocal<BlockNode> LOCAL = new ThreadLocal<>();
    private static final AtomicReferenceFieldUpdater<BlockLock, BlockNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(BlockLock.class, BlockNode.class, "tail");

    public void lock() {
        BlockNode node = new BlockNode();
        LOCAL.set(node);
        BlockNode preNode = UPDATER.getAndSet(this, node);
        if (preNode != null) {
            preNode.isLocked = Thread.currentThread();
            LockSupport.park(this);
            preNode = null;
            LOCAL.set(node);
        }
    }

    public void unLock() {
        BlockNode node = LOCAL.get();
        if (UPDATER.compareAndSet(this, node, null)) {
            System.out.println("unlock\t" + node.isLocked.getName());
            // 使用 LockSupport.unpark() 阻塞锁
            LockSupport.unpark(node.isLocked);
        }
        node = null;
    }
}

阻塞锁的优势在于:阻塞的线程不会占用CPU的时间,但进入时间和恢复时间比自旋锁慢。在竞争激烈的情况下阻塞锁的性能明显高于自旋锁(阻塞的线程不会额外占用CPU的时间),理想场景下,竞争不激烈可以选用自旋锁,响应比较及时,竞争激烈使用阻塞锁。

3. 可重入锁

 可重入锁,也叫递归锁,本意为一个线程可以重复获取锁,指同一个线程外层函数获取锁后,内层递归函数仍然有获取该锁的代码,但不受影响。在java语言中ReentrantLocksychronized都是可重入锁。

TestLock testLock;

void test1() {
    testLock.lock();
    doSth();
    testLock.unLock();
}

void test2() {
    testLock.lock();
    test1();
    doSth();
    testLock.unLock();
}

在方法test1()test2()方法中都有对同一个锁testLock获取的行为,如果上述的TestLock不是重入锁,调用test2()方法时肯定会出现死锁问题,但如果它是可重入锁,那没有问题。但不要认为所有的死锁问题都可以使用重入锁来代替非重入锁的方式来解决。给出问题场景:如果test2()test()1都在操作同一个共享变量,此时 testLock 是可重入锁,在上述2个方法中同时更改该变量,此时就会出现问题。下面是ReentrantLocksychronized使用的示例代码:

/**
 * 可重入锁测试,结果是每个线程的getter和setter方法都可以输出,没有出现死锁的情况,得出可重入的结论
 * @author liuwg-a
 * @date 2019/11/1 14:22
 * @description
 */
public class ReentrantTest {
    private static final int THREAD_COUNT = 3;

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), new MyFactoryBuilder().build());

        // 为了让2种场景下的结果观测更明显,使用串行的方式进行测试(因为懒,所以写到一个main方法内)
        CountDownLatch synchronizedCount = new CountDownLatch(THREAD_COUNT);
        SynchronizedRunnable synchronizedRunnable = new SynchronizedRunnable(synchronizedCount);
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.execute(synchronizedRunnable);
        }
        try {
            synchronizedCount.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        ReentrantLockRunnable reentrantLockRunnable = new ReentrantLockRunnable();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.execute(reentrantLockRunnable);
        }
    }
}

/**
 * synchronized 关键字可重入测试类
 */
class SynchronizedRunnable implements Runnable {

    private CountDownLatch countDownLatch;

    public SynchronizedRunnable(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        get();
        countDownLatch.countDown();
    }

    public synchronized void get() {
        System.out.println("SynchronizedRunnable.get(): " + Thread.currentThread());
        set();
    }

    public synchronized void set() {
        System.out.println("SynchronizedRunnable.set(): " + Thread.currentThread());
    }

}

/**
 * ReentrantLock 可重入测试类
 */
class ReentrantLockRunnable implements Runnable {

    // 可重入锁
    private ReentrantLock myLock = new ReentrantLock();
//     自旋锁,不可重入锁,测试自旋锁的不可重入性使用这个Lock,会出现死锁的现象,在getter获取锁后,其内部函数无法再次获取lock产生自旋,而外层的getter则始终持有锁
//    private SpinLock myLock = new SpinLock();

    @Override
    public void run() {
        get();
    }

    public void get() {
        myLock.lock();
        System.out.println("ReentrantLockRunnable.get(): " + Thread.currentThread());
        set();
        myLock.unlock();
    }

    public void set() {
        myLock.lock();
        System.out.println("ReentrantLockRunnable.set(): " + Thread.currentThread());
        myLock.unlock();
    }
}

可重入锁重点解决的问题是死锁。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章