今天开始咱们聊聊 Java 并发工具包中提供的一些工具类,本文主要从并发同步容器和并发集合工具角度入手,简单介绍下相关 API 的用法与部分实现原理,旨在帮助你们更好的使用和理解 JUC 工具类。java
在开始今天的内容以前,咱们还须要简单回顾下线程、 syncronized 的相关知识。git
Java 线程的运行周期中的几种状态, 在 java.lang.Thread 包中有详细定义和说明:github
NEW 状态是指线程刚建立, 还没有启动数组
RUNNABLE 状态是线程正在正常运行中缓存
BLOCKED 阻塞状态 安全
WAITING 等待另外一个线程来执行某一特定操做的线程处于这种状态。这里要区分 BLOCKED 和 WATING 的区别, BLOCKED 是在临界点外面等待进入, WATING 是在临界点里面 wait 等待其余线程唤醒(notify)多线程
TIMEDWAITING 这个状态就是有限的(时间限制)的 WAITING并发
TERMINATED 这个状态下表示 该线程的 run 方法已经执行完毕了, 基本上就等于死亡了(当时若是线程被持久持有, 可能不会被回收)dom
synchronized 实现同步的基础:Java 中的每个对象均可以做为锁。高并发
具体表现为如下 3 种形式:
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的 Class 对象。
对于同步方法块,锁是 synchronized 括号里配置的对象。当一个线程试图访问同步代码块时,它首先必须获得锁,退出或抛出异常时必须释放锁。
那么同步方法(syncronized ) 与 静态同步方法(static syncronized ) 的有什么区别呢? 咱们来看一个简单的例子:
class Phone { public /*static*/ synchronized void sendEmail() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("--------sendEmail"); } public /*static*/ synchronized void getMessage() { System.out.println("--------getMessage"); } public void getHello() { System.out.println("--------getHello");}main{ Phone p = new Phone(); p.sendEmail(); p.getMessage(); p.getHello();}}
经过以上代码回答下面问题:
标准访问的时候,请问先打印邮件仍是短信?
sendEmail方法暂停4秒钟,请问先打印邮件仍是短信?
新增Hello广泛方法,请问先打印邮件仍是Hello?
两部手机,请问先打印邮件仍是短信?
两个静态同步方法,同1部手机 ,请问先打印邮件仍是短信?
两个静态同步方法,有2部手机 ,请问先打印邮件仍是短信?
1个静态同步方法,1个普通同步方法,有1部手机 ,请问先打印邮件仍是短信?
1个静态同步方法,1个普通同步方法,有2部手机 ,请问先打印邮件仍是短信?
思考一下,咱们再作分析~
一个对象里面若是有多个 synchronized 方法,某一个时刻内,只要一个线程去调用 其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有惟一一个线程去访问这些 synchronized 方法;
全部的非静态同步方法用的都是同一把锁——实例对象自己,synchronized 方法锁的是当前对象 this,被锁定后,其它的线程都不能进入到当前对象的其它 synchronized 方法,也就是说若是一个实例对象的非静态同步方法获取锁后,该实例对象的其余非静态同步方法必须等待获取锁的方法释放锁后才能获取锁;
加个普通方法后发现和同步锁无关;
换成两个对象后,不是同一把锁了,毋须等待互不影响。
由于别的实例对象的非静态同步方法跟该实例对象的非静态同步方法用的是不一样的锁,因此毋须等待;
全部的静态同步方法用的是同一把锁——类对象自己(锁的是类模板),一旦一个静态同步方法获取锁后,其余的静态同步方法都必须等待该方法释放锁后才能获取锁,而不论是同一个实例对象的静态同步方法之间,仍是不一样的实例对象的静态同步方法之间,只要它们是同一个类模板的实例对象就要争取同一把锁;
第1和第5中的这两把锁是两个不一样的对象,因此静态同步方法与非静态同步方法之间是不会有竞态条件的。
通过分析,答案也就否则而喻了。
简单回顾以后,回到正文,JUC 中提供了比 synchronized 更加高级的同步结构,包括 CountDownLatch,CyclicBarrier,Semaphone 等能够实现更加丰富的多线程操做。
另外还提供了各类线程安全的容器 ConcurrentHashMap、有序的 ConcurrentSkipListMap,CopyOnWriteArrayList 等。
CountDownLatch (计数器)
让一些线程阻塞直到另外一些线程完成一系列操做后才被唤醒。
CountDownLatch 主要有 countDown、await 两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞。其它线程调用 countDown 方法会将计数器减 1 (调用 countDown 方法的线程不会阻塞),当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行。
代码案例:图书馆下班 ,等读者所有离开后,图书管理员才能关闭图书馆。
main 主线程必需要等前面线程完成所有工做后,本身才能执行。
public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(5);//参数表明读者的数量 for (int i = 1; i <= 5 ; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t 号读者离开了图书馆"); countDownLatch.countDown(); } ,CountryEnum.getKey(i).getName()).start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"\t ------图书管理员闭馆");}}结果以下:3 号读者离开了图书馆2 号读者离开了图书馆4 号读者离开了图书馆1 号读者离开了图书馆5 号读者离开了图书馆main ------图书管理员闭馆
CyclicBarrier (循环屏障)
CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。
它要作的事情是,让一组线程到达一个屏障(也能够叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,全部被屏障拦截的线程才会继续干活。
线程进入屏障经过 CyclicBarrier 的 await() 方法。
代码案例:集齐10张卡牌才能够召开奖
public class CyclicBarrierDemo {private static final int NUMBER = 10;public static void main(String[] args){ //构造方法 CyclicBarrier(int parties,Runnable action) CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Thread(() -> { System.out.println("集齐卡牌 开始开奖"); })); for (int i = 1; i <= NUMBER ; i++) { final int tempInt = i; new Thread(() -> { try { System.out.println(Thread.currentThread().getName()+"\t 收集了"+tempInt+"号卡牌"); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } } ,String.valueOf(i)).start(); }}}结果以下:1 收集了1号卡牌8 收集了8号卡牌4 收集了4号卡牌3 收集了3号卡牌5 收集了5号卡牌7 收集了7号卡牌9 收集了9号卡牌6 收集了6号卡牌2 收集了2号卡牌 10 收集了10号卡牌集齐卡牌 开始开奖
Semaphone (信号量)
信号量典型应用场景是多个线程抢多个资源。
在信号量上咱们定义两种操做:
acquire(获取): 当一个线程调用 acquire 操做时,它要么经过成功获取信号量(信号量减 1 ),要么一直等下去,直到有线程释放信号量,或超时。
release(释放):实际上会将信号量的值加 1,而后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另外一个用于并发线程数的控制。
代码案例:停车场停车 ,车抢车位
public class SemaphoreDemo {public static void main(String[] args){ Semaphore semaphore = new Semaphore(3);// 模拟 3 个停车位 for (int i = 1; i <= 6 ; i++) {//6 辆车 new Thread(() -> { try{ semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"\t 抢到停车位"); TimeUnit.SECONDS.sleep(new Random().nextInt(5)); System.out.println(Thread.currentThread().getName()+"\t 离开停车位"); }catch(Exception e){ e.printStackTrace(); }finally{ semaphore.release(); } } ,String.valueOf(i)).start(); }}}结果以下:2 抢到停车位4 抢到停车位1 抢到停车位2 离开停车位6 抢到停车位6 离开停车位5 抢到停车位4 离开停车位1 离开停车位3 抢到停车位3 离开停车位5 离开停车位
接下来,咱们来梳理下并发包里提供的线程安全的集合类,基本代码以下:
public class NotSafeDemo { public static void main(String[] args){ //高并发 list List<Object> list = new CopyOnWriteArrayList<>(); /高并发 set Set<Object> objects = new CopyOnWriteArraySet<>(); /高并发 map Map<String,String> map = new ConcurrentHashMap<String,String>(); for (int i = 0; i < 50 ; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,6)); System.out.println(list); } ,String.valueOf(i)).start(); } } }
CopyOnWrite 容器也被称为写时复制的容器。
往一个容器添加元素的时候,不直接往当前容器 Object[] 添加,而是先将当前容器 Object[] 进行 Copy,复制出一个新的容器 Object[] newElements,而后新的容器 Object[] newElements 里添加元素,添加完元素以后,再将原容器的引用指向新的容器 setArray(newElements)。
这样作的好处是能够对 CopyOnWrite 容器进行并发的读,而不须要加锁,由于当前容器不会添加任何元素。因此 CopyOnWrite 容器也是一种读写分离的思想,读和写不一样的容器,可是因为经过对底层数组复制来实现的,通常须要很大的开销。当遍历次数大大超过修改次数的时,这种方法比其余替代方法更有效。部分源码以下:
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1);//先复制,再添加一个空元素 newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
而带有 Concurrent 的通常才是真正的适用并发的工具,ConcurrentHashMap 被认为是弱一致性的,本质缘由在于 ConcurrentHashMap 在读数据是并无加锁。
关于并发集合的应用还要在实际开发中多多体会,实践才是最好的老师。
扩展知识:
今天的扩展知识简单介绍下 Java 经常使用的 4 种线程池:
newCachedThreadPool
建立可缓存的线程,底层是依靠 SynchronousQueue 实现的,建立线程数量几乎没有限制(最大为 Integer.MAX_VALUE)。
若是长时间没有往线程池提交任务,即若是工做线程空闲了指定时间(默认1分钟),该工做线程自动终止。终止后若是又有了新的任务,则会建立新的线程。
在使用 CachedTreadPool 时,要注意控制任务数量,不然因为大量线程同时运行,颇有可能形成系统瘫痪。
newFixedThreadPool
建立指定数量的工做线程,底层是依靠 LinkedBlockingQueue 实现的,没提交一个任务就建立一个工做线程,当工做线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
在线程空闲时,不会释放工做线程,还会占用必定的系统资源。
newSingleThreadExecutor
建立单线程,底层是 LinkedBlockingQueue 实现的,它只会用一个工做线程来执行任务,保证全部的任务按指定顺序执行。若是这个线程异常结束,会有另外一个取代它,保证顺序执行。
最大的特色是可保证顺序地执行各个任务,并在任意时间是不会有过个线程活动的。
newScheduleThreadPool
建立一个定长的线程池,支持定时以及周期性的任务调度。
参考资料:
https://github.com/fanpengyi/java-util-concurrent.git ---- 文中代码git库
关注一下,我写的就更来劲儿啦