返回 登录
0

java 利用同步工具类控制线程

阅读1424

学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:492139965 我们一起学Java!

前言

参考来源:《java并发编程实战》

同步工具类:根据工具类的自身状态来协调线程的控制流。通过同步工具类,来协调线程之间的行为。

可见性:在多线程环境下,当某个属性被其他线程修改后,其他线程能够立刻看到修改后的信息。典型的是用java内置关键字volatile,volatile变量可以让jvm知道这个变量是对所有线程共享的,jvm会做一定的同步工作,但不保证原子性。

发布和逸出:

发布:发布一个对象指,使对象能够在当前作用域之外的代码使用。举个栗子就是,对象A持有对象C,把C的引用传递到其他地方,或者外界访问A有非私有方法返回C。发布一个对象时要考虑是否有足够理由发布出去,否则可能会引起同步问题。

很明显,同步工具类肯定需要发布给通信双方。

逸出:当某个不应该发布的对象被发布时,叫做逸出。

线程之间通信,就需要把一个通信协议(某个对象)发布给双方,并且对双方来说都是可见的,这样来协调线程之间的行为。当然如果有特别需要,完全可以自己去写同步工具类。

闭锁

闭锁,可以使线程等待某个事件(信号)发生后才执行后续操作。闭锁有一个初始值,当初始值为0的时候闭锁终止,所有被该闭锁阻塞的线程都可以继续执行。假设有个线程A,我们想A启动之后需要等待线程B的信号才能继续执行,这时候我们可以把一个闭锁发布给A,B,A等待闭锁的结束,B来结束闭锁,这样A就能等B的命令才能继续执行。事实上,利用闭锁还可以让主线程等待所有子线程结束。有一点要注意的是,闭锁是一次性的,一旦进入终止状态(count==0),就不能被重置。

CountDownLatch简单用法介绍:

public CountDownLatch(int count); 构造函数,当count为0的时候,await方法不再阻塞。

public void countDown(); 使count减1。

public void await(); 使当前线程阻塞,直到CountDownLatch的count为0。

下面这个例子显示了利用闭锁控制子线程执行和等待所有子线程结束。


 1 public class TestDownLatch {
 2     int count = 5;
 3     //控制子线程执行的闭锁
 4     final CountDownLatch startGate = new CountDownLatch(1);
 5     //让主线程等待所有子线程结束
 6     final CountDownLatch endGate = new CountDownLatch(count);
 7     
 8     //测试线程
 9     class Task extends Thread{
10         
11         int i;
12         
13         public Task(int i) {
14             this.i = i;
15         }
16 
17 
18         @Override
19         public void run() {
20             //等待闭锁的结束
21             try {
22                 //等待主线程的命令
23                 startGate.await();
24                 System.out.println(i);
25                 //告诉主线程我已经做完了
26                 endGate.countDown();
27             } catch (InterruptedException igored) {
28             }
29         }
30         
31     }
32     
33     public void test(){
34         for (int i = 0; i < count; i++) {
35             Task task = new Task(i);
36             task.start();
37         }
38         System.out.println("所有线程已开启");
39         try {
40             Thread.sleep(3000);
41             startGate.countDown();
42             System.out.println("所有子线程开始输出");
43             
44             Thread.sleep(3000);
45             endGate.await();
46             System.out.println("主线程结束");
47         } catch (InterruptedException ignored) {
48         }
49         
50         
51     }
52     
53     public static void main(String[] args) {
54         TestDownLatch testDownLatch = new TestDownLatch();
55         testDownLatch.test();
56     }
57 
58 }

闭锁

运行结果:

图片描述

FutureTask

《java并发编程实战》的翻译者没有翻译这个词,我也只能这么叫它先。FutureTask本身实现Runnable接口和Future接口。

FutureTask相当于一个助手,你把一个任务(有返回结果并且往往比较耗时)交给FutureTask,让它先计算结果,然后你去干别的事,这个助手(FutureTask)会帮你保存结果,等你需要结果时(调用Future#get),如果计算结束了,FutureTask直接把结果给你,如果还没计算完,就让你等待(阻塞),直到计算完了。

FutureTask简单用法介绍:

public FutureTask(Callable callable); 构造方法。参数Callable,相当于一种可生成结果的Runnable,相当于上面的任务(带返回结果),V为结果的类型,实现Callable需要实现call()。

public V get(); 获取Callable返回的结果,如果Callable没执行完,该方法会阻塞当前线程。

public boolean cancel(boolean mayInterruptIfRunning); 取消执行

另外还有isDone(),isCancelled()方法知道FutureTask状态;

以下代码测试FutureTask,先实例化一个FutureTask并自动获取当前时间(一秒的消耗时间),输出当时时间,然后三秒后再去FutureTask拿结果,可以看到两个时间没有三秒间隔,说明FutureTask花费一秒后算好结果就保存起来,等主线程获取结果。


 1 public class TestFutureTask {
 2     
 3     //任务,返回计算时的时间
 4     private final FutureTask<Date> future = new FutureTask<Date>(new Callable<Date>() {
 5         @Override
 6         public Date call() throws Exception {
 7             //1秒后再返回结果
 8             Thread.sleep(1000);
 9             return new Date();
10         }
11         
12     });
13     
14     public TestFutureTask(){
15         new Thread(future).start();
16     }
17     
18     public Date get() throws InterruptedException, ExecutionException{
19         return future.get();
20     }
21     
22     public static void main(String[] args) {
23         try {
24             System.out.println(new Date());
25             TestFutureTask task = new TestFutureTask();
26             System.out.println("等待3秒.............");
27             System.out.println(task.get());
28         } catch (InterruptedException e) {
29             e.printStackTrace();
30         } catch (ExecutionException e) {
31             e.printStackTrace();
32         }
33     }
34     
35 }

FutureTask

运行结果:

图片描述

信号量

计数信号量用来控制同时访问某个特定资源的操作数量。信号量(Semaphore)维护的是一组许可,准确来说,信号量只是维护这组许可的数量。它有一个初始值,根据这个值的大小来决定每次请求许可时的行为,如果用过连接池就很容易明白,连接池里一开始放着一堆连接(Connection),每次从连接池拿出一个Connection,连接池就少一个Connection,每次Connection用完就还给连接池,方便以后的用户使用。这些资源池都拥有一个信号量,来控制资源的出入。同样,使用Semaphore可以将任何一种容器变成有界阻塞容器。

Semaphore简单用法介绍:

public Semaphore(int permits); 构造方法。参数premits为初始化信号量大小,当permits为0,获取(调用acquire方法)资源会阻塞,直到permits>0。

public void acquire() throws InterruptedException; 当permits>0时获取许可,permits会减一,否则阻塞直到permits>0。

public void release(); 释放许可,permits加一。

以下代码使用信号量简单模拟连接池,测试让连接池大小为5,然后开六个线程去获取连接,每个线程持有连接3秒,可以看到第六个线程阻塞住,要等有一个线程释放连接才可以获取到连接


 1 public class Pool {
 2     private final List<Connection> list;
 3     private final Semaphore sem;
 4     
 5     public Pool(int initSize) {
 6         //注意这里
 7         list = Collections.synchronizedList(new LinkedList<Connection>());
 8         for (int i = 0; i < initSize; i++) {
 9             list.add(new Connection());
10         }
11         sem = new Semaphore(initSize);
12     }
13     
14     //获取连接
15     public Connection get() throws InterruptedException{
16         //如果sem当前许可数量为0,阻塞当前线程
17         sem.acquire();
18         return list.remove(0);
19         
20     }
21     
22     //释放连接
23     public void release(Connection resource) throws InterruptedException{
24         sem.release();
25         list.add(resource);
26         
27     }
28     
29     public static void main(String[] args) {
30         final Pool pool = new Pool(5);
31         
32         for (int i = 0; i < 6; i++) {
33             pool.new TestSemaphore(i, pool).start();
34         }
35     }
36     
37     public class TestSemaphore extends Thread{
38         
39         final int i;
40         final Pool pool;
41         
42         public TestSemaphore(int i,Pool pool) {
43             this.i = i;
44             this.pool = pool;
45         }
46 
47         @Override
48         public void run() {
49             try {
50                 Connection conn = pool.get();
51                 System.out.println(i+"成功获取资源并占用三秒!");
52                 Thread.sleep(3000);
53                 pool.release(conn);
54                 System.out.println(i+"已释放资源!!!");
55             } catch (InterruptedException e) {
56                 System.out.println(i+"获取资源失败----");
57             }
58         }
59     }
60 
61 }

信号量

运行结果:

图片描述

特别注意:Pool的构造方法用Collections.synchronizedList方法使保存连接的list变成线程安全,在调用list的方法时会自动加锁,但是如果用迭代器访问list的时候需要手动加锁,这一点在Collections的API文档有特别说明。

栅栏

栅栏跟闭锁很像,先说栅栏的作用——阻塞一组线程直到某个事件发生。闭锁和栅栏的最大区别就是,栅栏可以让所有线程必须同时到达栅栏位置,才能继续执行,闭锁最多做到线程执行到阻塞的位置必须等待闭锁的终止,而无法保证线程等待其他线程的执行。栅栏就像赛马的那个栏(我也不知道叫什么),所有马到了起点,栅栏一开,所有马都开始跑,闭锁是没办法知道所有马都到达栅栏处的。

CyclicBarrier简单用法介绍:

public CyclicBarrier(int parties, Runnable barrierAction); 构造方法。parties指一共有多少个线程,barrierAction就是所有线程通过栅栏的时回调函数。

public int await() throws InterruptedException, BrokenBarrierException; 当前线程等待其他所有线程到达(也是等待栅栏打开)。

CyclicBarrier是指可以循环使用的栅栏,每次栅栏打开后都能重置以便下次使用。但是如果对await的调用超时,或者await阻塞的线程被中断,栅栏被认为是打破,所有阻塞的await调用都抛出BrokenBarrierException。

以下代码用栅栏使所有五个召唤师都到达战场后游戏开始,从开始连接到全军出击的时间差应该是五个召唤师最慢那个,就是LOL时半天进度条也没100的那个。虽然我戒撸很久了= =


 1 public class TestBarrier {
 2     private final CyclicBarrier barrier;
 3     
 4     public TestBarrier() {
 5         this.barrier = new CyclicBarrier(5, new Runnable() {
 6             @Override
 7             public void run() {
 8                 System.out.println(new Date()+"...全军出击!");
 9             }
10         });
11     }
12     
13     public void start(){
14         System.out.println(new Date()+"开始连接-----");
15         new Summoner(1,3).start();
16         new Summoner(2,1).start();
17         new Summoner(3,5).start();
18         new Summoner(4,6).start();
19         new Summoner(5,1).start();
20     }
21 
22     public class Summoner extends Thread{
23 
24         int i;
25         int preSecond;
26         
27         public Summoner(int i,int preSecond) {
28             this.i = i;
29             this.preSecond = preSecond;
30         }
31 
32         @Override
33         public void run() {
34             try {
35                 System.out.println("召唤师"+i+"需要"+preSecond+"秒到达战场");
36                 Thread.sleep(preSecond*1000);
37                 barrier.await();
38                 System.out.println("------------ 召唤师"+i+":"+new Date());
39             } catch (InterruptedException | BrokenBarrierException e) {
40                 e.printStackTrace();
41             }
42         }
43         
44     }
45     
46     public static void main(String[] args) {
47         new TestBarrier().start();
48     }
49 
50 }

栅栏

运行结果:

图片描述

小结

闭锁:当你的线程需要等待一个命令的时候可以用它。

FutureTask:当你有个耗时的过程想利用并行来提高效率,可以用FutureTask。

信号量:当你在多线程环境下想控制有限资源的访问,使一个容器变成有界阻塞容器,可以考虑信号量。

栅栏:当你的多个线程需要共同完成某些步骤才可以继续执行,例如子问题结果合并,可以使用栅栏。

学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:492139965 我们一起学Java!

评论