读写锁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));
}
};
Runnable write = new Runnable(){
public void run(){
demo.write(lock, new Random().nextInt());
}
};
for(int i=0; i<10; i++){
new Thread(read).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高并发程序设计第三章