一、Semaphore 信号量
1、定义
用来限制能同时访问共享资源的线程上限。
比喻为停车位,车位占用则不能使用,得等上一辆车开走,释放资源。(类似于能允许存在多个锁)
类似于之前的notify和wait。是经过封装的高级api,用起来更简单
2、应用
单机版、当许可数(资源线程上限)==资源数时,更适合用
3、原理
核心是继承了AQS.使用其中的states来存放允许的线程数。每消耗一个许可,则减一。释放一个许可则加一
4、示例代码
-
抢车位示例:
package com.jian8.juc.conditionThread; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3);//模拟三个停车位 for (int i = 1; i <= 6; i++) {//模拟6部汽车 new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "\t抢到车位"); try { TimeUnit.SECONDS.sleep(3);//停车3s } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t停车3s后离开车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }, "Car " + i).start(); } } }
二、CountdownLatch
1、定义
用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,countDown() 用来让计数减一,await() 用来等待计数归零(在归零前一直等待着)。
一般是主线程等待其他线程执行完,继续后面的操作。
2、原理
内部和信号量一样,有一个同步器Sync.也是根据state的值来判断,初始值时x,让所有应执行的线程都执行完,state=0时,才停止等待,进行之后的任务。
3、应用
和之前的join效果差不多,但是是高级api
1)同步等待多线程准备完毕
2)同步等待多个远程调用结束
4、代码示例:
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// general();
countDownLatchTest();
}
public static void general(){
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t上完自习,离开教室");
}, "Thread-->"+i).start();
}
while (Thread.activeCount()>2){
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.println(Thread.currentThread().getName()+"\t=====班长最后关门走人");
}
public static void countDownLatchTest() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t被灭");
countDownLatch.countDown();
}, CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t=====秦统一");
}
}
三、CyclicBarrier
1、定义
循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执
行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
CountdownLatch只能使用一次,CyclicBarrier可以循环使用多次。当计数减为0时,下一次线程数够时再调,又会从头开始
2、比较
CyclicBarrier 可以被比喻为『人满发车』,下一班满了再次发车;
而CountdownLatch是人下完之后,车开走了,并且不再回来。
3、代码示例:
package com.jian8.juc.conditionThread;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
cyclicBarrierTest();
}
public static void cyclicBarrierTest() {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("====召唤神龙=====");
});
for (int i = 1; i <= 7; i++) {
final int tempInt = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t收集到第" + tempInt + "颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, "" + i).start();
}
}
}
四、线程安全集合类概述
线程安全集合类不一定真的安全。因为他们只保证某个方法的原子性。
在多个线程中,一个方法同时get+计算+put 就会线程不安全。要考虑如何安全使用
1)前两类都是直接用sync关键字,效率不高。不推荐使用。
2)JUC安全集合
Blocking 大部分实现基于锁,并提供用来阻塞的方法
CopyOnWrite 之类容器修改开销相对较重
Concurrent 类型的容器
内部很多操作使用 cas 优化,一般可以提供较高吞吐量
弱一致性
遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
求大小弱一致性,size 操作未必是 100% 准确
读取弱一致性
3) fail-fast
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出
ConcurrentModificationException,不再继续遍历
五、currentHashMap
1、hashMap并发死链
1.7中用的是头插法,链表中的数据新来的都插在头部
1.8 是尾插法
具体代码见(并发原理 P81)
2、jdk8(重点)
2.1重要属性和内部类
// 默认为 0
// 当初始化时, 为 -1(懒加载)
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
// 静态内部类,单个节点。
static class Node<K,V> implements Map.Entry<K,V> {}
//整个ConcurrentHashMap 就是一个 Node[]。 hash 表
transient volatile Node<K,V>[] table;
// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
2.2重要方法
// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
2.3构造器
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ...
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
2.4 get
由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。
同时每个元素是一个Node实例,它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。而其Value及对下一个元素的引用由volatile修饰,可见性也有保障。–无需加锁
2.5 put
put
1)如果放入时表还未创建,则建表;
2)如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值;
3)如果放入时正在扩容,则帮忙扩容;
4)如果Key对应的数组元素不为null(发生了hash冲突),这时才对头元素进行sync加锁
如果是普通链表:
如果找到相同key,则更新当前节点
如果没找到 ,则在链表尾部追加
如果是tree,调用tree的方法
5)如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。
2.5 transfer
入参是当前tab和新的nexttab
1)容量x2创建nexttab
2)节点搬迁,以单个链表为单位
处理完的链表状态为fnode
链表头有元素,按链表和tree不同的方法去移动
2.6总结
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
1)初始化,使用 cas 来保证并发安全,懒惰初始化 table树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
2)put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
3)get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新table 进行搜索
4)扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可
3、JDK7
它维护了一个 segment 数组,每个 segment 对应一把锁
优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
详情见P92
4、ConcurrentHashMap变化
为何JDK8要放弃分段锁?
- 加入多个分段锁 浪费了内存空间
- 生产环境中,map在放入时 竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待
- 为了提高GC的效率
为什么是用Synchronized 而不是 ReentrantLock?
- 减少内存开销
- 假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要同步支持,只有链表的头结点(红黑树的根节点)需要同步,这无疑带来了巨大的浪费
- 获得JVM支持
- 可重入锁毕竟是API这个级别的,后续的性能优化空间 很小
- Synchronized则是由JVM直接支持,JVM能够在运行时做出对应的优化措施:锁粗化,锁消除,锁自旋等。这就是使得Synchronized能够随着JDK版本的升级而无需改动代码的前提下获得性能上的提升。
六、LinkedBlockingQueue(阻塞)
1、比较 推荐linked
Linked 支持有界(默认无界),Array 强制有界
Linked 实现是链表,Array 实现是数组
Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
Linked 两把锁,Array 一把锁
七、ConcurrentLinkedQueue(并发)
1、比较
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像
1)两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
2)dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
3)不同:只是这【锁】使用了 cas 来实现
2、应用
例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了
ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
八、CopyOnWriteArrayList
1、特点
CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更
改操作在新数组上执行,这时不影响其它线程的并发读,读写分离
2、应用
适合『读多写少』的应用场景,用空间换取线程安全。
3、弱一致性
1)get | 迭代器弱一致性
2)不要觉得弱一致性就不好
数据库的 MVCC(多版本并发控制) 都是弱一致性的表现
并发高和一致性是矛盾的,需要权衡