Java并发Concurrent包的锁(二)——自旋/阻塞/可重入

Java 并发包 Concurrent 的包结构共可分为五个部分:
- 原子变量类
- 锁
- collection并发集合框架
- excutor线程池
- 同步工具

本文介绍锁的一些原理和特征,比如自旋,阻塞,可重入,公平锁和非公平锁。

自旋

比如可以用 synchronized 关键字自己来实现一个简单的锁类 Lock,让它有一个标志 isLocked 来标记锁对象是否正在使用,或者已经释放。

public class Lock {

    private AtomicReference lockedBy = new AtomicReference();

    public void lock() throws InterruptedException {
        Thread current = Thread.currentThread();
        while (!lockedBy.compareAndSet(null, current)) {

        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        lockedBy.compareAndSet(current, null);
    }

}

使用了 CAS 原子操作,当加锁时,预测之前的状态为 null,之后将 owner 设置为当前线程;解锁时,预测之前的状态为当前线程,之后将 owner 设置为 null。这样第一个线程加锁后,如果第二个线程也来加锁,就会一直在 while 中循环,直到第一个线程解锁后,第二个线程才能开始真正开始执行。自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

阻塞

阻塞锁,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
包含阻塞的方法有,synchronized 关键字,ReentrantLock,wait()\notify() , LockSupport.park()/unpart() 。
阻塞锁的优势在于,阻塞的线程不会占用 CPU 时间, 不会导致 CPU 占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。
在竞争激烈的情况下阻塞锁的性能要明显高于自旋锁。在线程竞争不激烈的情况下使用自旋锁,竞争激烈的情况下使用阻塞锁。

可重入

可重入的意思是,以 synchronized 为例,如果当前一个线程进入了代码中的 synchronized 同步块,并因此获得了该同步块使用的同步对象的锁,那么这个线程可以进入由同一个对象所同步的另一个代码块。
比如,下边的例子:

public class Reentrant1 {

    public synchronized void step1(){
        step2();
    }

    public synchronized void step2(){
        // do something
    }

}

如果一个线程已经拥有了一个对象上的锁,那么它就有权访问被这个对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。
如果使用上边自己定义的锁 Lock 类呢:

public class Reentrant2 {

    private Lock lock = new Lock();

    public void step1() throws InterruptedException {
        lock.lock();
        try {
            step2();
        } finally {
            lock.unlock();
        }
    }

    public void step2() throws InterruptedException {
        lock.lock();
        try {
            // do something
        } finally {
            lock.unlock();
        }
    }

}

可以看到,当执行 step1 方法,首先对 lock 对象执行加锁,然后进入 step2 方法,而 step2 方法第一步也是对 lock 对象加锁,由于 Lock 类的 while 中的条件判断,这里第二步会使当前线程进入循环。因为没有判断是哪个线程。
修改为:

public class Lock {

    private AtomicReference lockedBy = new AtomicReference();
    private AtomicInteger lockCount;

    public void lock() throws InterruptedException {
        Thread current = Thread.currentThread();
        while (!lockedBy.compareAndSet(null, current) && !lockedBy.compareAndSet(current, current)) {

        }
        lockCount.incrementAndGet();
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        if (lockCount.decrementAndGet()==0) {
            lockedBy.compareAndSet(current, null);
        }
    }

}

这样修改就判断了是否是前对象加锁的线程了,如果是的话,就允许通过,然后将对象的加锁次数 lockCount 加 1 。在 unlock 方法中,每次调用都减 1 。只有当对象的加锁次数为 0 时,才能解除对象锁的控制。

公平和非公平

公平和非公平在 Reentrant 锁中详细说明。

你可能感兴趣的:(Java)