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

来源:互联网 发布:新sat阅读 知乎 编辑:程序博客网 时间:2024/05/16 19:57

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


参考文献

java高并发程序设计第三章
浅谈Java中CyclicBarrier的用法
Lock、synchronized和ReadWriteLock的区别和联系
阅读全文
0 0
原创粉丝点击