聊聊 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){
//高并发 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库
关注一下,我写的就更来劲儿啦
聊聊 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到底是什么,为什 ...
随机推荐
- 基于Python的数据分析(3):文件和时间
在接下来的章节中,我会重点介绍一下我自己写的基于之前做python数据分析的打包接口文件common_lib,可以认为是专用于python的第三方支持库.common_lib目前包括文件操作.时间操作 ...
- java基础语法3
逻辑运算符 &:与,和有共同的,必须条件都满足才是true 有false就返回false,必须都是true才返回true |:或者,其中有一个满足条件就返回true ^亦或,相同是false, ...
- centos下网络的配置
1.网络模式要进行使用NAT,要连网的话,要配置好设置:网络要进行一下共享到虚拟机 进入vi /etc/sysconfig/network-scripts/ifcfg-eth0 把里面的onboo ...
- SSM-Spring-07:Spring基于注解的di注入
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 注解: 说起注解,哇哦,每个人都或多或少的用到过 像什么@Overried,@Test,@Param等等之前就 ...
- Spring Security OAuth2 Demo -- good
1. 添加依赖授权服务是基于Spring Security的,因此需要在项目中引入两个依赖: <dependency> <groupId>org.springframework ...
- JavaWeb(一)JavaWeb应用的概念
JavaWeb应用的概念 在Sun的Java Servlet规范中,对Java Web应用作了这样定义:"Java Web应用由一组Servlet.HTML页.类.以及其它可以被绑定的资源构 ...
- Andoird 自定义ViewGroup实现竖向引导界面
转载请表明出处:http://write.blog.csdn.net/postedit/23692439 一般进入APP都有欢迎界面,基本都是水平滚动的,今天和大家分享一个垂直滚动的例子. 先来看看效 ...
- HTML5 FormData 方法介绍以及实现文件上传
XMLHttpRequest 是一个浏览器接口,通过它,我们可以使得 Javascript 进行 HTTP (S) 通信.XMLHttpRequest 在现在浏览器中是一种常用的前后台交互数据的方式. ...
- MySQL 8 新特性之降序索引
MySQL 8.0终于支持降序索引了.其实,从语法上,MySQL 4就支持了,但正如官方文档所言,"they are parsed but ignored",实际创建的还是升序索引 ...
- Java 读书笔记 (八) 修饰符
Java语言提供了很多修饰符,主要分为以下两类: 访问修饰符 非访问修饰符 访问控制修饰符 default (即缺省,什么也不写): 在同一包内可见,不使用任何修饰符.使用对象.类.接口.变量.方法. ...