J.U.C之AQS

AQS是J.U.C的核心

AQS(AbstractQueuedSynchronizer)队列同步器,AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。

同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待和唤醒等底层操作。

同步队列中的节点用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。

同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。
如果一个线程没有获得同步状态,那么包装它的节点将被加入到队尾,显然这个过程应该是线程安全的。因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递一个它认为的尾节点和当前节点,只有设置成功,当前节点才被加入队尾。这个过程如下所示
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,这一过程如下:
 
独占式同步状态获取
节点进入同步队列后,就进入了自旋的过程,每个节点都在自省的观察,头结点出队列时,自己的前驱节点是否是头结点,如果是,尝试获取同步状态。可以看见节点和节点之间在循环检查的过程中基本不相互通信,而是简单的判断自己的前驱节点是否是头结点,这样就使得节点的释放符合FIFO。
 

总结:在获取同步状态时,同步器维护这一个同步队列,并持有对头节点和尾节点的引用。获取状态失败的线程会被包装成节点加入到尾节点后面称为新的尾节点,在进入同步队列后开始自旋,停止自旋的条件就是前驱节点为头节点并且成功获取到同步状态。在释放同步状态时,同步器调用tryRelease方法释放同步状态,然后唤醒头节点的后继节点。

共享式同步状态获取

 共享式获取与独占式获取的区别就是同一时刻是否可以多个线程同时获取到同步状态。

设计原理

  • 使用Node实现FIFO队列

  • 维护了一个volatile int state(代表共享资源)

  • 使用方法是继承,基于模板方法

  • 子类通过继承同步器并实现它的抽象方法来管理同步状态

  • 可以实现排它锁和共享锁的模式(独占、共享)

具体实现的思路

1.首先 AQS内部维护了一个CLH队列,多线程争用资源被阻塞时会进入此队列。同时AQS管理一个关于共享资源状态信息的单一整数volatile int state,该整数可以表现任何状态,同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)

2.线程尝试获取锁,如果获取失败,则将等待信息等包装成一个Node结点,加入到同步队列Sync queue里

3.不断重新尝试获取锁(当前结点为head的直接后继才会 尝试),如果获取失败,则会阻塞自己,直到被唤醒

4.当持有锁的线程释放锁的时候,会唤醒队列中的后继线程

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch),独占式或者共享式获取同步状态state。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

AQS同步组件

  • CountDownLatch
  • Semaphore
  • CyclicBarrier
  • ReentrantLock
  • Condition
  • FutureTask

独占锁:ReentrantLock

共享锁:CountDownLatch, CyclicBarrier, Semaphore

共享和独占:ReentrantReadWriteLock

CountDownLatch

同步阻塞类,可以完成阻塞线程的功能

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。

 

使用场景

1.程序执行需要等待某个条件完成后,才能进行后面的操作。比如父任务等待所有子任务都完成的时候,在继续往下进行

实例1:基本用法

@Slf4j
public class CountDownLatchExample1 {

    private final static int threadCount = 200;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                } finally {
                    // 为防止出现异常,放在finally更保险一些
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        log.info("finish");
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        Thread.sleep(100);
        log.info("{}", threadNum);
        Thread.sleep(100);
    }
}
View Code

2.比如有多个线程完成一个任务,但是这个任务只想给他一个指定的时间,超过这个任务就不继续等待了。完成多少算多少

@Slf4j
public class CountDownLatchExample2 {

    private final static int threadCount = 200;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
           // 放在这里没有用的,因为这时候还是在主线程中阻塞,阻塞完以后才开始执行下面的await
           // Thread.sleep(1);
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
       // 等待指定的时间 参数1:等待时间 参数2:时间单位
        countDownLatch.await(10, TimeUnit.MILLISECONDS);
        log.info("finish");
       // 并不是第一时间内销毁掉所有线程,而是先让正在执行线程执行完
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        Thread.sleep(100);
        log.info("{}", threadNum);
    }
}
View Code

Semaphore

控制某个资源能被并发访问的次数

使用场景

1.仅能提供有限访问的资源:比如数据库的连接数最大只有20,而上层的并发数远远大于20,这时候如果不做限制,可能会由于无法获取连接而导致并发异常,这时候可以使用Semaphore来进行控制,当信号量设置为1的时候,就和单线程很相似了

实例1:每次获取1个许可

@Slf4j
public class SemaphoreExample1 {

    private final static int threadCount = 20;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    semaphore.acquire(); // 获取一个许可
                    test(threadNum);
                    semaphore.release(); // 释放一个许可
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}
View Code

实例2:一次性获取多个许可

@Slf4j
public class SemaphoreExample2 {

    private final static int threadCount = 20;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    semaphore.acquire(3); // 获取多个许可
                    test(threadNum);
                    semaphore.release(3); // 释放多个许可
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}
View Code

2.并发很高,想要超过允许的并发数之后,就抛弃

@Slf4j
public class SemaphoreExample3 {

    private final static int threadCount = 20;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try{
                    if (semaphore.tryAcquire()) { // 尝试获取一个许可
   // 本例中只有一个三个线程可以执行到这里
                        test(threadNum);
                        semaphore.release(); // 释放一个许可
                    }
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}
View Code

3.尝试获取获取许可的次数以及超时时间都可以设置

@Slf4j
public class SemaphoreExample4 {

    private final static int threadCount = 20;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    if (semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)) { // 尝试获取一个许可
                        test(threadNum);
                        semaphore.release(); // 释放一个许可
                    }
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}
View Code

CyclicBarrier

同步辅助类,允许一组线程相互等待,知道所有线程都准备就绪后,才能继续操作,当某个线程调用了await方法之后,就会进入等待状态,并将计数器-1,直到所有线程调用await方法使计数器为0,才可以继续执行,由于计数器可以重复使用,所以我们又叫他循环屏障。

CyclicBarrier与CountDownLatch区别

1.CyclicBarrier可以重复使用(使用reset方法),CountDownLatch只能用一次
2.CountDownLatch主要用于实现一个或n个线程需要等待其他线程完成某项操作之后,才能继续往下执行,描述的是一个或n个线程等待其他线程的关系,而CyclicBarrier是多个线程相互等待,知道满足条件以后再一起往下执行。描述的是多个线程相互等待的场景

可以设置等待时间

@Slf4j
public class CyclicBarrierExample1 {

   // 1.给定一个值,说明有多少个线程同步等待
    private static CyclicBarrier barrier = new CyclicBarrier(5);

    public static void main(String[] args) throws Exception {

        ExecutorService executor = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            // 延迟1秒,方便观察
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
       // 2.使用await方法进行等待
        barrier.await();
        log.info("{} continue", threadNum);
    }
}
View Code
@Slf4j
public class CyclicBarrierExample2 {

    private static CyclicBarrier barrier = new CyclicBarrier(5);

    public static void main(String[] args) throws Exception {

        ExecutorService executor = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        try {
            // 由于状态可能会改变,所以会抛出BarrierException异常,如果想继续往下执行,需要加上try-catch
            barrier.await(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.warn("BarrierException", e);
        }
        log.info("{} continue", threadNum);
    }
}
View Code
@Slf4j
public class CyclicBarrierExample3 {

    private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
       // 当线程全部到达屏障时,优先执行这里的runable
        log.info("callback is running");
    });

    public static void main(String[] args) throws Exception {

        ExecutorService executor = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        barrier.await();
        log.info("{} continue", threadNum);
    }
}
View Code

Lock

ReentrantLock与Condition

java一共分为两类锁,一类是由synchornized修饰的锁,还有一种是JUC里提供的锁,核心就是ReentrantLock

synchornized与ReentrantLock的区别对比:

 

对比维度synchornizedReentrantLock
可重入性(进入锁的时候计数器自增1) 可重入 可重入
锁的实现 JVM实现,很难操作源码,得到实现 JDK实现
性能 在引入轻量级锁后性能大大提升,建议都可以选择的时候选择synchornized -
功能区别 方便简洁,由编译器负责加锁和释放锁 手工操作
粒度、灵活度 粗粒度,不灵活
可否指定公平所 不可以 可以
可否放弃锁 不可以 可以

基本使用

@Slf4j
@ThreadSafe
public class LockExample2 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static int count = 0;

    private final static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}
View Code

公平锁非公平锁以及可重入的理解

轻松学习java可重入锁(ReentrantLock)的实现原理

Condition

Condition的特性:

1.Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。

2.Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。      

 如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。  但是,通过Condition,就能明确的指定唤醒读线程。

public class Task {
    
    private final Lock lock = new ReentrantLock();
    
    private final Condition addCondition = lock.newCondition();
    
    private final Condition subCondition = lock.newCondition();
    
    
    private static int num = 0;
    private List<String> lists = new LinkedList<String>();
    
    public void add() {
        lock.lock();
        
        try {
            while(lists.size() == 10) {//当集合已满,则"添加"线程等待
                addCondition.await();
            }
            
            num++;
            lists.add("add Banana" + num);
            System.out.println("The Lists Size is " + lists.size());
            System.out.println("The Current Thread is " + Thread.currentThread().getName());
            System.out.println("==============================");
            this.subCondition.signal();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {//释放锁
            lock.unlock();
        }
    }
    
    
    public void sub() {
        lock.lock();
        
        try {
            while(lists.size() == 0) {//当集合为空时,"减少"线程等待
                subCondition.await();
            }
            
            String str = lists.get(0);
            lists.remove(0);
            System.out.println("The Token Banana is [" + str + "]");
            System.out.println("The Current Thread is " + Thread.currentThread().getName());
            System.out.println("==============================");
            num--;
            addCondition.signal();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
}
View Code

Condition的实现分析

 ConditionObject类是AQS的内部类,实现了Condition接口。每个Condition对象都包含着一个等待队列,这个队列是实现等待/通知功能的关键。在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并非包中的Lock(同步器)拥有一个同步队列和多个等待队列。等待队列和同步队列一样,使用的都是同步器AQS中的节点类Node。 同样拥有首节点和尾节点, 每个Condition对象都包含着一个FIFO队列。

 等待:

如果一个线程调用了Condition.await()方法,那么该线程就会释放锁,构成节点加入等待队列并进入等待状态。相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。

 

通知:

调用Condition.signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中,加入到获取同步状态的竞争中,成功获取同步状态(或者说锁之后),被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功获取了锁。

 

=============================================================================

ReentrantReadWriteLock

ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。
“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。
“写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。

@Slf4j
public class LockExample3 {

    private final Map<String, Data> map = new TreeMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

// 在没有任何读写锁的时候才可以进行写入操作
    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            readLock.unlock();
        }
    }

    class Data {

    }
}
View Code

特性:

  1. 公平性选择:支持公平和非公平(默认)两种获取锁的方式,非公平锁的吞吐量优于公平锁;
  2. 可重入:支持可重入,读线程在获取读锁之后能够再次获取读锁,写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁(同一线程)
  3. 锁降级:线程获取锁的顺序遵循获取写锁,获取读锁,释放写锁,写锁可以降级成为读锁。

 优点:

  1. 通过分离读锁和写锁,能够提供比排它锁更好的并发性和吞吐量。
  2. 读写锁能够简化读写交互场景的编程。

针对第二点,比如说一个共享的用作缓存数据结构,大部分时间提供读服务,而写操作占有的时间较少,但是写操作完成后的更新需要对后序的读服务可见。

在没有读写锁支持的时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行 通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键字进行同步),这样做的目的是使读操作都能读取到正确的数据,而不会出现脏读。

改用读写锁实现上述功能,只需要在读操作时获取读锁,而写操作时获取写锁即可,当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被 阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

读写锁实现简单的Cache

public class Cache { 
  static Map<String, Object> map = new HashMap<String, Object>(); 
  static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 
  static Lock r = rwl.readLock(); 
  static Lock w = rwl.writeLock(); 
  // 获取一个key对应的value 
  public static final Object get(String key) { 
    r.lock(); 
    try { 
      return map.get(key); 
    } finally { 
      r.unlock(); 
    } 
  } 

  // 设置key对应的value,并返回旧有的value 
  public static final Object put(String key, Object value) { 
    w.lock(); 
    try { 
      return map.put(key, value); 
    } finally { 
      w.unlock(); 
    } 
  } 

  // 清空所有的内容 
  public static final void clear() { 
    w.lock(); 
    try { 
      map.clear(); 
    } finally { 
      w.unlock(); 
    } 
  } 
} 
View Code

Cache使用读写锁提升读操作并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

读写锁的实现分析:

1.读写状态设计

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。回想ReentrantLock中自定义同步器的实现,同步状态 表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写 锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁是将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图1所示。

2.锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
       // Must release read lock before acquiring write lock
       rwl.readLock().unlock();
       rwl.writeLock().lock();
       try {
         // Recheck state because another thread might have
         // acquired write lock and changed state before we did.
         if (!cacheValid) {
           data = ...
           cacheValid = true;
         }
         // Downgrade by acquiring read lock before releasing write lock
         rwl.readLock().lock();
       } finally {
         rwl.writeLock().unlock(); // Unlock write, still hold read
       }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性。就例子的代码来说,是需要锁降级获取读锁的。如果不这样,在释放完写锁后,别的线程(假设B)可能在当前线程(假设A)还没有执行到user(data)时获取到写锁,然后修改data的值,当线程A恢复运行后,由于可见性问题,此时线程A的data已经不是正确的data了,当前线程无法感知线程B的数据更新。使用读锁可以保证在线程A获取读锁时别的线程无法修改data

这里要着重讲一讲“无法感知”是什么意思:

也就是说,在另一个线程(假设叫线程1)修改数据的那一个瞬间,当前线程(线程2)是不知道数据此时已经变化了,但是并不意味着之后线程2使用的数据就是旧的数据,相反线程2使用还是被线程1更新之后的数据。也就是说,就算我不使用锁降级,程序的运行结果也是正确的(这是因为锁的机制和volatile关键字相似)。“感知”其实是想强调读的实时连续性,但是却容易让人误导为强调数据操作。

RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。原因也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程不可见。

使用java的ReentrantReadWriteLock读写锁时,锁降级是必须的么?

不是必须的。

在这个问题里,如果不想使用锁降级

  1. 可以继续持有写锁,完成后续的操作。
  2. 也可以先把写锁释放,再获取读锁。

但问题是

  1. 如果继续持有写锁,如果 use 函数耗时较长,那么就不必要的阻塞了可能的读流程
  2.  如果先把写锁释放,再获取读锁。在有些逻辑里,这个 cache 值可能被修改也可能被移除,这个取决于能不能接受了。
  3. 另外,降级锁比释放写再获取读性能要好,因为当前只有一个写锁,可以直接不竞争的降级。而释放写锁,获取读锁的过程就面对着其他读锁请求的竞争,引入额外不必要的开销。

downgrading 只是提供了一个手段,这个手段可以让流程不被中断的降低到低级别锁,并且相对同样满足业务要求的其他手段性能更为良好

https://www.zhihu.com/question/265909728/answer/301363927

https://segmentfault.com/q/1010000009659039

 

StampLock

该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。

读不阻塞写的实现思路:在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!因为在读线程非常多而写线程比较少的情况下写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,因此写线程可能几乎很少被调度成功!当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。

所以读写都存在的情况下,使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!

  StampedLock有三种模式的锁,用于控制读取/写入访问。StampedLock的状态由版本和模式组成。锁获取操作返回一个用于展示和访问锁状态的票据(stamp)变量,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。锁释放以及其他相关方法需要使用邮编(stamps)变量作为参数,如果他们和当前锁状态不符则失败,这三种模式为:
       • 写入:方法writeLock可能为了获取独占访问而阻塞当前线程,返回一个stamp变量,能够在unlockWrite方法中使用从而释放锁。也提供了tryWriteLock。当锁被写模式所占有,没有读或者乐观的读操作能够成功。
       • 读取:方法readLock可能为了获取非独占访问而阻塞当前线程,返回一个stamp变量,能够在unlockRead方法中用于释放锁。也提供了tryReadLock。
       • 乐观读取:方法tryOptimisticRead返回一个非0邮编变量,仅在当前锁没有以写入模式被持有。如果在获得stamp变量之后没有被写模式持有,方法validate将返回true。这种模式可以被看做一种弱版本的读锁,可以被一个写入者在任何时间打断。乐观读取模式仅用于短时间读取操作时经常能够降低竞争和提高吞吐量。

程序举例:

public class Point {
    //一个点的x,y坐标
    private double x, y;
    /**
     * Stamped类似一个时间戳的作用,每次写的时候对其+1来改变被操作对象的Stamped值
     * 这样其它线程读的时候发现目标对象的Stamped改变,则执行重读
     */
    private final StampedLock stampedLock = new StampedLock();
 
    // an exclusively locked method
    void move(doubledeltaX, doubledeltaY) {
        /**stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化
         * 即每次+1,直到加到最大值,然后从0重新开始 */
        long stamp = stampedLock.writeLock(); //写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp);//释放写锁
        }
    }
 
    double distanceFromOrigin() {    // A read-only method
        /**tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写
         * 每次读的时候得到一个当前的stamp值(类似时间戳的作用)*/
        long stamp = stampedLock.tryOptimisticRead();
 
        //这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断
        double currentX = x, currentY = y;
 
        /**如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读,
         * 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁)
         * 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式
         * 读锁又重新返回一个stampe值*/
        if (!stampedLock.validate(stamp)) {
            stamp = stampedLock.readLock(); //读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp);//释放读锁
            }
        }
        //读锁验证成功后才执行计算,即读的时候没有发生写
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}
View Code

==================================================================

Synchronize和ReentrantLock的比较:

功能比较:

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

ReenTrantLock独有的能力:

1.      ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

public class TestReentrantLock2 {
    private static Lock lock = new ReentrantLock(true);  //lock为公平锁

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();

                try {
                    System.out.println("线程1启动...");
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();

                try {
                    System.out.println("线程2启动...");
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();

                try {
                    System.out.println("线程3启动...");
                } finally {
                    lock.unlock();
                }
            }
        });

        t1.start();
        t3.start();
        t2.start();
    }
}
运行结果:
线程1启动...
线程3启动...
线程2启动...
View Code

  在ReentrantLock中的构造函数中,提供了一个参数,指定是否为公平锁。

  公平锁:线程将按照它们发出的请求顺序来获得锁 
  非公锁:当一个线程请求非公平锁的时候,如果发出请求时,获得锁的线程刚好释放锁,则该线程将会获得锁而跳过在该锁上等待的线程。

2.      ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

3.      ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。可以让它中断自己或者在别的线程中中断它,中断后可以放弃等待,去  处理其他事,而不可中断锁不会响应中断,将一直等待,synchronized就是不可中断。

4.      ReenTrantLock的tryLock(long time, TimeUnit unit)起到了定时锁的作用,如果在指定时间内没有获取到锁,将会返回false。应用:具有时间限制的操作时使用 

public class TestReentrantLock {
    public static void main(String[] args) {
        Lock r = new ReentrantLock();

        //线程1
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //获得锁
                r.lock();
                try {
                    System.out.println("线程1获得了锁");
                    //睡眠5秒
                    Thread.currentThread().sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    r.unlock();
                }
            }
        });
        thread1.start();

        //线程2
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    if (r.tryLock(1000, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程2获得了锁");
                    } else {
                        System.out.println("获取锁失败了");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread2.start();
    }
}
View Code

 

什么情况下使用ReenTrantLock:

答案是,如果你需要实现ReenTrantLock的四个独有功能时。因为对于 java.util.concurrent.lock 中的锁定类来说,synchronized 仍然有一些优势。比如,在使用 synchronized 的时候,不可能忘记释放锁;在退出 synchronized 块时,JVM 会为您做这件事。您很容易忘记用 finally 块释放锁,这对程序非常有害。

 

性能比较:

synchronized: 
在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。 

ReentrantLock: 
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。 

Atomic: 
和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。 

所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。 

 

 

posted @ 2018-07-06 16:05  开拖拉机的蜡笔小新  阅读(368)  评论(0编辑  收藏  举报