JUC
求甚解
图片在后续补充
1、ConcurrentHashMap实现原理
参考博客1:ConcurrentHashMap底层实现原理(JDK1.7 & 1.8) - 简书 (jianshu.com)
参考博客2:HashMap 线程安全问题 - CSDN
参考博客3:concurrentHashMap原理分析和总结(JDK1.8)- 博客园 (cnblogs.com)
参考博客4:解析为什么hashmap是线程不安全的
1、HashMap并发安全问题
由于在多线程情况下,HashMap会出现线程安全问题,因为倒叙链表插入的情况,导致HashMap在resize()
的情况下,链表出现环的出现。一旦出现了环,那么在while(null != e){}
的循环的时候,就可能会出现死循环。(参考博客4、2)
HashMap resize操作
for (Entry<K,V> e : table) { while(null != e) { 1、当前结点不为null Entry<K,V> next = e.next; 2、获取 e.next,赋值给next暂存 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); // 计算e的哈希值 } int i = indexFor(e.hash, newCapacity); // 计算 e在新Entry[]中的位置i e.next = newTable[i]; 3、让e.next指向newTable[i]的头结点 newTable[i] = e; 4、让e作为newTable[i]的头结点。这里是倒叙插入,新的结点作为头结点,头插法 e = next; 5、让next作为oldTable[j]的头结点 } }
HashMap正常情况下的resize
HashMap多线程resize,出现环(紫色是线程A,绿色是线程B)
Hashtable,和Collections.synchronizedMap(new HashMap<>()),都在put()
方法外加了同步监视器,因此效率低
jdk1.5之后,引入了ConcurrentHashMap,jdk1.8又对其进行了优化
2、JDK1.7的实现
1、数据结构
在JDK1.7及之前,ConcurrentHashMap的数据结构:一个Segment数组 和 多个HashEntry
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据结构一样
2、初始化
CHM的初始化是会通过 位与 运算来初始化Segment的大小,用size来表示,如下所示:
int size = 1;
while(size < concurrencyLevel){
++a;
size <<= 1;
}
如上所示,因为size用位于运算来计算(size <<=1),所以Segment的大小取值都是以2的N次方,无关concurrencyLevel的取值,当然concurrencyLevel最大只能用16位的二进制来表示,即65536,换句话说,Segment的大小最多65536个,没有指定concurrencyLevel元素初始化,Segment的大小size默认为16。
每一个Segment元素下的HashEntry的初始化也是按照位于运算来计算,用cap来表示,如下所示:
int cap = 1;
while(cap < c){
cap <<= 1;
}
如上所示,HashEntry大小的计算也是2的N次方(cap <<=1), cap的初始值为1,所以HashEntry最小的容量为2
3、put操作
对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
从上Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒
流程图示:
4、get操作
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null
5、size操作
计算ConcurrentHashMap的元素大小是一个有趣的问题,因为他是并发操作的,就是在你计算size的时候,他还在并发的插入数据,可能会导致你计算出来的size和你实际的size有相差(在你return size的时候,插入了多个数据),要解决这个问题,JDK1.7版本用两种方案:
- 使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的
- 如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回
3、JDK1.8的实现
0、数据结构
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
Node
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,部分源代码如下
/*
键值输入。 此类永远不会导出为用户可变的Map.Entry(即一种支持setValue;请参见下面的MapEntry),但可以用于批量任务中使用的只读遍历。 具有负哈希字段的Node的子类是特殊的,并且包含空键和值(但从不导出)。 否则,键和值永远不会为空。
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
...
}
TreeNode
TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,他就是通过TreeNode作为存储结构代替Node来转换成黑红树,部分源代码如下
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
...
}
TreeBin
TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制,部分源码结构如下
/*
bin顶部使用的TreeNodes。 TreeBins不保存用户键或值,而是指向TreeNode及其根的列表。 它们还维护一个寄生的读写锁,迫使编写者(谁拥有bin锁)等待读者(谁没有bin)在完成树重组操作之前完成操作。
*/
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
...
}
1、put操作
源代码
public V put(K key, V value) {
return putVal(key, value, false);
}
/**
* Implementation for put and putIfAbsent
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
int hash = spread(key.hashCode()); // 两次hash,减少hash冲突,可以均匀分布
int binCount = 0;
for (Node<K, V>[] tab = table; ; ) { // 对这个table进行迭代,死循环,出口见下面的分析
Node<K, V> f; // f是桶的头结点
int n, i, fh; // fh是头结点的哈希值
// 这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
if (tab == null || (n = tab.length) == 0)
// 初始化结束之后继续迭代,因为这里是死循环
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果table[i]没有数据(头结点),就直接无锁插入
// 这里使用了CAS,避免使用锁。如果CAS失败,说明该节点已经发生改变,
// 可能被其他线程插入了,那么继续执行死循环,在链尾插入。
if (casTabAt(tab, i, null,
new Node<K, V>(hash, key, value, null)))
// 可能的出口一!!!!!!
break; // no lock when adding to empty bin
} else if ((fh = f.hash) == MOVED) // static final int MOVED = -1
// 如果其它线程在对table进行扩容,则先进行扩容操作,帮助扩容
// 这里监测到的的条件是目标桶被设置成了FORWORD。如果桶没有设置为
// FORWORD节点,即使正在扩容,该线程也无感知。
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
// 这里请求了synchronized锁。这里要注意,不会出现桶正在resize的过程中执行插入,因为桶resize的时候也请求了synchronized锁。即如果该桶正在resize,这里会发生锁等待
synchronized (f) {
// 表示该节点是链表结构
if (tabAt(tab, i) == f) {
// fh>=0:头结点的哈希值大于等于0,表示是一个用户节点,非Forwarding等节点
if (fh >= 0) {
binCount = 1;
for (Node<K, V> e = f; ; ++binCount) {
K ek;
//这里涉及到相同的key进行put就会覆盖原先的value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
// 可能的出口二!!!!!!
break;
}
Node<K, V> pred = e;
if ((e = e.next) == null) { //插入链表尾部
pred.next = new Node<K, V>(hash, key, value, null);
// 可能的出口三!!!!!!
break;
}
}
} else if (f instanceof TreeBin) { //红黑树结构
Node<K, V> p;
binCount = 2;
//红黑树结构旋转插入
if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) { // 如果链表长度(碰撞次数)超过8,将链表转化为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
// 可能的出口四!!!!!!
break;
}
}
}
addCount(1L, binCount); //统计size,并且检查是否需要扩容
return null;
}
概述
- 如果没有初始化就先调用initTable()方法来进行初始化过程
- 如果没有hash冲突就直接CAS插入
- 如果还在进行扩容操作就先进行扩容
- 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
- 最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
流程图示:
2、get操作
根据key的hash值定位,遍历链表或者红黑树,获取节点。
具体一点:
- 根据key的hash值定位到桶位置。
- map是否初始化,没有初始化则返回null。否则进入3
- 定位到的桶位置是否有头结点,没有返回nul,否则进入4
- 是否有其他线程在扩容,有的话调用find方法查找。所以这里可以看出,扩容操作和get操作不冲突,扩容map的同时可以get操作。
- 若没有其他线程在扩容,则遍历桶对应的链表或者红黑树,使用equals方法进行比较。key相同则返回value,不存在则返回null.
源代码
// 不用担心get的过程中发生resize,get可能遇到两种情况:
// 1.桶未resize(无论是没达到阈值还是resize已经开始但是还未处理该桶),遍历链表
// 2.在桶的链表遍历的过程中resize,resize并未破坏原tab的桶的节点关系,遍历仍可以继续
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
3、扩容操作
扩容触发条件
- 链表转换为红黑树时(此时链表节点个数至少达到8个),如果转换时map长度小于64则直接扩容一倍,不转化为红黑树。如果此时map长度大于64,则不会扩容,直接进行链表转红黑树的操作。
- map中总节点数大于阈值(即大于map长度的0.75倍)时会进行扩容。
如何扩容
- 创建一个新的map,是原先map的两倍。注意此过程是单线程创建的
- 复制旧的map到新的map中。注意此过程是多线程并发完成。(将map按照线程数量平均划分成多个相等区域,每个线程负责一块区域的复制任务)
具体过程
参考博客4:深入分析ConcurrentHashMap1.8的扩容实现 - 简书 (jianshu.com)
待我打怪升级,手撕源码
1、JUC
这三个包下的内容,即为JUC
package
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
2、进程&线程
1、进程/线程 是什么
- 程序:为完成特定任务、用某种语言编写的一组指令的集合
- 进程:一个程序的一次执行过程。有其生命周期。进程是操作系统分配资源的单位
- 线程:一个进程可以有多个线程(一对多关系),线程是程序内部的一条执行路径。
在Java中,每个线程都与操作系统线程直接映射
2、进程/线程 的例子
-
查看任务管理器
-
jvisualvm查看
public static void main(String[] args) throws InterruptedException { TimeUnit.HOURS.sleep(1); }
E:\InteliiJMavenProjects\JustJVM\out\production\JustJVM>jvisualvm E:\InteliiJMavenProjects\JustJVM\out\production\JustJVM> The launcher has determined that the parent process has a console and will reuse it for its own console output. Closing the console will result in termination of the running program. Use '--console suppress' to suppress console output. Use '--console new' to create a separate console window.
3、线程状态
创建、就绪、运行、阻塞、死亡
4、wait/sleep 的区别
同:都会造成 阻塞
异:
- wait - 属于Object类 - 由同步监视器对象调用 - 会释放同步资源 - 只能在同步监视器的作用范围内使用
- sleep - 属于Thread类 - 由线程对象调用 - 不会释放同步资源 - 在任何范围内都可以使用
5、什么是并发?什么是并行
并发:一个CPU同时执行多个线程
并行:多个CPU同时执行多个线程
6、Thread的start()和run()
参考博客:https://blog.csdn.net/sihai12345/article/details/80458763
start()
-
start()方法来启动线程,真正实现了多线程运行
-
线程不是马上执行的,而是使线程从新建–>就绪状态,等待CPU调度才会变为运行状态,start()的顺序不能决定线程的执行顺序,这由CPU调度决定
-
线程的状态从新建–>就绪只会发生一次,一个线程只能调用一次start()
测试两次调用start()
public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("good"); }); thread.start(); thread.start(); }
输出
Exception in thread "main" good java.lang.IllegalThreadStateException
run()
-
run方法与start方法关联:run()其实是一个普通方法,只不过当线程调用了start()后,一旦线程被CPU调度,处于运行状态,那么线程才会去调用run()
-
不需要线程调用start()后才可以调用的。线程对象可以随时随地调用run方法
-
run()方法不能实现并发
// 二、使用Thread.run() for (int i = 0; i < 10; i++) { final int j = i; new Thread(() -> { System.out.println(j); }).start(); // run() } // start() -- 0~9 随机输出 // run() ---- 0~9 顺序输出 // 一、使用Runnable.run() for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { final int tmp = j; Runnable runnable = new Runnable() { @Override public void run() { System.out.println(tmp); } }; runnable.run(); } } // 0~9 顺序输出
测试:
-
run()
-
start()
总结
- start方法是用于启动线程的,可以实现并发
- run方法只是一个普通方法,是不能实现并发的,只是在并发执行的时候会调用
3、Lock锁
读写锁后面介绍
使用模板
Lock l = ...; // 通常使用的是ReentrantLock
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
ReentrantLock
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- 公平锁:十分公平:可以先来后到
- 非公平锁:十分不公平:可以插队 (默认)
4、生产者和消费者问题
参考博客:一篇文章,让你彻底弄懂生产者–消费者问题 - 简书 (jianshu.com)
1、理解
生产者-消费者模式是一个经典的多线程并发协作的模式,弄懂生产者-消费者问题,可以加深对并发编程的理解。
所谓生产者-消费者问题,实际上包含了两类线程,一种是用于生产数据的线程-使用者线程,另一种是用于消费/使用数据的线程-消费者线程。为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享区域中去获取数据,不需要关系生产者的行为。
但是,这个共享数据区域中应该具备如下 线程间并发协作的功能:
- 如果共享数据区已满的话,阻塞生产者继续生产数据放入共享数据区
- 如果共享数据区为空的话,阻塞消费者继续消费数据
在实现生产者-消费者问题时,可以采用三种方式:
- 使用Object类的 wait/notify 的消息通知机制
- 使用Lock类的Condition的 await/signal 的消息通知机制
- 使用BlockingQueue实现(重点)
接下来我们使用代码进行实现
2、实现
a、使用Object类的 wait/notify 的消息通知机制
代码测试:
/**
* @author zedh
* @date 2020/12/7-11:14
* <p>
* 使用Object的 wait 和 notify 实现生产者和消费者
*/
public class Demo_Object {
public static void main(String[] args) {
BreadHome home = new BreadHome();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
home.increment();
} catch (Exception e) {
e.printStackTrace();
}
}, "A" + String.valueOf(i)).start();
}
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
home.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}, "B" + String.valueOf(i)).start();
}
}
}
// 面包商店 - 模拟 数据共享区
// 为了方便,注意这里始终保持只生产一个面包
class BreadHome {
private int num; // 面包数量
// 制作面包 +1
public synchronized void increment() throws Exception {
while (num != 0) { // 此处原为 if (num != 0)
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + " -- bread's number = " + num);
this.notifyAll();
}
// 售卖面包 -1
public synchronized void decrement() throws Exception {
while (num == 0) { // 此处原为 if (num == 0)
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + " -- bread's number = " + num);
this.notifyAll();
}
// =====================================================================
// 注意在decrement()和increment()中使用的if会造成虚假唤醒
// 解决方法:将if改为while
}
虚假唤醒
一个消费者线程抢到执行权,发现product是0,就等待,这个时候,另一个消费者又抢到了执行权,product是0,还是等待,此时两个消费者线程在同一处等待。然后当生产者生产了一个product后,就会唤醒两个消费者,发现product是1,同时消费,结果就出现了0和-1。这就是虚假唤醒。解决办法就是把if判断改成while。
错误结果分析:if条件下,线程被唤醒后,会从 wait() 处开始继续往下执行;
b、使用Lock类的Condition的 await/signal 的消息通知机制
官方文档对于Condition的介绍
参考博客:Java并发之Condition - 数月亮 - 博客园 (cnblogs.com)
Condition的使用说明:
- Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式
- 使用Condition之前必须获取锁
- 一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现
- 调用await方法后,将当前线程加入Condition等待队列中。当前线程释放锁。否则别的线程就无法拿到锁而发生死锁。自旋(while)挂起,不断检测节点是否在同步队列中了,如果是则尝试获取锁,否则挂起。当线程被signal方法唤醒(将会唤醒在等待队列中等待最长时间的节点,即条件队列里的首节点),被唤醒的线程将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。
代码测试:
/**
* @author zedh
* @date 2020/12/7-11:33
*/
public class Demo2_Lock_Condition {
public static void main(String[] args) {
BreadHome2 home = new BreadHome2();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
home.increment();
} catch (Exception e) {
e.printStackTrace();
}
}, "A" + String.valueOf(i)).start();
}
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
home.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}, "B" + String.valueOf(i)).start();
}
}
}
class BreadHome2 {
private ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private int num; // 面包数量
// 制作面包 +1
public void increment() throws Exception {
lock.lock();
try {
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + " -- bread's number = " + num);
condition.signalAll();
} finally {
lock.unlock();
}
}
// 售卖面包 -1
public void decrement() throws Exception {
lock.lock();
try {
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + " -- bread's number = " + num);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
思考:如何让a、b、c三个线程按次序执行
思路:
-
用一个ReentrantLock对象创建三个Condition实例
private Lock lock = new ReentrantLock(); private int num = 1; private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition();
-
执行当前方法,唤醒下一个condition
public void printA() { // printB() lock.lock(); try { while (num != 1) { // while (num != 2) { condition1.await(); // condition2.await(); } // } System.out.println(Thread.currentThread().getName() + " => A"); // => B num = 2; // num = 3; condition2.signal(); // 1执行完,让2唤醒 // condition3.signal(); // 2执行完,让3唤醒 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }
c、使用BlockingQueue实现
ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足矣
这里使用ArrayBlockingQueue实现
main
public class Demo3_BlockingQueue {
public static void main(String[] args) {
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(2);
// 5个消费者,1个生产者
for (int i = 0; i < 5; i++) {
new Thread(new Consumer(queue), "Thread-Consumer-" + i).start();
}
new Thread(new Producer(queue), "Thread-Producer").start();
}
}
productor
class Producer implements Runnable {
private BlockingQueue queue;
public Producer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
int cur = (int) (Math.random() * 100);
queue.put(cur);
System.out.println(Thread.currentThread().getName() + " 生产:no." + cur + ", 现有队列元素" + queue.size() +
"个");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
consumer
class Consumer implements Runnable {
private BlockingQueue queue;
private static ReentrantLock lock = new ReentrantLock();
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
System.out.println(Thread.currentThread().getName() + " 消费:no." + queue.take() + ", 剩余队列元素" + queue.size() + "个");
} catch (Exception e) {
System.out.println(e);
}
}
}
}
部分输出(1次sleep)
Thread-Producer 生产:no.90, 现有队列元素1个
Thread-Consumer-0 消费:no.90, 剩余队列元素0个
Thread-Producer 生产:no.24, 现有队列元素1个
Thread-Consumer-1 消费:no.24, 剩余队列元素0个
Thread-Producer 生产:no.14, 现有队列元素1个
Thread-Consumer-2 消费:no.14, 剩余队列元素0个
Thread-Producer 生产:no.87, 现有队列元素1个
Thread-Consumer-3 消费:no.87, 剩余队列元素0个
Thread-Producer 生产:no.83, 现有队列元素1个
Thread-Consumer-4 消费:no.83, 剩余队列元素0个
Thread-Producer 生产:no.20, 现有队列元素1个
Thread-Consumer-0 消费:no.20, 剩余队列元素0个
Thread-Producer 生产:no.91, 现有队列元素1个
Thread-Consumer-1 消费:no.91, 剩余队列元素0个
Thread-Producer 生产:no.34, 现有队列元素1个
Thread-Consumer-2 消费:no.34, 剩余队列元素0个
5、八锁现象
参考博客: JUC并发编程(五)-8锁的现象_二总的猫-CSDN博客
代码参考博客
1、多个线程使用同一把锁——顺序执行
多个线程使用同一个对象,多个线程就是使用一把锁,先调用的先执行
示例1、标准访问,请问先打印邮件还是短信?
public class MultiThreadUseOneLock01 {
public static void main(String[] args){
Mobile mobile = new Mobile();
// 两个线程使用的是同一个对象。两个线程是一把锁!先调用的先执行!
new Thread(()->mobile.sendEmail(),"A").start();
// 干扰
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile.sendMS(),"B").start();
}
}
// 手机,发短信,发邮件
class Mobile {
// 被 synchronized 修饰的方法、锁的对象是方法的调用者、
public synchronized void sendEmail() {
System.out.println("sendEmail");
}
public synchronized void sendMS() {
System.out.println("sendMS");
}
}
2、多个线程使用同一把锁,其中某个线程里面还有阻塞——顺序先执行
多个线程使用同一个对象,多个线程就是使用一把锁,先调用的先执行,即使在某方法中设置了阻塞
示例2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
3、多个线程有锁与没锁——随机执行
多个线程,有的线程有锁,有的线程没锁,两者之间不存在竞争同一把锁的情况,先后执行顺序是随机的
示例3、新增一个普通方法getWeixinMs(),请问先打印邮件还是接收微信?
4、多个线程使用多把锁——随机执行
1、被 synchronized 修饰的方法,锁的对象是方法的调用者;
2、调用者不同,它们之间用的不是同一个锁,相互之间没有关系。注意:synchronized 修饰的静态方法 ——类锁、非静态方法——对象锁
示例4、两部手机、请问先打印邮件还是短信?
5、Class锁:多个线程使用一个对象——顺序执行
被 synchronized 和 static 同时修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁。线程之间是顺序执行。
锁Class和锁对象的区别:
- Class 锁 ,类模版,只有一个;
- 对象锁 , 通过类模板可以new 多个对象。
如果全部都锁了Class,那么这个类下的所有对象都具有同一把锁。
示例5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?
6、Class锁:多个线程使用多个对象——顺序执行
被 synchronized 修饰 和 static 同时修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁。
Class锁是唯一的,所以多个对象使用的也是同一个Class锁。
示例6、两个静态同步方法,2部手机,请问先打印邮件还是短信?
7、Class锁与对象锁:多个线程使用一个对象——随机执行
被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;
只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。
示例7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
8、Class锁与对象锁:多个线程使用多个对象——随机执行
被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;
只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。
示例8、一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?
总结
- new this 本身的这个对象,调用者
- static class 类模板,保证唯一!
6、不安全的集合类
List
public class ListTest {
public static void main(String[] args) {
// java.util.ConcurrentModificationException 并发修改异常
// List<String> list = new ArrayList<>();
// 解决方式一:
// 缺点:learn.help_class_3()底层使用synchronized,效率低
// List<String> list = new Vector<>();
// 解决方式二:
// 底层使用同步代码块,效率比前者略高
// List<String> list = Collections.synchronizedList(new ArrayList<>());
// 解决方式三
// 底层使用了ReentrantLock锁,效率比前两个方案都高
/*
CopyOnWrite 写入时复制,cow是计算机程序设计领域的一种优化策略;
读写分离 {。。。Arrays.copyOf(elements, len + 1)。。。}
*/
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
Set
public class SetTest {
public static void main(String[] args) {
// java.util.ConcurrentModificationException
// Set<String> set = new HashSet<>();
// 解决方案一:
/*
mutex = this;
public boolean learn.help_class_3(E e) {
synchronized (mutex) {return c.learn.help_class_3(e);}
}
*/
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
// 解决方案二:
/*
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
learn.help_class_3()使用的CopyOnWriteArrayList中的方法addIfAbsent()
*/
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
Map
public class MapTest {
public static void main(String[] args) {
// java.util.ConcurrentModificationException
// Map<String,String> map = new HashMap<>();
// 解决方案一:
// public synchronized V put(K key, V value) { 效率较低
// Map<String,String> map = new Hashtable<>();
// 解决方案二:ConcurrentHashMap原理分析见`0、求甚解`
// Map<String,String> map = new ConcurrentHashMap<>(20);
// 解决方案三:
// 使用了synchronized的put():synchronized (mutex) {return m.put(key, value);},效率仍然很低
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
for (int i = 0; i < 20; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,4));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
7、Callable
使用方式
FutureTask
Thread
代码示例
public class Test {
public static void main(String[] args) {
A a = new A();
FutureTask<Object> futureTask = new FutureTask<Object>(a); // 适配类
new Thread(futureTask, "p").start();
new Thread(futureTask, "q").start(); // 结果会被缓存,提升效率
try {
System.out.println(futureTask.get()); // get()可能会产生阻塞
} catch (Exception e) {
e.printStackTrace();
}
}
}
class A implements Callable {
@Override
public Object call() throws Exception {
System.out.println("call()");
return 12;
}
}
/*
输出:
call()
12
*/
8、三个常用辅助类
CountDownLatch
参考博客:https://www.jianshu.com/p/e233bb37d2e6
latch:门闩
1.背景:
- CountDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue。
- 存在于java.util.cucurrent包下。
2.概念
- CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
- 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了
这就好像学生放寒假离开宿舍,一个宿舍5个人,其中1~4个人离开,房间都不能贴封条,只有所有人都离开,才能贴
3.源码
- CountDownLatch类中只提供了一个构造器:
//参数count为计数值
public CountDownLatch(int count) { };
- 类中有三个方法是最重要的:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
4.示例
public class CountDownLatchTest {
public static void main(String[] args) throws Exception {
System.out.println("open the door");
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "--- go out");
latch.countDown(); // 数值 -1,表示还需要执行的线程数-1
}).start();
}
latch.await(); // 等待计数器归0,然后再向下执行
// ---------------------
System.out.println("close the door");
}
}
/*
输出如下:
open the door
Thread-0 --- go out
Thread-2 --- go out
Thread-1 --- go out
Thread-3 --- go out
Thread-4 --- go out
close the door
*/
debug的线程运行情况:
CyclicBarrier
参考博客:https://www.jianshu.com/p/333fd8faa56e
1. CyclicBarrier 是什么?
- 从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
- 它的作用就是会让所有线程都等待完成(就绪)后才会继续下一步行动。CyclicBarrier 类有一个整数初始值,此值表示将在同一点同步的线程数量。当其中一个线程到达确定点,它会调用await() 方法来等待其他线程。当线程调用这个方法,CyclicBarrier阻塞线程进入休眠直到其他线程到达。当最后一个线程调用CyclicBarrier 类的await() 方法,它唤醒所有等待的线程并继续执行它们的任务
举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。
2. 怎么使用 CyclicBarrier
2.1 构造方法
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
解析:
-
parties 是参与线程的个数,需要循环栅栏结束等待,可以设置线程数量为parties的正整数倍,否则会一直等待
-
第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达的线程要做的任务。当全部线程都到达同一个点时,CyclicBarrier类 会把这个对象当做线程来执行
private int dowait(boolean timed, long nanos){ // ... try { final Runnable command = barrierCommand; if (command != null) command.run(); // 执行 // ... } // ... }
2.2 重要方法
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
public int getParties() // 返回克服此障碍所需的线程数目
解析:
- 线程调用 await() 表示自己已经到达栅栏
- BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
2.3 示例
public class CyclicBarrierDemo {
public static void main(String[] args) {
System.out.println("准备上课,要5个人都到齐才能开始");
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("人员到齐");
});
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 已到");
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, "学生" + i).start();
}
}
}
3. CyclicBarrier 使用场景
可以用于多线程计算数据,最后合并计算结果的场景。
4. CyclicBarrier 与 CountDownLatch 区别
- CountDownLatch是线程组之间的等待,即一个(或多个)线程等待N个线程完成某件事情之后再执行;而CyclicBarrier则是线程组内的等待,即每个线程相互等待,即N个线程都被拦截之后,然后依次执行。
- CountDownLatch是减计数方式,而CyclicBarrier是加计数方式。
- CountDownLatch计数为0无法重置,而CyclicBarrier计数达到初始值,则可以重置。
- CountDownLatch不可以复用,而CyclicBarrier可以复用。
5.“循环栅栏” 体现演示——使用jvisualvm监控运行情况
代码
在2.3示例for循环之后加上等待
try {
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
i%parties==0时:如i=5
可以看到循环栅栏预定线程数执行完,只有main线程处于休眠状态
i%parties!=0时:如i=8
可以看到,循环栅栏没有失效,还有3个线程处于驻留状态,main线程处于休眠状态
Semaphore(信号量)
参考博客:https://www.jianshu.com/p/ec637f835e08
1. Semaphore 是什么?
-
Semaphore 是一个计数信号量,必须由获取它的线程释放。
-
常用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。
Semaphore 只有3个操作:
- 初始化
- 获取:如果满了则等待
- 释放:释放&信号量+1、唤醒等待线程
// permits the initial number of permits available
public Semaphore(int permits);
// fair = true:这个信号量将保证在争用下 先出先出 授予许可证
public Semaphore(int permits, boolean fair);
// Acquires a permit from this semaphore, blocking until one is available, or the thread is interrupted
void acquire();
// Releases a permit, returning it to the semaphore.
void release();
// 返回此信号量中当前可用的许可数
int availablePermits();
// 查询是否有线程正在等待获取
boolean hasQueuedThreads();
2. 示例
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 车辆准备停靠
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " -- 车辆已停靠");
TimeUnit.SECONDS.sleep(5);
semaphore.release();
System.out.println(Thread.currentThread().getName() + " -- 车辆已离开");
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
jvusualvm
总结
CountDownLatch:减法计数器,n减到0,才能放其它线程执行。比如宿舍放寒假贴封条,所有人走了才能贴
CyclicBarrier:可重用的加法计数器,0加到n,所有线程到达await()之前,都不能执行。比如聚餐,所有人到齐才能吃
Semaphore:限制线程的数量为n。比如停车场限制停车的数量
9、读写锁(ReadWriteLock)
1、继承结构
2、理解
我们在读数据的时候,可以多个线程同时读,不会出现问题,但是写数据的时候,如果多个线程同时写数据,那么到底是写入哪个线程的数据呢?所以,如果有两个线程,写写/读写需要互斥,读读不需要互斥。这个时候可以用读写锁
3、示例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author zedh
* @date 2020/12/8-17:57
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
B b = new B();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
b.read();
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(() -> {
b.write();
}).start();
}
}
}
class B {
private volatile int num;
ReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
lock.readLock().lock();
try {
System.out.println("read()" + num);
} finally {
lock.readLock().unlock();
}
}
public void write() {
lock.writeLock().lock();
try {
System.out.println("write()");
num++;
} finally {
lock.writeLock().unlock();
}
}
}
10、BlockingQueue
参考博客:BlockingQueue(阻塞队列)详解 - 一步一个小脚印 - 博客园 (cnblogs.com)
1、入门
1、什么是阻塞队列
队列,顾名思义,FIFO
阻塞,从两个方面说明:
-
当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列
-
当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒
2、继承结构
2、常用实现类讲解
详细内容参照博客
1、ArrayBlockingQueue
部分源代码
// 数据结构:数组
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
// 构造器,ReentrantLock默认使用公平锁
public ArrayBlockingQueue(int capacity, boolean fair) {
// ...
this.items = new Object[capacity];
// ...
}
/** Main lock guarding all access */
final ReentrantLock lock;
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock; // 即上面的lock
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
public E poll() {
final ReentrantLock lock = this.lock; // 和offer一样相同的锁对象,成员变量lock
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
2、LinkedBlockingQueue
部分源代码
// 三个的其中一个构造器,指定队列大小,另外两个构造器都是Integer.MAX_VALUE,生猛
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
// 数据结构:单链表
// 链表头,队首,队首出队列,将下一个结点作为head
transient Node<E> head;
// 链表尾,队尾,后插入的元素为队尾,插入的部分源代码:last = last.next = node; //node为插入的结点
private transient Node<E> last;
// Node
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 不同于ArrayBlockingQueue,LinkedBlockingQueue有两个可重入锁,分别用于入队列和出队列,来控制数据同步
/** Lock held by take, poll, etc 消费者端用的锁*/
private final ReentrantLock takeLock = new ReentrantLock();
/** Lock held by put, offer, etc 生产者端用的锁*/
private final ReentrantLock putLock = new ReentrantLock();
3、DelayQueue
4、PriorityBlockingQueue
5、SynchronousQueue
3、BlockingQueue的方法总结
抛出异常 | 有返回值,不抛异常 | 阻塞,等待 | 超时等待 | |
---|---|---|---|---|
Insert | add(e) | offer(e) | put(e) | offer(e, time, unit) |
Remove | remove() | poll() | take() | poll(time, unit) |
检查队首元素 | element() | peek() | not applicable | not applicable |
int size():检查队列大小
4、代码示例
offer(e) & poll()
public class BQDemo {
public static void main(String[] args) {
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<Object>(2);
System.out.println(queue.offer(12));
System.out.println(queue.offer(22));
System.out.println(queue.offer(32));
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
}
/*
输出:
true
true
false
12
22
null
*/
11、线程池
参考博客:https://www.jianshu.com/p/7726c70cdc40
1、线程池的优势
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))
- 提供更强大的功能,延时定时线程池
2、七个参数
// ThreadPoolExecutor.java
/*
使用给定的初始参数创建一个新的{@code ThreadPoolExecutor}。
@param corePoolSize 即使在空闲时也要保留在池中的线程数,除非设置了{@code allowCoreThreadTimeOut}
@param maximumPoolSize 池中允许的最大线程数
@param keepAliveTime 当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间
@param unit {@code keepAliveTime}参数的时间单位
@param workQueue 在执行任务之前用于保留任务的队列。此队列将仅保存由{@code execute}方法提交的{@code Runnable} *任务。
@param threadFactory 执行程序创建新线程时要使用的工厂
@param handler 当执行被阻塞时要使用的处理程序,因为达到了线程界限和队列容量(maximumPoolSize + workQueue.size)
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// ...
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
// 拒绝策略默认是 AbortPolicy
// Executors.defaultThreadFactory() 内部类线程工厂,用于创建线程池和线程
// 线程池名字前缀为:pool-(poolNumber)-thread-(threadNumber),两个number都是AtomicInteger类的
}
3、四个创建方法
Executors返回的线程池对象的缺点:
-
FixedThreadPool和SingleThreadPool
允许请求的队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
-
CachedThreadPool和ScheduledThreadPool
允许创建的线程数量为Integer.MAX_VALUE,可能会堆积大量的线程,从而导致OOM
ExecutorService es1 = Executors.newSingleThreadExecutor();
ExecutorService es2 = Executors.newFixedThreadPool(int nThreads);
ExecutorService es3 = Executors.newCachedThreadPool();
ScheduledExecutorService es4 = Executors.newScheduledThreadPool(int corePoolSize);
Executors.newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
// Executors.java
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// ThreadPoolExecutor.java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
使用示例:
public class SingleThreadExecutorDemo {
public static void main(String[] args) {
ExecutorService threadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int temp = i;
threadExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " " + temp);
});
}
}
}
/*
输出: 注意这里是按序输出!!! 0~9
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
...
*/
Executors.newFixedThreadPool(int nThreads)
创建一个可重用固定个数的线程池,以共享的无界队列(无界队列:队列长度为Integer.MAX_VALUE)方式来运行这些线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Executors.newCachedThreadPool()
可缓存线程池,先查看池中有没有以前建立的线程:
-
如果有,就直接使用;
-
如果没有,就建一个新的线程加入池中
缓存型池子通常用于执行一些生存期很短的异步型任务
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Executors.newScheduledThreadPool(int corePoolSize)
Scheduled:已安排的
创建一个定长线程池,支持定时及周期性任务执行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue()); // super即为ThreadPoolExecutor
}
使用示例:
public class ScheduledThreadPoolDemo {
public static void main(String[] args) {
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
System.out.println(LocalDateTime.now());
threadPool.scheduleWithFixedDelay(() -> {
System.out.println(Thread.currentThread().getName()
+ " - 延迟3s后每2s执行一次 - "
+ LocalDateTime.now()
);
}, 3, 2, TimeUnit.SECONDS);
// 注意,如果使用了shutdown(),则会直接退出
}
}
/*
输出:
2020-12-09T16:26:38.209
pool-1-thread-1 - 延迟3s后每2s执行一次 - 2020-12-09T16:26:41.230 // 此处已经延迟3秒,之后不需要再延迟了
pool-1-thread-1 - 延迟3s后每2s执行一次 - 2020-12-09T16:26:43.235
pool-1-thread-2 - 延迟3s后每2s执行一次 - 2020-12-09T16:26:45.245
...
*/
4、四种拒绝策略
拒绝策略的作用:
当 需要执行的线程数 > maximumPoolSize+workQueue.size ,如何处理新的线程请求
RejectedExecutionHandler handler:饱和策略,有以下四种:
AbortPolicy: 直接抛出异常,默认
CallerRunsPolicy: 用调用者所在的线程(在main方法中即为main线程)来执行任务
DiscardOldestPolicy: 丢弃阻塞队列里最 老 的任务,队列里最 靠前 的任务
DiscardPolicy: 当前任务直接丢弃
5、DIY一个线程池
public class DIYThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
5,
10,
5,
TimeUnit.SECONDS,
new ArrayBlockingQueue(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
for (int i = 0; i < 100; i++) {
final int temp = i;
poolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " " + temp);
});
}
poolExecutor.shutdown();
}
}
pool-1-thread-1 0
pool-1-thread-2 1
pool-1-thread-2 5
pool-1-thread-2 6
pool-1-thread-2 7
pool-1-thread-2 8
pool-1-thread-2 9
pool-1-thread-2 13
pool-1-thread-2 15
pool-1-thread-2 16
pool-1-thread-2 24
pool-1-thread-2 47
pool-1-thread-2 95
pool-1-thread-2 96
pool-1-thread-2 97
pool-1-thread-2 98
pool-1-thread-2 99
pool-1-thread-3 2
pool-1-thread-5 4
pool-1-thread-4 3
pool-1-thread-6 10
pool-1-thread-7 11
pool-1-thread-9 14
pool-1-thread-8 12
pool-1-thread-10 18
可以看到,在采用 ThreadPoolExecutor.DiscardOldestPolicy() 策略的时候,会丢失部分任务,根据需求采用对应的策略
12、四大函数式接口
接口 | 方法 | 可能用途 |
---|---|---|
public interface Function<T, R> | R apply(T t) | 计算,并获取返回值 |
public interface Consumer | void accept(T t) | 输出 |
public interface Supplier | T get() | 获取 |
public interface Predicate | boolean test(T t) | 判断 |
13、Stream流式计算
略(函数式编程的笔记见day02.md)
14、Fork/Join
1、入门介绍
参考链接:ForkJoin分析 - 简书 (jianshu.com)
1、fork和join如何理解呢?
fork就是创建分支的意思:如果任务大小小于我们能接受的大小,那线程直接执行;否则,我们会创建分支,由两个子线程来执行原任务,依次递归;
join就是线程等待的意思:父任务线程创建了子任务线程,他需要等待子任务线程执行完毕,返回最终结果。
2、主要的涉及的几个类:
- ForkJoinPool:执行ForkJoinTask的线程池;继承了AbstractExecutorService(implements ExecutorService);
- ForkJoinTask:抽象类,是ForkJoinPool池的任务基类。其有两个实现的子类:
- RecursiveAction:针对没有返回结果的子任务集;(RecursiveAction extends ForkJoinTask)
- RecursiveTask:针对有返回结果的子任务集。(RecursiveTask extends ForkJoinTask)
2、ForkJoin注意点
使用ForkJoin将相同的计算任务通过多线程的进行执行。从而能提高数据的计算速度。在google的中的大数据处理框架mapreduce就通过类似ForkJoin的思想。通过多线程提高大数据的处理。但是我们需要注意:
- 使用这种多线程带来的数据共享问题,在处理结果的合并的时候如果涉及到数据共享的问题,我们尽可能使用JDK为我们提供的并发容器。
- 在使用JVM的时候我们要考虑OOM的问题,如果我们的任务处理时间非常耗时,并且处理的数据非常大的时候,会造成OOM。
- ForkJoin也是通过多线程的方式进行处理任务。那么我们不得不考虑是否应该使用ForkJoin。因为当数据量不是特别大的时候,我们没有必要使用ForkJoin。因为多线程会涉及到上下文的切换。所以数据量不大的时候使用串行比使用多线程快。
3、ForkJoin工作窃取(work-stealing)
为什么ForkJoin会存在工作窃取呢?因为我们将任务进行分解成多个子任务的时候。每个子任务的处理时间都不一样。例如分别有子任务A、B。如果子任务A的1ms的时候已经执行完,而子任务B还在执行。那么如果我们子任务A的线程等待子任务B完毕后再进行汇总,那么子任务A线程就会在浪费执行时间,最终的执行时间就以最耗时的子任务为准。而如果我们的子任务A执行完毕后,处理子任务B的任务,并且执行完毕后将任务归还给子任务B。这样就可以提高执行效率。而这种就是工作窃取。
4、ForkJoin的使用
总的过程(结合代码示例分析)
- 创建ForkJoinTask实现类(RecursiveAction或者RecursiveTask,泛型T) 的实现类(MyTask)
- 实现compute(),在方法体中实现具体的fork/join
- 创建实现类MyTask的对象task
- 创建ForkJoinPool pool
- 获取结果的两种方式:
- pool调用其invoke(task),返回T submit
- pool调用 submit(task) ,返回ForkJoinTask submit,通过submit.get()获取返回值
代码示例(ForkJoin计算)
public class ForkJoinTaskDemo {
public static void main(String[] args) {
// 3、4
MyTask task = new MyTask(0L, 100000L);
ForkJoinPool pool = new ForkJoinPool();
// 方式1
Long res1 = pool.invoke(task);
System.out.println(res1);
// 方式2
ForkJoinTask<Integer> submit = pool.submit(task);
Integer res2 = submit.get(); // try-catch
System.out.println(res2);
}
// 1
static class MyTask extends RecursiveTask<Long> {
private Long start;
private Long end;
private Long mid;
public MyTask(Long start, Long end) {
this.start = start;
this.end = end;
}
// 2
@Override
protected Long compute() {
Long sum = 0L;
if (end <= 20000) {
for (Long i = start; i <= end; i++) {
sum += i;
}
} else {
mid = (end + start) / 2;
MyTask task1 = new MyTask(start, mid);
MyTask task2 = new MyTask(mid + 1, end);
// invokeAll(task1,task2);
// 效果等同
task1.fork();
task2.fork();
sum = task1.join() + task2.join();
}
return sum;
}
}
}
15、异步回调(CompletableFuture)
参考博客
https://www.jianshu.com/p/e54af7f40772
https://blog.csdn.net/qq_29051413/article/details/108002189
异步
类似Ajax异步请求
Future:对将来的某个事件结果进行建模
设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future
中触发那些潜在耗时的操作 把 调用线程解放出来,让线程能继续执行其他有价值的工作,不需要等待耗时的操作完成
示例:使用Future
以异步的方式执行一个耗时的操作
ExecutorService executor = Executors.newCachedThreadPool();
Future<Double> future = executor.submit(new Callable<Double>() { //向ExecutorService提交一个Callable对象
public Double call() {
return doSomeLongComputation();//以异步方式在新线程中执行耗时的操作
}
});
doSomethingElse();
try {
Double result = future.get(1, TimeUnit.SECONDS);//获取异步操作结果,如果被阻塞,无法得到结果,在等待1秒钟后退出
} catch (ExecutionException ee) {
// 计算抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 在Future对象完成之前超时
}
这种编程方式让你一部分的线程,在 另一部分线程执行消耗时间的操作 的时候,并发地去执行其他任务。
如果已经运行到没有异步操作,可以调用它的get()
去获取操作结果。如果操作已经完成,该方法会立刻返回操作结果,否则(操作未完成)它会阻塞线程,直到操作完成,返回相应的结果。
为了处理长时间运行的操作永远不返回的可能性,虽然Future
提供了一个无需任何参数的get
方法,但还是推荐使用重载版本的get(long timeout, TimeUnit unit)
,它不会永无止境地等待。
CompletableFuture:异步回调
没有返回值的
CompletableFuture<Void> runAsync(Runnable runnable)
public class SyncDemo1 {
public static void main(String[] args) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("runAsync");
});
System.out.println("---");
try {
System.out.println(future.get(2, TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
输出:
--- // 输出这个后,主线程到 get(),再回调输出 "runAsync"
java.lang.ArithmeticException: / by zero
at ........
runAsync
null
*/
有返回值的
<U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public class SyncDemo2 {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
int i = 1 / 0;
return 1024;
});
Integer integer = null;
try {
integer = future.whenComplete((t, u) -> { //t:返回值,u:异常类型
System.out.println("t:" + t);
System.out.println("u:" + u);
}).exceptionally((e) -> { // 输出CompletableFuture.supplyAsync(.)里面运行的错误
System.out.println("exception : " + e.getMessage());
return 400; // 执行出错返回的值
}).get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(integer);
}
}
/*
未开启错误的那行:
t:1024
u:null
1024
开启错误的那行:
t:null
u:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
exception : java.lang.ArithmeticException: / by zero
Caused by: java.lang.ArithmeticException: / by zero
...
400
*/
成功的回调:
whenComplete()
& 出错的回调exceptionally()
// 返回值都是 CompletableFuture<T> ,满足 链式编程
public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {
return uniWhenCompleteStage(null, action);
}
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn) {
return uniExceptionallyStage(fn);
}
16、JMM(Java内存模型)
参考博客
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.jianshu.com/p/8a58d8335270
1、什么是JMM
摘自《深入理解Java虚拟机》
Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的(这里指的是局部变量表中的reference用类型,不是对象本身),不会被共享,自然就不会存在并发问题
现代计算机的内存模型:
JVM线程、主内存、工作内存三者的交互关系:
- JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
- 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写该共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
- 它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
2、JVM对JMM的实现
- 在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区
- JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,即栈帧。随着代码的不断执行,栈帧会不断变化。
- 数据存储在哪:
- 一个局部变量如果是原始类型,那么它会被完全存储到栈区
- 一个局部变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区
- 对于一个对象的成员方法,这些方法中包含局部变量,仍需要存储在栈区,即使它们所属的对象在堆区
- 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区
- static类型的变量以及类本身相关信息都会随着类本身存储在堆区(jdk7及之后静态变量存储在堆;类信息在方法区)
3、JMM引发的问题
1、可见性
- 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的
- CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中。
- 要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁
2、有序性(重排序)
- 在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序
- 1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
3、原子性
- 原子是世界上的最小单位,具有不可分割性
- 比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作
- 再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作
- 非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
- java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
- 在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性
4、内存间交互操作
关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立即刷回主存
- 线程加锁前,必须把共享变量最新的值从主存读到线程的工作内存中
- 加锁和解锁使用同一把锁
JMM的8个操作&8个规则
8个操作
lock
(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态unlock
(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定read
(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用load
(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中use
(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时将会执行这个操作assign
(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作store
(存储):作用于工作内存的变量,它把工作内存中一个变量的值传到主内存中,以便随后的write操作使用write
(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
说明:JVM实现时必须保证这些操作是原子的(对于double、long类型,在32位操作系统,有注意的点)
8个规则
- 不允许read和load、store和write操作之一单独出现(要成对出现),即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现;
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回 主内存;
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存 中;
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作;
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁(类似互斥锁);
- 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值;
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量;
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
17、volatile
volatile关键字是JVM提供的最轻量级的同步机制
volatile的两个作用:可见性、禁止指令重排序优化
JMM是围绕原子性、可见性、有序性这三个特征来建立的
1、三个特征
1、原子性
- JMM中直接保证原子性的操作:read、load、use、assign、store、write
- 基本数据类型的读写具有原子性(double和long有非原子协定,即允许虚拟机实现自行选择是否要实现64bit的读写操作划分为两次32bit的操作来进行,除非需要,否则别用volatile修饰它们)
- 如果需要更大范围的原子性保证,JMM提供了lock和unlock操作来满足这种需求,尽管虚拟机未直接开放lock和unlock给用户,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,而synchronized就是这两个操作的使用者,也就是说在synchronized块之间的操作具备原子性
- volatile不保证原子性,只保证 read 的时候可以读取最新的值、assign 之后会立即更新到主存
2、可见性
points
- 保证volatile变量对所有线程的可见性。**“可见性”是指当一条线程改变了这个变量的值,新值对于其它线程是可以立即得知的。**而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
- 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主内存,当有其他线程需要读取时,它会去主内存中读取最新的值
- 通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。
- 同步块和锁的可见性是由 “对一个变量执行unlock之前,必须先把此变量同步到主内存中(store、write)”这条规则获得的
- final也可以保证可见性
注意:基于volatile变量的运算在并发下是存在线程安全问题的!
同步安全问题-代码示例
public class VolatileTest {
private static volatile int race = 0;
static void increase() {
race++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
}).start();
}
// 等所有累加线程都结束,main、gc
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(race); // 总是小于100000的数
}
}
分析(重点)
increase()的字节码:
当getstatic指令把race的值取到OS栈顶时,volatile关键字保证了race的值在此时是正确的,但是执行iconst_1、iadd这些指令的时候,其它线程可能已经把race的值改变了,因此OS栈顶的值就变成了过期的数据,所以putstatic指令执行后,就把较小的race值同步回主内存中
解决方法
-
给increase()加synchronized关键字
static synchronized void increase() { race++; }
-
给race++操作加锁
private ReentrantLock lock = new ReentrantLock(); static void increase() { lock.lock(); try { race++; } finally { lock.unlock(); } }
-
使用原子类(Integer -> AtomicInteger)来替代volatile变量
public static AtomicInteger atomicint = new AtomicInteger(); static void increase() { atomicint.getAndIncrement(); }
3、有序性
volatile关键字能禁止指令重排序优化,所以volatile能在一定程度上保证有序性
volatile关键字禁止指令重排序有两层意思
-
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
例子:
//x、y为非volatile变量 //flag为volatile变量 x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的
-
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
例子:
//线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕
synchronized的重排序
一个变量在同一时刻只允许一条线程对其进行lock(字节码层面是monitorenter)操作
2、volatile的原理和实现机制
这段话摘自《深入理解Java虚拟机》:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
3、安全并发的volatile使用场景
由于volatile变量只能保证可见性,我们要通过加锁或者使用原子类(synchronized、java.lang.concurrent中的锁或原子类)来保证操作的原子性,以下是不需要使用锁的情况:
- 运算结果不依赖变量的当前值,或者能够确保只有单一的线程改变变量的值
- 变量不需要与其它的状态变量共同参与不变约束
总而言之,就是保证对volatile变量的操作是原子的即可
更多详细内容以后补充
18、安全的单例模式
1、破坏单例模式的三种方式
序列化、反射、克隆
双检锁单例模式
public static class Singleton implements Serializable, Cloneable {
public static final Long serialVersionUID = 23900129092L;
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 创建并返回该对象的副本
}
}
1、序列化
public static void destoryBySerializable() throws Exception {
Singleton originSingleton = Singleton.getInstance();
ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 字节数组输出流,底层创建了字节数组
ObjectOutputStream oos = new ObjectOutputStream(bos); // 对象流
oos.writeObject(originSingleton); // 对象输出流写对象到字节数组
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); // 字节数组读取流,bis的buf数组==bos的buf数组
ObjectInputStream ois = new ObjectInputStream(bis); // 对象流输入流
Singleton serializeSingleton = (Singleton) ois.readObject(); // 从字节数组中读取对象
System.out.println(originSingleton == serializeSingleton); // false
}
2、反射
public static void destoryByReflect() throws Exception {
Singleton instance = Singleton.getInstance(); // 触发创建单例对象
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); // 返回空参的构造函数
constructor.setAccessible(true); // 设置权限
Singleton singleton = constructor.newInstance(); // 利用反射创建对象
System.out.println(singleton == instance); // false
}
3、克隆
public static void destoryByCloneable() throws Exception {
Singleton instance = Singleton.getInstance();
Singleton clone = (Singleton) instance.clone();
System.out.println(instance == clone); // false
}
有关clone()方法的说明:
创建并返回此对象的副本。 “复制”的确切含义可能取决于对象的类别。
通常的意图是,对于任何对象{@code x}有:
- 表达式: x.clone()==x 将为false
- 表达式: x.clone().getClass()== x.getClass() 将是true
但这不是绝对要求, 虽然通常情况是:x.clone().equals(x)将是true(浅拷贝),但这不是绝对要求。
conle()的代码测试:
public class CloneDemo {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
A clone = (A) a.clone();
System.out.println(a == clone); // false
System.out.println(a.getClass() == clone.getClass()); //true
}
static class A implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}
2、解决方法
序列化:添加Object readResolve()
反射:设置一个标志,如果是第二次调用构造器则抛出异常
克隆:重写方法,直接返回单例对象
package review2._18;
import java.io.*;
import java.lang.reflect.Constructor;
/**
* @author zedh
* @date 2021/1/13-14:42
*/
public class SolveSingletonSafeProblem {
public static void main(String[] args) {
try {
destoryBySerializable();
destoryByCloneable();
destoryByReflect();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 三个破坏方法与前面的相同
*/
public static void destoryBy_() throws Exception {
}
public static class Singleton implements Serializable, Cloneable {
public static final Long serialVersionUID = 12893892839L;
private static volatile Singleton singleton;
private static volatile boolean isCreated = false;
private Singleton() {
// 防止反射破坏单例模式,第二次使用构造方法则抛出异常
if (isCreated) {
throw new RuntimeException("你正在尝试破坏单例模式,拒绝此行为");
}
isCreated = true;
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
/**
* 防止序列化破坏单例
*
* @return
*/
private Object readResolve() {
return singleton;
}
/**
* 防止克隆破坏单例模式
*
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return singleton;
}
}
}
测试的输出
true
true
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at review2._18.SolveSingletonSafeProblem.destoryByReflect(SolveSingletonSafeProblem.java:37)
at review2._18.SolveSingletonSafeProblem.main(SolveSingletonSafeProblem.java:15)
Caused by: java.lang.RuntimeException: 你正在尝试破坏单例模式,拒绝此行为
at review2._18.SolveSingletonSafeProblem$Singleton.<init>(SolveSingletonSafeProblem.java:55)
... 6 more
19、CAS
参考博客: 面试必问的CAS,你懂了吗?_程序员囧辉-CSDN博客_cas
1、CAS概述
CAS,CompareAndSwap,比较并交换
CAS需要有三个操作数:内存地址V,旧的预期值A,目标值B
CAS指令执行时,当且仅当A和内存地址V的值匹配时,将内存地址V的值修改为B
2、CAS的源码分析
我们以AtomicInteger.getAndIncreament()举例
AtomicInteger.class
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe.class
Unsafe类的实现主要是通过编译器,利用CPU的一些原子指令来实现原子性
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object <code>o</code>
* at the given <code>offset</code>.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add,加数
* @return 返回旧值v,这里不是返回新值,注意!!!
* @since 1.8
*/
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 获取字段当前的值
} while (!compareAndSwapInt(o, offset, v, v + delta)); // 通过CAS修改值,不成功则一直尝试(自旋锁)
return v; // 返回旧值
}
/** 原子操作版getInt(Object o, long offset) */
public native int getIntVolatile(Object o, long offset);
// 从给定的Java变量中获取一个值。
// 更具体地说,从给定对象o 处以给定偏移量获取字段或数组元素,或者从数值为给定偏移量的内存地址中获取字段或数组元素(如果o 为null)
public native int getInt(Object o, long offset);
/**
* 参数:
* o和offset:共同表示原值
* v:预期值
* x:新值
*
* 作用:当且仅当 原值等于预期值的时候才修改为新值
* @return 修改成功返回true
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
C/C++的源码分析
compareAndSwapInt()底层实现为Atomic::cmpxchg方法,方法体实现的功能为:
- os::is_MP():判断当前系统是否是多核处理器,将参数给mp变量名
- 是,则返回1
- 否,则返回0
- LOCK_IF_MP(mp)
- 如果mp为1,则为cmpxchg指令加lock前缀
- 如果mp为0,则不加前缀
lock前缀的作用:确保对内存的读-改-写操作原子执行;禁止指令重排序;将缓冲区中的数据刷新到内存中
3、CAS的缺点
1、循环时间长开销很大。
如果CAS失败,则会一直循环操作,给CPU带来很大的开销
2、只能保证一个变量的原子操作。
当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。
但是我们可以通过以下两种办法来解决:
- 使用互斥锁来保证原子性;
- 将多个变量封装成对象,通过 AtomicReference 来保证原子性
3、ABA问题
CAS 的使用流程通常如下:
- 首先从地址 V 读取值 A;
- 根据 A 计算目标值 B;
- 通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。
Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步 可能 会比原子类更高效。
4、用Java实现CAS操作
/**
* @author zedh
* @date 2021/1/14-16:23
*/
public class CASdemo {
public static void main(String[] args) {
MyUnsafe unsafe = new MyUnsafe(5);
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
unsafe.getAndAddOne();
latch.countDown();
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("val = " + unsafe.getIntVolatile()); // val = 10
}
static class MyUnsafe {
private volatile int val;
public MyUnsafe(int val) {
this.val = val;
}
public int getAndAddOne() {
int v;
do {
v = getIntVolatile();
} while (!compareAndSwap(getIntVolatile(), v, v + 1));
return v;
}
public int getIntVolatile() {
return val;
}
public boolean compareAndSwap(int latest, int expect, int newVal) {
if (latest == expect) {
synchronized (MyUnsafe.class) {
this.val = newVal;
return true;
}
}
return false;
}
}
}
20、原子类
博客参考:Java 原子性引用 AtomicReference - 简书 (jianshu.com)
文档参考:Java AtomicReference Example | Examples Java Code Geeks - 2021
- java.util.concurrent.atomic(since jdk1.5)
- 支持单个变量上的无锁线程安全编程。
- 原子变量和引用都是使用compareAndSwap(CAS指令)来实现:依赖当前值的原子修改的
- 他们的实现都是使用volatile和Unsafe:volatile保证可见性,而Unsafe保证原子性
使用举例1:AtomicStampedReference
/**
* @author zedh
* @date 2021/1/14-21:20
* 构造函数AtomicStampedReference(V initialRef, int initialStamp)
* compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
* 如果当前参考==期望参考并且当前标记等于期望标记,则以原子方式将参考和标记的值设置为给定的更新值
*/
public class SolveABAProblem {
public static void main(String[] args) {
AtomicStampedReference<Integer> sf = new AtomicStampedReference<>(5, 0);
Integer reference = sf.getReference();
int stamp = sf.getStamp();
boolean compareAndSet = sf.compareAndSet(reference, 9, stamp + 1, stamp + 1);
boolean compareAndSet2 = sf.compareAndSet(reference, 9, stamp, stamp + 1);
System.out.println(compareAndSet); // false 版本不一致
System.out.println(compareAndSet2); // true
// ====此时reference已经是9了
stamp = sf.getStamp();
boolean compareAndSet3 = sf.compareAndSet(10, 99, stamp, stamp + 1);
boolean compareAndSet4 = sf.compareAndSet(9, 77, stamp, stamp + 1);
System.out.println(compareAndSet3); // false 期望变量不一致
System.out.println(compareAndSet4); // true
}
@Test
public void m() {
Integer a = 127, b = 127, c = 128, d = 128;
System.out.println(a == b); // true
System.out.println(c == d); // false
// IntegerCache.cache [-128,127],而int就不会,因为int直接使用的就是字面量
}
}
注意Integer静态内部类IntegerCache:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
// 省略...
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
}
使用举例2:AtomicReference
public class Person{
private String name;
private int age;
// get set
}
public class NormalDemo {
private static Person person;
public static void main(String[] args) throws InterruptedException {
person = new Person("Tom", 18);
System.out.println("initial Person is " + person.toString());
Thread t1 = new Thread(()->{
person.setAge(person.getAge() + 1);
person.setName("Tom1");
System.out.println("Thread1 Values "
+ person.toString());
});
Thread t2 = new Thread(()->{
person.setAge(person.getAge() + 2);
person.setName("Tom2");
System.out.println("Thread2 Values "
+ person.toString());
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Now Person is " + person.toString());
}
}
/*
可能的输出:
initial Person is [name: Tom, age: 18]
Thread1 Values [name: Tom2, age: 21]
Thread2 Values [name: Tom2, age: 21]
Now Person is [name: Tom2, age: 21]
*/
public class AtomicReferenceDemo {
// 普通引用
private static Person person;
private static AtomicReference<Person> aRperson;
public static void main(String[] args) throws InterruptedException {
person = new Person("Tom", 18);
aRperson = new AtomicReference<Person>(person);
System.out.println("Atomic Person is " + aRperson.get().toString());
Thread t1 = new Thread(()->{
aRperson.getAndSet(new Person("Tom1", aRperson.get().getAge() + 1));
System.out.println("Thread1 Atomic References " + aRperson.get().toString());
});
Thread t2 = new Thread(()->{
aRperson.getAndSet(new Person("Tom2", aRperson.get().getAge() + 2));
System.out.println("Thread2 Atomic References " + aRperson.get().toString());
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Now Atomic Person is " + aRperson.get().toString());
}
}
/*
Atomic Person is [name: Tom, age: 18]
Thread1 Atomic References [name: Tom1, age: 19]
Thread2 Atomic References [name: Tom2, age: 21]
Now Atomic Person is [name: Tom2, age: 21]
*/
21、各种锁的理解
参考博客:java中的各种锁详细介绍 - JYRoy - 博客园 (cnblogs.com)
人家的博客太好了,我都不想写了…
0、死锁
1、乐观锁、悲观锁
乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据:
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
- 如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
在java中可用 CAS + 自旋锁 实现乐观锁
悲观锁
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
Java中,synchronized关键字和Lock的实现类都是悲观锁
2、自旋锁、自适应自旋锁
自旋锁
让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。
- 如果锁被占用的时间很短,自旋等待的效果就会非常好。
- 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。
所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁
自适应自旋锁
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间
- 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
3、无锁、偏向锁、轻量级锁、重量级锁
略
4、公平锁、非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁
优点:等待锁的线程不会饿死
缺点:整体吞吐效率相对非公平锁更低、除了队首以外的线程都会阻塞、CPU唤醒阻塞线程的开销比非公平锁大
Java中,new ReentrantLock(true)
非公平锁
非公平锁是多个线程加锁时直接尝试获取锁:
- 获取不到则会到等待队列的队尾等待
- 如果锁刚好可用,则无需阻塞直接获取锁
优点:减少换起线程的开销、整体的吞吐效率高
缺点:等待队列中的线程可能很久都不会获得锁,甚至饿死
Java中,new ReentrantLock(false)
5、可重入锁、非可重入锁
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞
优点:一定程度上避免死锁
Java中ReentrantLock和synchronized都是可重入锁
6、独享锁、共享锁
独享锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有
TODO
22、AbstractQueuedSynchronizer
23、ReentrantLock原理
底层使用CAS+AQS