并发编程(2):AQS
全称:AbstractQueuedSynchronizer
同步组件:CountDownLatch,Semaphore,CyclicBarrier,ReentrantLock,Condition,FutureTask
CountDownLatch
使一个线程等待其他线程完成各自的工作后再执行(5个人开发一个版本,等封板结束可以下班回家,任何一个人没有完成都不能封板,所有人都不能回家)
public class CountDownLatchDemo { public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); CountDownLatch countDownLatch = new CountDownLatch(5); for (int i = 0; i < 5 ;i++) { final int threadCount = i + 1; executorService.execute(() ->{ log.info("I am the No. " + threadCount + ", I arrived"); countDownLatch.countDown(); log.info("I am the No. " + threadCount + ", I get in th bus"); }); Thread.sleep(100); } countDownLatch.await(); log.info("go go go"); executorService.shutdown(); } }
注意:1、CountDownLatch 必须在所有线程开始执行之前初始化
2、countDown方法放到finally里,防止不执行
3、必须在所有线程结束的代码加上await(),不带参数会一直等待,带参数控制等待多久
4、ExecuteService.shutdown并不是立即终止线程,而是所有线程结束后才关闭线程池
CyclicBarrier(同步屏障)
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
@Slf4j public class CyclicBarrierDemo { public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); CyclicBarrier cyclicBarrier = new CyclicBarrier(5); for (int i = 0; i < 5 ;i++) { final int threadCount = i + 1; executorService.execute(() ->{ try { log.info("I am the No. " + threadCount + ", I arrived"); cyclicBarrier.await(); log.info("I am the No. " + threadCount + ", I get in th bus"); } catch (Exception e) { e.printStackTrace(); } }); Thread.sleep(100); } log.info("go go go"); executorService.shutdown(); } }
CyclicBarrier和CountDownLatch的区别:
1、CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
2、CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。
3、CountDownLatch是主线程等待其他线程就位后才可以执行什么事情,子线程该干嘛干嘛;CyclicBarrier是线程相互等待,子线程一个都不准继续走;
Semaphore(信号量)
Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。
private final static int threadCount = 20; public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(3); for (int i = 0; i < threadCount; i++) { final int threadCount = i; exec.execute(() -> { try { semaphore.acquire(); Thread.sleep(1000); System.out.println(threadCount); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } }); } exec.shutdown(); }
注意:1、acquire和release可以传入参数n,表示一次申请或者释放n个信号量
2、new Semaphore(3, true)表示公平信号量,不带true默认是非公平信号量
3、公平信号量表示调用aquire的顺序就是获取许可证的顺序(相当于大家都在等着排队打饭,有三个窗口,突然一个人闯到前面去了,就是非公平,公平就是按先后排队的顺序去打饭)
4、可以使用tryAquire(),返回boolean,表示是否有可用信许可证,没有就不执行。可以带上两个参数表示等待时间,超过这个时间还没有获取到许可证,就不会执行其他线程了。(排队太长,设置个时间,超过这个时间,我就不吃饭了)
===============================================================================================
ReentrantLock(可重入锁)
独有功能,sync没有的功能:1、可以指定公平锁或者非公平锁。2、提供Condition,可以分组唤醒特定的线程。3、可以提供中断等待锁
为什么不放弃sync:1、sync比较简单,不需要手工去释放,性能一致;2、sync可以跟踪死锁,lock不知道哪个线程持有lock
lock和condition的配合使用:主线程创建Lock和Condition,通过lock,newCondition创建condition,condition可以理解为一个信号量,当这个信号量await后,代码不准向下执行,只有别的线程把这个信号量signal后才可以继续执行。
public class LockDemo { private final static int threadCount = 20; private static int sum = 0; public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); Lock lock = new ReentrantLock(); for (int i = 0; i < threadCount; i++) { final int threadCount = i; exec.execute(() -> { try { lock.lock(); System.out.println("thread " + threadCount + " operate!"); System.out.println("sum= " + ++sum); } finally { lock.unlock(); } }); } System.out.println("======finish========"); exec.shutdown(); } }
多线程的深入理解:
1、线程池只是约定一个池子,至于有多少线程会运行和线程池的大小无关。
2、主线程里约定多少个线程去执行任务,主线程没有去new Thread,就不会多线程代码。
3、Runnable的run方法,Callable的calll方法还有Future里面的submit方法,里面的代码是子线程执行的,会多次执行,看到就应该当成子线程代码。
4、JUC里面的AQS组件,都是由主线程new一个组件,子线程去使用这个组件。
===========================================================================
AQS的相关扩展类
Future
一个接口!Future归根到底的作用是,在主线程里得到每个子线程的返回值
public class FutureTaskDemo { private static int sum = 0; public static void main(String[] args) throws Exception { Callable<String> callable = () -> { Thread.sleep(500); return String.valueOf(sum++); }; ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i< 5; i++) { Future<String> future = executorService.submit(callable); String result = future.get(); System.out.println(result); } executorService.shutdown(); } }
FutureTask
一个类,定义来未来任务,可重用,线程未执行前没有结果的!
public class FutureTaskDemo { private static int sum = 0; public static void main(String[] args) throws Exception { Callable<String> callable = () -> { Thread.sleep(500); return String.valueOf(sum++); }; for (int i = 0; i< 5; i++) { FutureTask<String> futureTask = new FutureTask<>(callable); new Thread(futureTask).start(); System.out.println(futureTask.isDone()); String result = futureTask.get(); System.out.println(futureTask.isDone()); System.out.println(result); } } }
Future和FutureTask的不同之处:
1、Future是一个接口,其实现依赖于ExecutorService.submit(callble),定义就是执行;
2、FutureTask是一个类,继承于RunnableFuture,同时继承了Future和Runnable可以很方便的定义和重用,既可以使用ExecutorService.execute(futureRask)执行,也可以使用new Thread(futureTask).start()使用
Future和FutureTask的注意点:
1、每个线程都需要单独定义一个Future或者FutureTask。
2、没有get到结果前,主线程会一直阻塞!
3、FutureTask可以理解为一个任务,一个已经做过的任务,即使再次thread.start,但是不执行
3、如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。
===========================================================================
BlockingQueue(阻塞队列)
相关方法
放入数据:
offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程)
offer(E o, long timeout, TimeUnit unit),可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.
获取数据:
poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
相关实现类:
ArrayBlockingQueue:定长的队列
LinkedBlockingQueue:基于链表的阻塞队列,没有指定大小的情况下,大小为Integer.MAX_VALUE,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
DelayQueue:当其指定的延迟时间到了,才能够从队列中获取到该元素。
PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),其不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
SynchronousQueue:无缓冲的等待队列
                    
                
                
            
        
浙公网安备 33010602011771号