Java同步工具类

    上一篇文章我们介绍了一些Java平台类库的并发基础构建模块,介绍了一种Java同步工具类--阻塞队列(链接点击此处)。实际上还有一些其他的同步工具类,本文将介绍这些除阻塞队列之外的同步工具类,信号量栅栏闭锁


    一、闭锁  CountDownLatch

    闭锁是一种同步工具类,可以延迟线程的进度直到其达到终止状态。闭锁相当于门闩,闭锁到达结束状态之前,这扇门一直是关闭的,没有任何线程能通过,当到达结束状态时,这扇门会打开并且允许所有线程的通过。这是一次性的门闩,也就是打开后再也无法改变状态再次关闭。这种东西可以保证某些活动直到其他活动都完成才继续执行。

    可以使用在场景比如:

    1、保证必要资源的加载;

    2、确保某个服务只在其依赖的所有服务都启动之后才启动;

    3、某个操作的所有参与者都就绪了在启动之后的操作。

    Java同步工具类中有闭锁的实现,CountDownLatch的文档描述如下:

    CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。这个闭锁内存在一个计数表示需要完成的操作剩余的需要操作的计数,当计数等于0,开始闭锁下面的操作。

    常用方法如下:

  • await(); // 使当前线程在锁存器倒计数至零之前一直等待
  • countDown(); // 递减锁存器的计数,如果计数到达零,则释放所有等待的线程
  • getCount();//获取闭锁中的计数

    是不是觉得还是不是很理解,而且迷迷糊糊觉得和join()方法很像?不用担心,下面我们通过一段代码来解释这个问题:

import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) {
        System.out.println("主线程开始...");
        CountDownLatch cdl = new CountDownLatch(1);
        MyThread myThread = new MyThread(cdl);
        myThread.start();
        try {
            cdl.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程继续运行...");
        try {
            myThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程运行结束...");
    }
}

class MyThread extends Thread {
    private CountDownLatch countDownLatch;

    public MyThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            System.out.println("第一阶段开始...");
            Thread.sleep(3000);
            System.out.println("第一阶段结束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            countDownLatch.countDown();
        }
        System.out.println("第二阶段开始...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第二阶段结束...");

    }
}
    我们创建一个计数为1的闭锁cdl,传入我们的myThread线程。在主线程启动myThread线程,然后使用cdl.await()方法使得在闭锁打开(在CountDownLatch中的计数为0)之前下面的线程操作一直在等待。在MyThread第一阶段结束后,调用门闩的countDown()方法,使得门闩中的计数减1,变成0,使得cdl.await()之后的操作可以继续执行。

    我们在主线程中使用myThread.join()方法,使得主线程等待myThread直到其结束。

    运行结果如下:


    实际上这个例子就体现出了门闩和join()的区别:

    对于闭锁对象cdl,是在某个其他线程的内部操作的结束之后调用cdl.countDown()方法,这个方法最好写在finally块内,保证countDown()方法的执行,否则countDown()执行出现问题会导致死锁。cdl.await()方法则是在此处设立闭锁闭锁的计数不等于0之前,该闭锁之后的操作不能执行。

    而myThread.join()则是在myThread线程完全执行完之前这行代码之后的操作不能执行。

    也就是闭锁是可以在线程没有完全执行完就打开闭锁继续未执行代码的(比如例子中在第一阶段结束就打开闭锁),而join()方法一定要线程执行完毕才能继续未执行的代码(比如例子中第二阶段结束才继续)。


二、FutureTask

    FutureTask也可以被用作闭锁(FutureTask实现了Future接口,实际上使用了多线程设计模式中的Future Pattern,之前写过,链接点击此处)。

    Future.get()的欣慰取决于任务的状态。任务完成该方法会立即返回结果,否则该方法将进入阻塞状态,直到任务完成,然后返回结果或者抛出异常。FutureTask将计算结果从执行计算的线程传递到获取这个结果的线程,而Future的规范确保了这种传递过程能实现结果的安全发布。

    Future是一个接口,下面约定了一个V call() throws Exception方法,Runnable接口则是约定了void run()方法。FutureTask实现了这两个接口,其中Callable的实现需要传入,而run()方法在FutureTask内部实现。我们传入Callable的实现,就可以使用了。通过FutureTask的get()方法就可以阻塞获得call()方法返回的计算结果。

    FutureTask在Executor框架中表示异步任务,我们可以利用这个特点把FutureTask当作闭锁使用。举个例子:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis(); 
        System.out.println("主线程开始...");
        FutureTask<Integer> future = new FutureTask<Integer>(new Task());
        System.out.println("进行Task任务计算的子线程开始...");
        new Thread(future).start();;
        try {
            System.out.println("主线程正在执行自己的任务...");
            Thread.sleep(1000);
            System.out.println("主线程尝试获取Task结果...");
            System.out.println("时间过去"+(System.currentTimeMillis()-startTime));
            System.out.println("主线程获取到结果为:"+future.get());
            System.out.println("时间过去"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("主线程结束");
    }
}
class Task implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        //花3s模拟计算过程
        Thread.sleep(3000);
        //模拟计算结果是1
        return 1;
    }
}
    运行结果如下:


    我们来分析一下代码和运行结果。我们在主线程创建FutureTask实例future,传入Callable接口。启动匿名线程new Thread(future)。然后模拟主线程花费1s的时间执行其他的任务,之后使用future.get()阻塞获取Task的任务执行结果。从尝试future.get()开始,2s之后获取到执行结果,最后主线程结束。

    我们可以看到实际上我们可以使用FutureTask的实例future的get()方法作为一个门闩,在callable接口的call()方法执行完之前future.get()会一直阻塞,下面的代码不会执行。直到该方法获取到了结果,下面代码才会执行,因此也可以当作一个闭锁


三、信号量  Semaphore

    信号量可以用来控制多个线程访问某个有限资源的资源池。在同步工具类的体现为Semaphore,其常用方法为acquire()获取一个许可,使得信号量减少一个许可,release()释放一个许可,而Semaphore中维护着许可的个数。acquire()方法和release()方法是阻塞的,也就是获取不到这个许可或者释放不了这个序列会一直阻塞,直到可以获取/释放,而且Semaphore还提供了tryAcquire()方法和tryRelease()方法,这是无阻塞,获取/释放不了许可则直接返回false。

    为了保证release()方法一定会执行,最好写在finally块中。

    我们使用信号量来模拟实现30个线程从10个数据库连接池中获取一个连接,最后30个线程都存储完毕的过程:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

//模拟数据库存储过程的线程 只有CONNECTION_NUM个数据库 但是有THREAD_NUM个线程
//使用信号量Semaphore来解决这个公用资源有限的问题
class SaveRunnableImpl implements Runnable {
    //线程共享的信号量
    private final Semaphore sem;
    public SaveRunnableImpl(Semaphore sem){
        this.sem = sem;
    }
    @Override
    public void run() {
        try {
            //请求资源 资源个数减一
            sem.acquire();
            try{
                System.out.println(Thread.currentThread()+"存储了数据");
                Thread.sleep(3000);
            }
            finally{
                //释放资源 资源个数加一 最好把release写到finally块中 保证release()一定会执行
                sem.release();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    private static int THREAD_NUM = 30;
    private static int CONNECTION_NUM = 10;
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_NUM);
        Semaphore sem = new Semaphore(CONNECTION_NUM);
        for(int i = 0; i < THREAD_NUM; i++){
            threadPool.execute(new SaveRunnableImpl(sem));
        }
        threadPool.shutdown();
    }
}
    运行结果应该是10个一批10个一批的线程执行了存储操作。


四、栅栏  CyclicBarrier/Exchanger

    前面我们介绍了闭锁,用来启动一组相关的操作,直到这些相关操作做完才可以打开闭锁,使后面的操作能够继续。闭锁是一次性的,一旦进入终止状态就不能再重置。

    栅栏和闭锁比较类似,栅栏能使得一组线程在某个事件全都发生之前使得线程互相等待。这就保证了这一组线程同时到达栅栏处才继续执行。

    栅栏在Java同步工具类的其中一个体现是CyclicBarrier,构造方法有两个:

   

//创建一个新的栅栏,当给定数量的线程都在等待它时,栅栏打开,但是不会执行预定于的动作
CyclicBarrier(int parties);
   
//创建一个新的栅栏,在给定数量的线程等待时,栅栏打开,最后一个进入栅栏的线程执行预定义操作
CyclicBarrier(int parties, Runnable barrierAction);
    常用方法如下:

   

//等待直到parties个数的线程都在等待
int await();

//等待直到parties个数的线程都在等待或者指定过去过去
int await(long timeout, TimeUnit unit);

//将屏障重置为初始化状态(栅栏可重用 和闭锁不同)
void reset();
    如果await()调用超时或者阻塞的线程被中断,那么栅栏就被打破了,所有阻塞的await()调用的地方都会终止并且抛出BrokenBarrierException

    我们下面使用屏障来模拟这么一种场景,三个不同的线程执行各自的任务,执行完后在栅栏处等待,等待结束输出自己的等待时间,再给栅栏进行事件预定义,执行该定义事件:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

class Thread1 extends Thread{
    private final CyclicBarrier barrier;
    public Thread1 (CyclicBarrier barrier){
        this.barrier = barrier;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            //这1s模拟运行Thread1处理时间
            Thread.sleep(1000);
            //只要等待的线程个数没有达到要求 就一直互相等待
            barrier.await();
            System.out.println(Thread.currentThread()+"等待结束,等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

class Thread2 extends Thread{
    private final CyclicBarrier barrier;
    public Thread2 (CyclicBarrier barrier){
        this.barrier = barrier;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            //这2s模拟运行Thread2处理时间
            Thread.sleep(2000);
            //只要等待的线程个数没有达到要求 就一直互相等待
            barrier.await();
            System.out.println(Thread.currentThread()+"等待结束,等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

class Thread3 extends Thread{
    private final CyclicBarrier barrier;
    public Thread3 (CyclicBarrier barrier){
        this.barrier = barrier;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            //这3s模拟运行Thread3处理时间
            Thread.sleep(3000);
            //只要等待的线程个数没有达到要求 就一直互相等待
            barrier.await();
            System.out.println(Thread.currentThread()+"等待结束,等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        //创建栅栏
        CyclicBarrier barrier = new CyclicBarrier(3,new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread()+"全部线程到达");
            }
        });
        new Thread1(barrier).start();
        new Thread2(barrier).start();
        new Thread3(barrier).start();
    }
}
   

    Thread1处理自己的任务花了1s,Thread2处理自己的任务花了2s,Thread3处理自己的任务花了3s,执行完自己的任务每个线程就都在栅栏处等待,输出结果如下:


    也就是虽然各个线程处理自己任务的时间不一样,但是最终等待的时间实际上是取决于最后到达线程的等待时间


    栅栏在Java同步工具包中的体现还有一个Exchanger,是一个双方栅栏,每一个在栅栏处交换数据。当双方执行的操作不对称的时候,Exchanger会很有用。当双方线程都到达栅栏的时候,将双方的数据进行交换,这个Exchanger对象可以使得两个线程生成的对象能够安全地交换。

    这个类只提供了一个空构造函数,提供了两个方法:

    exchange(V x);//交换双方线程生成对象 交换成功或者被中断

    exchange(V x,long timeout, TimeUnit unit);//交换双方线程生成对象 交换成功或者超时抛出超时异常或者被中断

    我们使用这个栅栏类模拟以下场景,两个线程,一个线程沉睡3000ms后交换字符串,一个线程直接交换字符串,互相输出接收到的字符串已经等待时间:

import java.util.concurrent.Exchanger;

class Thread1 extends Thread{
    private Exchanger<String> exchanger;
    private String name;
    public Thread1(String name,Exchanger<String> exchanger){
        super(name);
        this.exchanger = exchanger;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            Thread.sleep(3000);
            System.out.println(Thread.currentThread()+"获取到数据:"+exchanger.exchange("我是Thread1的实例"));
            System.out.println("等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class Thread2 extends Thread{
    private Exchanger<String> exchanger;
    private String name;
    public Thread2(String name,Exchanger<String> exchanger){
        super(name);
        this.exchanger = exchanger;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread()+"获取到数据:"+exchanger.exchange("我是Thread2的实例"));
            System.out.println("等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<String>();
        new Thread1("thread1",exchanger).start();
        new Thread2("thread2",exchanger).start();
    }
}
    运行结果如下:


    也就是交换双方先到栅栏处的会等待后到达栅栏处的,直到交换双方都到达栅栏然后开始交换数据。


    以上就是常用的Java同步工具类--阻塞队列(见文章开头链接)、栅栏、信号量、FutureTask、闭锁。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值