java高并发程序设计总结五:jdk并发包其他同步控制工具类:ReadWriteLock/CountDownLatch/CyclicBarrier/LockSupport

本文深入探讨Java中的并发工具,包括读写锁ReadWriteLock的工作原理及其应用场景,CountDownLatch和CyclicBarrier在多线程间的计数等待机制,以及LockSupport作为线程阻塞工具的便捷使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

读写锁ReadWriteLock
之前的博客介绍了synchronized和重入锁ReentrantLock都可以实现线程同步,这两种方式确实实现了线程同步,保证了同时只能有一个线程能获得锁资源。不过他们有一个缺点:多个线程对数据进行读取操作也是需要进行等待的。而这实际上是没必要的,因为读操作不会对数据造成污染。ReadWriteLock的出现优化了这一个缺点:多个线程读不会阻塞,而读写和写写会进行阻塞。
ReadWriteLock是一个接口,通常我们可以使用它的一个实现类:ReentrantReadWriteLock,该实现类的声明如下
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    ...
}
这是其一部分,通过这里可以看到,它是可以序列化的,并且内部包含了两个私有的final变量:readLock/writeLock。这两个变量分别表示着读锁和写锁,我们可以通过内部提供的相应的方法来获取。
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
使用读写锁的示例code
package org.blog.controller;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @ClassName: ReadWriteLockDemo
 * @Description: 读写锁测试类
 * @author Chengxi
 * @Date: 2017-10-23下午1:00:21
 *
 * 
 */
public class ReadWriteLockDemo {

    public static void main(String[] args){

        //读线程
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable read = new Runnable(){
            public void run(){
                System.out.println(demo.read(readlock));
                //System.out.println(demo.read(lock));
            }
        };

        //写线程
        Runnable write = new Runnable(){
            public void run(){
                demo.write(lock, new Random().nextInt());
            }
        };

        //分别开启10个读线程和写线程
        for(int i=0; i<10; i++){
            new Thread(read).start();
        }
//      for(int i=0; i<10; i++){
//          new Thread(write).start();
//      }
    }


    public static ReentrantLock lock = new ReentrantLock();
    public static ReentrantReadWriteLock readwritelock = new ReentrantReadWriteLock();
    public static Lock readlock = readwritelock.readLock();
    public static Lock writelock = readwritelock.writeLock();
    private int value = 0;

    public int read(Lock lock){
        lock.lock();
        try{
            Thread.sleep(1000);
            return value;
        }
        catch(Exception e){
            e.printStackTrace();
            return -1;
        }
        finally{
            lock.unlock();
        }
    }

    public void write(Lock lock, int value){
        lock.lock();
        try{
            Thread.sleep(2000);
            this.value = value;
        }
        catch(Exception e){
            e.printStackTrace();
        }
        finally{
            lock.unlock();
        }
    }
}
在这里可以值测试读操作就行了,因为写操作和重入锁是一样都,都需要进行等待。在读操作那里,我们使用重入锁lock会发现每次都只有一个线程进行读,因此最终完成的时间为10秒;而使用读写所readlock来进行读时,我们会发现最终只需要一秒。这就是他们之间的性能差别(ReadWriteLock适用于读多写少的场景)


倒计时器:CountDownLatch
CountDownLatch是用来指定当前只能同时有几个线程处于执行状态,待这几个线程全部执行完成之后才能继续往下执行,期间会一直处于等待状态。CountDownLatch只有一个构造器,需要传入一个int变量,表示这个计时器的计数个数:
public CountDownLatch(int count){
    if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
}
如果count小于0则会抛出异常
它内部主要提供了两个方法:await和countDown,他们的签名如下:
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
调用await使当前线程处于等待状态,响应中断

public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
表示等待指定时间,当时间到了计时器个数还是不为0则返回false,否则返回true并继续执行

public void countDown() {
    sync.releaseShared(1);
}
调用countDown表示当前计数器减1
这两个方法是搭配使用的,计时器的执行步骤为:首先new一个计时器,并指定计时个数;然后在当前线程中创建多线程环境,同时调用await使当前线程处于等待状态,当countDown调用次数等于计时个数时,则当前线程等待完毕,继续执行下去。CountDownLatch内部维护这一个计数器,初始化为Math.min(当前执行线程个数,count),当调用await进行等待时,每次调用countDown计数器都会自减1,当计数器的值为0时,结束等待
CountDownLatch测试code
package org.blog.controller;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @ClassName: CountDownLatchDemo
 * @Description: 倒计时器:CountDownLatch
 * @author Chengxi
 * @Date: 2017-10-23下午1:16:09
 *
 * 
 */
public class CountDownLatchDemo {

    public static CountDownLatch latch = new CountDownLatch(10);
    public static void main(String[] args) throws InterruptedException {

        Thread test = new Thread(){
            public void run(){
                try{
                    Thread.sleep(new Random().nextInt(10)*1000);
                    System.out.println("check complete");
                    latch.countDown();
                }
                catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        };

        ExecutorService exec = Executors.newFixedThreadPool(12);
        for(int i=0; i<12; i++){
            exec.submit(test);
        }

        latch.await();
        System.out.println("fire!");
        exec.shutdown();
    }
}
上面的代码运行结果为:先执行线程池中的前面十个线程,然后计时器释放当前线程,main主线程继续执行,然后线程池里的2个线程执行;(这里需要说明的是在计时器释放之后,main主线程和线程池里面的两个剩余线程是多线程执行的,不过因为main中不需要sleep等待,而线程池中的需要等待随机事件,所以大部分都是先执行main,在执行两个线程)。如果将CountDownLatch中的计数器个数设置为13,则程序会一直等待,不会打印fire,因为计时器永远不会为0,一直为1


循环栅栏:CyclicBarrier
CyclicBarrier和CountDownLatch一样,都可以用来实现线程间的计数等待;不过他的功能要比CountDownLatch强大。它的两个构造器如下:
public CyclicBarrier(int parties) {
    this(parties, null);
}
用于创建一个CyclicBarrier对象并指定等待个数(这里没有指定runnable,
则当等待数达到了parties数量时,不执行任何回调操作)

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
用于创建一个CyclicBarrier对象并指定等待个数,并且当等待数到了parties时
启动barrierAction线程,在parties个等待线程被唤醒前执行
循环栅栏可以多次计数,相比CountDownLatch的await/countDown来说,这里仅仅使用await方法,每次调用就会计数一次线程等待数,当数值等于parties时执行创建时的回调线程,然后启动所有等待线程(类似于一种分批次排队,人数够了就开始)。await方法签名如下:
public int await() throws InterruptedException, BrokenBarrierException 
该方法响应中断,在等待时被中断会抛出中断异常,而他也可能抛出BrokenBarrierException异常,表示当前的CyclicBarrier已经破损了,可能系统没有办法等待所有线程到齐了。比如我们在await的过程中中断一个线程,而cyclicBarrier必须要等他们所有线程await,所以这时候是没有办法等到所有的了,因此就会抛出一个InterruptedException和九个BrokenBarrierException
循环栅栏使用场景实例:比如在一次任务中,需要先等10个士兵到齐,再一起去执行任务,然后还要等所有士兵一起回来报道任务完成。这里可以使用循环栅栏进行计数10次等待并等待两次,实现代码为:
package org.blog.controller;

import java.util.Random;
import java.util.concurrent.CyclicBarrier;

/**
 * @ClassName: CyclicBarrierDemo
 * @Description: 循环栅栏测试类 CyclicBarrier
 * @author Chengxi
 * @Date: 2017-10-23下午3:03:26
 *
 * 
 */
public class CyclicBarrierDemo {

    public static class Soldier implements Runnable{

        private String soldier;
        private CyclicBarrier barrier;
        public Soldier(CyclicBarrier barrier, String soldier){
            this.barrier = barrier;
            this.soldier = soldier;
        }

        public void run(){
            try{
                //等待十个士兵到齐
                barrier.await();
                doWork();
                //等待十个士兵完成任务
                barrier.await();
            }
            catch(Exception e){
                e.printStackTrace();
                System.out.println("soldier run");
            }
        }

        //全都到齐之后每个人执行任务
        public void doWork(){
            try{
                Thread.sleep(new Random().nextInt(10)*100);
            }
            catch(Exception e){
                e.printStackTrace();
                System.out.println("dowork");
            }
            System.out.println(soldier+" 完成任务");
        }
    }

    //每次等待完成之后的一次回调
    public static class CallRun implements Runnable{

        boolean flag;
        int N;
        public CallRun(boolean flag, int n){
            this.flag = flag;
            this.N = n;
        }

        public void run(){
            if(flag){
                System.out.println("司令:【士兵"+N+"个任务完成】");
            }
            else{
                System.out.println("司令:【士兵"+N+"个集合完毕】");
                flag = true;
            }
        }
    }

    public static void main(String[] args) {

        final int N = 10;
        Thread[] allsoldiers = new Thread[N];
        boolean flag = false;
        CyclicBarrier barrier = new CyclicBarrier(N,new CallRun(flag,N));
        System.out.println("集合队伍");
        for(int i=0; i<N; i++){
            System.out.println("士兵"+i+"来报道");
            allsoldiers[i] = new Thread(new Soldier(barrier,"士兵"+i));
            allsoldiers[i].start();
        }
    }

}

输出结果:
集合队伍
士兵0来报道
士兵1来报道
士兵2来报道
士兵3来报道
士兵4来报道
士兵5来报道
士兵6来报道
士兵7来报道
士兵8来报道
士兵9来报道
司令:【士兵10个集合完毕】
士兵6 完成任务
士兵7 完成任务
士兵1 完成任务
士兵9 完成任务
士兵3 完成任务
士兵5 完成任务
士兵2 完成任务
士兵0 完成任务
士兵8 完成任务
士兵4 完成任务
司令:【士兵10个任务完成】


线程阻塞工具类:LockSupport
LockSupport是一个非常方便使用的线程阻塞工具,它可以在线程内的任意位置让线程阻塞,它弥补了suspend执行在resume之后导致线程无法继续执行的情况、同时也不需要先获得对象锁,也不会抛出中断异常;
使用LockSupport阻塞线程不需要获得对象的锁,也不需要new对象,因为其阻塞park和继续执行unpark的方法都是静态成员,他们的方法签名如下:
public static void park(Object blocker) ;
使当前线程阻塞,直到调用对应的unpark释放;期间不会影响其他线程的执行

public static void unpark(Thread thread);
释放指定线程,使其继续运行
前面说:park/unpark弥补了resume/suspend必须按顺序执行的缺陷,这是因为park和unpark的内部实现原理造成的。对于LockSupport来说,它会为每一个线程初始化一个许可,如果当前线程的许可可用,那么调用park就会使用该许可并立即返回,同时设置该线程的许可为不可用状态,而对于LockSupport来说, 如果当前线程的许可不可用,那么线程就会进入阻塞状态。而unpark方法就会将指定线程的许可变为可用状态,所以最终线程是否阻塞是看该线程的许可是否可用,所以它们之间的先后顺序是没有影响的(不过这里要注意的是,LockSupport的许可和信号量不同,许可不能累加,每个线程最多只能拥有一个可用许可)
测试代码:
package org.blog.controller;

import java.util.concurrent.locks.LockSupport;

/**
 * @ClassName: LockSupportDemo
 * @Description: 线程阻塞工具类测试: LockSupport
 * @author Chengxi
 * @Date: 2017-10-23下午5:24:41
 *
 * 
 */
public class LockSupportDemo {

    public static lsthread ls1 = new lsthread("ls1");
    public static lsthread ls2 = new lsthread("ls2");

    public static class lsthread extends Thread{

        public lsthread(String name){
            super(name);
        }

        public void run(){
            synchronized(this){
                System.out.println("now is->"+getName());
                LockSupport.park();
            }
        }
    }

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

        ls1.start();
        Thread.sleep(1000);
        ls2.start();
        LockSupport.unpark(ls1);
        LockSupport.unpark(ls2);
        ls1.join();
        ls2.join();
    }

}

输出结果:
now is->ls1
now is->ls2
从main函数中来看,park和unpark两个方法的执行顺序是不确定的,不过最终的输出结果总会是一样的
不过这里需要注意的是park方法不会抛出InterruptedException,他只是会默默的返回,我们只能够通过Thread.interrupted()等方法来获得中断标记


参考文献
java高并发程序设计第三章
浅谈Java中CyclicBarrier的用法
Lock、synchronized和ReadWriteLock的区别和联系
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值