JAVA并发编程(三)——同步控制(下)

要实现并发的同步控制,除了上篇文章中介绍的使用synchronizedReentrantLock以外,JDK中内部提供了许多使用的API和框架。这里我将介绍其中几种常用的。

  • Semaphore(信号量)
  • ReadWriteLock(读写锁)
  • CountDownLatch(倒计时器)
  • CyclicBarrier(循环栅栏)

Semaphore

我们知道,不论是synchronized还是ReentrantLock,一次都只允许一个线程访问共享的资源。而Semaphore可以指定某个数量的线程同时访问一个资源。就好像一个厕所,有多个坑位,就可以使得多个人同时使用,人数超过了,就需要等待。
Semaphore提供了两个常用的构造函数,以及常用的方法如下:

//创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits);
// 创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair);
//获得使用资源的许可
void acquire();
//释放获得的许可
void release(); 

为了更好的学习Semaphore,我写了一个小的demo,模拟多个人同时上多个厕所。

class Toilet implements Runnable{
    private Semaphore semaphore;
    private int id;
    public Toilet(Semaphore semaphore,int id){this.semaphore = semaphore;this.id=id;}
    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println("编号:"+id+"在上厕所时间为:"+System.currentTimeMillis()/1000);
            //模拟上厕所
            TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10000));
            System.out.println("编号:"+id+"厕所上完了");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class TestSemaphore {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(2);
        for (int i = 1; i <= 10; i++) {
            exec.execute(new Toilet(semaphore, i));
        }
        TimeUnit.SECONDS.sleep(30);
        exec.shutdown();
        System.out.println("厕所都上完了");
    }
}
//output:
/*
编号:2在上厕所时间为:1507810796
编号:1在上厕所时间为:1507810796
编号:1厕所上完了
编号:4在上厕所时间为:1507810797
编号:4厕所上完了
编号:3在上厕所时间为:1507810798
编号:2厕所上完了
编号:6在上厕所时间为:1507810799
编号:3厕所上完了
编号:7在上厕所时间为:1507810804
编号:6厕所上完了
编号:5在上厕所时间为:1507810808
编号:5厕所上完了
编号:8在上厕所时间为:1507810810
编号:7厕所上完了
编号:9在上厕所时间为:1507810812
编号:9厕所上完了
编号:10在上厕所时间为:1507810814
编号:10厕所上完了
编号:8厕所上完了
厕所都上完了
*/

这里我模拟了10个人上两个厕所的过程。只要在需要同步的业务逻辑上加上semaphore.acquire();semaphore.release();即可。非常简单实用。


ReadWriteLock(读写锁)

我们知道,在程序中使用锁是非常消耗性能的,所以为了提高性能,需要减少锁的使用。要知道,我们对一个资源访问,但是不去修改而是简单的读取数据,在这种情况下如果加锁,是完全没有必要的。所以对于一些经常用来读取但是几乎不修改的变量,我们需要重新考虑,使得多个线程读取的时候不需要锁,仅仅在写入的时候加锁。这时候,就需要用到ReadWriteLock。具体用法如下:

public class ReadWriteLockDemo {

    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException{
        try{
            lock.lock();
            TimeUnit.SECONDS.sleep(3);
            System.out.println("reading"+"读取时间为:"+System.currentTimeMillis()/1000);
            TimeUnit.SECONDS.sleep(1);
            return value;
        }finally{
            lock.unlock();
        }
    }
    public void handleWrite(Lock lock,int index) throws InterruptedException{
        try{
            lock.lock();
        System.out.println("write 数据为:"+index+"写入时间为:"+System.currentTimeMillis()/1000);
            TimeUnit.SECONDS.sleep(3);
            value = index;
        }finally{
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        long currentTimeMillis = System.currentTimeMillis();
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.handleRead(readLock);
                    //demo.handleRead(lock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable writeRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.handleWrite(writeLock, new Random().nextInt());
                    //demo.handleWrite(lock, new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 8; i++) {
            new Thread(readRunnable).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(writeRunnable).start();
        }
    }
}
/*
使用readWriteLock输出:
reading读取时间为:1507811783
reading读取时间为:1507811783
reading读取时间为:1507811783
reading读取时间为:1507811783
reading读取时间为:1507811783
reading读取时间为:1507811783
reading读取时间为:1507811783
reading读取时间为:1507811783
write 数据为:-1205341170写入时间为:1507811784
write 数据为:2021909351写入时间为:1507811787
*/
/*
使用ReentrantLock输出如下:
reading读取时间为:1507811880
reading读取时间为:1507811884
reading读取时间为:1507811888
reading读取时间为:1507811892
reading读取时间为:1507811896
reading读取时间为:1507811900
reading读取时间为:1507811904
reading读取时间为:1507811908
write 数据为:273063536写入时间为:1507811909
write 数据为:-708027034写入时间为:1507811912
*/

可以看到,使用readWriteLock在读取的时候不阻塞,在写入数据的时候需要同步。性能相比较ReentrantLock高了许多。


CountDownLatch(倒计时器)

CountDownLatch翻译为倒计时插销,其实就是一个倒计时工具,就像火箭发射前指挥员喊得:“5,4,3,2,1,发射!”。(我还记得2003年读小学时,电视上看着神州5号飞船发射,那一个激动啊。。。扯远了)。在并发中,他的作用就是计算还有多少剩下的线程需要执行,执行一个线程,计数器减一,直到为零,然后通知目标线程开始执行。
CountDownLatch的构造器接收一个整数位参数:

public CountDownLatch(int count);//count为执行的个数

老样子,给一个例子能更好的学习:

class Work extends Thread{
    private int thredId;
    private CountDownLatch latch;
    public Work(int id,CountDownLatch latch){
        this.latch = latch;
        this.thredId = id;
    }
    public void run(){
        dowork();
        //执行完一个任务,count减一
        latch.countDown();
    }
    public void dowork(){
        System.out.println(this+" is doing");
    }
    public String toString(){return "Thread:"+thredId ;}
}

class MainWork extends Thread{
    private CountDownLatch latch;
    public MainWork(CountDownLatch latch){this.latch = latch;}
    public void run(){
        try {
            System.out.println("准备主任务中");
            //主任务在这里等待小任务完成
            latch.await();
            System.out.println("开始执行主要任务");
        } catch (InterruptedException e) {          
            e.printStackTrace();
        }
    }
}
public class CountDownLatchDemo {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(10);
        ExecutorService executorService = Executors.newCachedThreadPool();
        //开始主任务线程
        executorService.execute(new MainWork(latch));
        //开始10个小任务线程
        for (int i = 1; i <= 10; i++) {
            executorService.execute(new Work(i, latch));
        }
        executorService.shutdown();
    }
}
/*
output:
准备主任务中
Thread:2 is doing
Thread:6 is doing
Thread:10 is doing
Thread:4 is doing
Thread:8 is doing
Thread:3 is doing
Thread:7 is doing
Thread:1 is doing
Thread:5 is doing
Thread:9 is doing
开始执行主要任务
*/

可以看到,我们构造了一个计数器数量为10的CountDownLatch当10个小任务执行完成后,MainWork继续执行。


CyclicBarrier(循环栅栏)

CyclicBarrier和CountDownLatch非常类似,也实现线程之间的计数等待的功能,但是他的功能更加复杂且强大。用学生集合去做操形容CyclicBarrier是比较好的:学生们在教室前集合,先到的同学需要等待,直到所有的同学都到期,这是,大家一起执行出操的任务。如果熟悉赛车比赛的同学也可以理解为比赛前的发车。
CyclicBarrier的构造函数接收一个count和一个task任务,当计数器计数完成以后,执行task任务,task执行完成以后,计数器中的任务线程还可以再次执行的

CyclicBarrier(int count, Runnable task);

这里我就用我喜爱的F1(赛车)发车用来模拟(对了我是从04年中国赛开始看F1的,F1是属于比较小众的运动吧,关注也不高,但不知道为什么就这么坚持下来了):

public class F1 {

    public static class Car implements Runnable {
        private int carId;
        private final CyclicBarrier barrier;

        public Car(int carId, CyclicBarrier barrier) {
            this.barrier = barrier;
            this.carId = carId;
        }

        @Override
        public void run() {
            try {
                System.out.println(carId+"号车准备发车");
                TimeUnit.MILLISECONDS.sleep(2000);
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(carId+"号车起步了");
        }
    }
    //控制台,发出同步指令的,相当于信号灯
    //当22个小车线程执行到wait时,执行Controll的task
    public static class Controll implements Runnable{
        @Override
        public void run() {
            System.out.println("所有车都准备好了,信号灯倒计时");
            for(int i=3;i>=0;i--){
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println((i==0? "发车":i));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(22, new Controll());
        System.out.println("马来西亚大奖赛正赛开始了");
        TimeUnit.SECONDS.sleep(3);
        //F1一共有22辆赛车
        for(int i =1;i<23;i++){
            new Thread(new Car(i, barrier)).start();;
        }
    }
}
/*
output:
马来西亚大奖赛正赛开始了
1号车准备发车
2号车准备发车
。。。。。。
。。。。。。
21号车准备发车
22号车准备发车
所有车都准备好了,信号灯倒计时
3
2
1
发车
22号车起步了
1号车起步了
。。。。。。
。。。。。。
9号车起步了
21号车起步了
*/

从输出可以知道,当22个赛车线程都执行到wait()时候(准备发车),一个目标控制台线程开始执行(倒计时3,2,1),目标线程执行完成以后,22个赛车线程继续执行接下未完成的任务(发车,启动车),这和CountDownLatch是不同的。


JDK中的java.util.concurrent包还有很多的同步控制工具,由于篇幅有限,需要大家自己去学习了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值