聊聊 JUC 并发包
今天开始我们聊聊 Java 并发工具包中提供的一些工具类,本文主要从并发同步容器和并发集合工具角度入手,简单介绍下相关 API 的用法与部分实现原理,旨在帮助大家更好的使用和理解 JUC 工具类。
在开始今天的内容之前,我们还需要简单回顾下线程、 syncronized 的相关知识。
Java 线程的运行周期中的几种状态, 在 java.lang.Thread 包中有详细定义和说明:
NEW 状态是指线程刚创建, 尚未启动
RUNNABLE 状态是线程正在正常运行中
BLOCKED 阻塞状态
WAITING 等待另一个线程来执行某一特定操作的线程处于这种状态。这里要区分 BLOCKED 和 WATING 的区别, BLOCKED 是在临界点外面等待进入, WATING 是在临界点里面 wait 等待其他线程唤醒(notify)
TIMEDWAITING 这个状态就是有限的(时间限制)的 WAITING
TERMINATED 这个状态下表示 该线程的 run 方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)
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){//高并发 listList<Object> list = new CopyOnWriteArrayList<>();/高并发 setSet<Object> objects = new CopyOnWriteArraySet<>();/高并发 mapMap<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库
关注一下,我写的就更来劲儿啦
聊聊 JUC 并发包的更多相关文章
- JUC并发包与容器类 - 面试题(一网打净,持续更新)
文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 大厂必备 ...
- JUC并发包基本使用
一.简介 传统的Java多线程开发中,wait.notify.synchronized等如果不注意使用的话,很容易引起死锁.脏读问题.Java1.5 版本开始增加 java.util.concurre ...
- JUC并发包学习
1.什么是JUC java.util工具包.包.分类 业务:普通的线程代码 Thread Runable:没有返回值.效率相对于Callable相对较低. 2.线程和进程 进程:一个程序.如:QQ.e ...
- JUC——并发集合类
如果要进行多个数据的保存,无疑首选类集(List.Set.Queue.Map),在类集的学习的时候也知道一个概念:许多集合的子类都具有同步与异步的差别,但是如果真的要在多线程之中去使用这些类,是否真的 ...
- 并发包学习(三)-AbstractQueuedSynchronizer总结
J.U.C学习的第二篇AQS.AQS在Java并发包中的重要性,毋庸置疑,所以单独拿出来理一理.本文参考总结自<Java并发编程的艺术>第五章第二节队列同步器. 什么是AbstractQu ...
- JUC——JUC开发简介(一)
前言 JUC是Java5.0开始提供的一组专门实现多线程并发处理的开发框架,利用JUC开发架构可以有效的解决实际线程项目开发之中出现的死锁.阻塞.资源访问与公平机制. 此笔记主要记录java.util ...
- Java并发包——线程通信
Java并发包——线程通信 摘要:本文主要学习了Java并发包里有关线程通信的一些知识. 部分内容来自以下博客: https://www.cnblogs.com/skywang12345/p/3496 ...
- J.U.C并发包(1)
J.U.C并发包(1) AbstractQueuedSynchronizer AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchr ...
- java架构之路(多线程)JUC并发编程之Semaphore信号量、CountDownLatch、CyclicBarrier栅栏、Executors线程池
上期回顾: 上次博客我们主要说了我们juc并发包下面的ReetrantLock的一些简单使用和底层的原理,是如何实现公平锁.非公平锁的.内部的双向链表到底是什么意思,prev和next到底是什么,为什 ...
随机推荐
- require.js配置路径的用法和css的引入
前端开发在近一两年发展的非常快,JavaScript作为主流的开发语言得到了前所未有的热捧.大量的前端框架出现了,这些框架都在尝试着解决一 些前端开发中的共性问题,但是实现又不尽相同.通常一般的前端加 ...
- 架构之高可用性(HA)集群(Keepalived)
Keepalived简介 Keepalived是Linux下一个轻量级别的高可用解决方案.高可用(High Avalilability,HA),其实两种不同的含义:广义来讲,是指整个系统的高可用行,狭 ...
- Java面向对象(二、继承)
Java 继承 继承的概念 继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类. 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法 ...
- Python_CRC32
#计算文件的CRC32值 import sys import zlib import os.path filename=sys.argv[1] print(filename) if os.path.i ...
- C语言 > 构造素数表
#include <stdio.h> #define NUMBER 1000 int main(){ int isPrime[NUMBER]; ; i < NUMBER; i++){ ...
- mybatis一对一映射配置详解
听说mybatis一对一有三种写法,今天我试了一下. 数据库表准备 为了偷懒,我直接就拿用户权限菜单里的菜单表和菜单与权限的中间表做实现,他们原来是多对多的关系,这边我假设这两张表是一对一. 表 g ...
- linux timerfd系列函数总结
网上关于timerfd的文章很多,在这儿归纳总结一下方便以后使用,顺便贴出一个timerfd配合epoll使用的简单例子 一.timerfd系列函数 timerfd是Linux为用户程序提供的一个定时 ...
- Tiny4412MMU内存管理
MMU是Memory Management Unit的缩写,中文名是内存管理单元,MMU是由ARM芯片中的cp15协处理器管理,它的作用是负责虚拟内存到物理内存的映射 要将虚拟内存映射为物理内存,就要 ...
- 系列博文-Three.js入门指南(张雯莉)-网格 setInterval方法 requestAnimationFrame方法 使用stat.js记录FPS
第6章 动画 在本章之前,所有画面都是静止的,本章将介绍如果使用Three.js进行动态画面的渲染.此外,将会介绍一个Three.js作者写的另外一个库,用来观测每秒帧数(FPS). CSS3动画那么 ...
- 双层嵌套json字符串(即json对象内嵌json数组)解析为Map
之前我层写过一篇文章,介绍了json与map的相互转化,但当时只涉及到单一的json对象或json数组,对json对象内嵌套这json数组的json字符串无法处理,这篇文章主要解决这个问题. 之前的那 ...