聊聊 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到底是什么,为什 ...
随机推荐
- HTML编码和CSS编码会遇到的问
http://codeguide.bootcss.com/#html-syntax 参考链接 属性顺序 HTML 属性应当按照以下给出的顺序依次排列,确保代码的易读性. class id, name ...
- PHP session有效期session.gc_maxlifetime详解
一个已知管用的方法是,使用session_set_save_handler,接管所有的session管理工作,一般是把session信息存储到数据库,这样可以通过SQL语句来删除所有过期的sessio ...
- 使用xshell链接虚拟机的方法
给大家介绍一下虚拟机和Xshell5连接的基本配置1.安装虚拟机,跟着提示一步一步安装即可,注意添加镜像文件,虚拟机就完成了.2.下载一个Xshell5,安装好之后.要修改虚拟机的网卡状态 1) ...
- 关于HTML5中的sessionStorage和localStorage
需求: 做项目的时大多数情况下我们需要对请求的数据进行多次复用,为了降低请求次数我们需要对请求的数据进行本地存储: 以前用的cooking来存储为本地数据,HTML5后提出sessioStorage. ...
- Scrapy爬虫框架第三讲(linux环境)
下面我们来学习下Spider的具体使用: 我们已上节的百度阅读爬虫为例来进行分析: 1 # -*- coding: utf-8 -*- 2 import scrapy 3 from scrapy.li ...
- 【多线程】-ThreadPool线程池
ThreadPool介绍: Thread类是一个静态类,所有不存在实例化构造函数操作,直接可以调用其内所存在的方法. 微软官网给出的解释: 提供一个线程池,该线程池可用于执行任务.发送工作项.处理异步 ...
- ELK 架构之 Logstash 和 Filebeat 安装配置
上一篇:ELK 架构之 Elasticsearch 和 Kibana 安装配置 阅读目录: 1. 环境准备 2. 安装 Logstash 3. 配置 Logstash 4. Logstash 采集的日 ...
- 【Service Fabric】小白入门记录 本地Service Fabric集群安装及设置
本篇内容是自学自记,现在我还不知道Service Fabric究竟是怎么个入门法,反正按照入门教程先进行本地Service Fabric集群的安装,万里路始于足下,要学习总得先把环境装好了才能开始学习 ...
- 3GPP 测试 /etc/udev/ruse.d/50文件 /lib/udev/ruse.d/55* 网络配置
3GPP是个标准化协议组织,其工作目标是制定协议实现由2G网络到3G网络的平滑过渡,保证未来技术的后向兼容性,支持轻松建网及系统间的漫游和兼容性. 3GPP协议的制订主要是以GSM核心网为基础. 测试 ...
- 开启irqbalance提升服务器性能
操作系统 性能调休 公司有次压测存在一个问题:CPU资源压不上去,一直在40%已达到了性能瓶颈,后定位到原因,所在的服务器在压测过程中产生的中断都落在CPU0上处理,这种中断并没有均衡到各个CPU ...