欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-25 23:38:04
...

要实现并发的同步控制,除了上篇文章中介绍的使用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包还有很多的同步控制工具,由于篇幅有限,需要大家自己去学习了。

相关标签: java 并发