ThreadLocal
典型的空间交换时间,比如在hibernate中session就存在与ThreadLocal中,避免synchronized的使用。 线程的局部变量,使线程之间的变量隔离,每个线程只能够操作自己线程内的变量。 在通常的业务开发中,ThreadLocal 有两种典型的使用场景:
- ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
- ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。
容器
HashTable(synchronized)->HashMap(线程不安全)->synchronizedMap(线程安全)->ConcurrentHashMap(线程安全)
vector(synchronized)->arrayList(线程不安全)->Collections.synchronizedXXX(线程安全)->CopyOnWriteList(线程安全)
ConcurrentHashMap
主要应用场景,多线程高并发读多写少的场景,ConcurrentHashMap在并发读上性能非常好。 类属性
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
// 表的最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认表的大小
private static final int DEFAULT_CAPACITY = 16;
// 最大数组大小
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 默认并发数
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 装载因子
private static final float LOAD_FACTOR = 0.75f;
// 转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 由红黑树转化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 转化为红黑树的表的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 每次进行转移的最小值
private static final int MIN_TRANSFER_STRIDE = 16;
// 生成sizeCtl所使用的bit位数
private static int RESIZE_STAMP_BITS = 16;
// 进行扩容所允许的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 记录sizeCtl中的大小所需要进行的偏移位数
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 一系列的标识
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//
/** Number of CPUS, to place bounds on some sizings */
// 获取可用的CPU个数
static final int NCPU = Runtime.getRuntime().availableProcessors();
//
/** For serialization compatibility. */
// 进行序列化的属性
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("segments", Segment[].class),
new ObjectStreamField("segmentMask", Integer.TYPE),
new ObjectStreamField("segmentShift", Integer.TYPE)
};
// 表
transient volatile Node<K,V>[] table;
// 下一个表
private transient volatile Node<K,V>[] nextTable;
//
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
// 基本计数
private transient volatile long baseCount;
//
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
// 对表初始化和扩容控制
private transient volatile int sizeCtl;
/**
* The next table index (plus one) to split while resizing.
*/
// 扩容下另一个表的索引
private transient volatile int transferIndex;
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
// 旋转锁
private transient volatile int cellsBusy;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
// counterCell表
private transient volatile CounterCell[] counterCells;
// views
// 视图
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
// Unsafe mechanics
private static final sun.misc.Unsafe U;
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final long ABASE;
private static final int ASHIFT;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = ConcurrentHashMap.class;
SIZECTL = U.objectFieldOffset
(k.getDeclaredField("sizeCtl"));
TRANSFERINDEX = U.objectFieldOffset
(k.getDeclaredField("transferIndex"));
BASECOUNT = U.objectFieldOffset
(k.getDeclaredField("baseCount"));
CELLSBUSY = U.objectFieldOffset
(k.getDeclaredField("cellsBusy"));
Class<?> ck = CounterCell.class;
CELLVALUE = U.objectFieldOffset
(ck.getDeclaredField("value"));
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (Exception e) {
throw new Error(e);
}
}
}
源码剖析 Map.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) {
//key-value校验,抛出异常
if (key == null || value == null) throw new NullPointerException();
//求hash值
int hash = spread(key.hashCode());
int binCount = 0;
//无限循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果tab是null或者长度0,初始化
if (tab == null || (n = tab.length) == 0)
//初始化表
tab = initTable();
// 表不为空并且表的长度大于0,并且该桶不为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//当前位置进行cas操作,compareAndSet,如tab的第i项为空则用新生成的node替换
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 该结点的hash值为MOVED
else if ((fh = f.hash) == MOVED)
// 进行结点的转移(**在扩容的过程中**)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//加锁同步
synchronized (f) {
//找到下标为i的节点f
if (tabAt(tab, i) == f) {
//该table表中该结点的hash值大于0
if (fh >= 0) {
// binCount赋值为1
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {//循环
K ek;
//结点的hash值相等并且key也相等
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 保存该结点的val值
oldVal = e.val;
// 进行判断
if (!onlyIfAbsent)
// 将指定的value保存至结点,即进行了结点值的更新
e.val = value;
break;
}
// 保存当前结点
Node<K,V> pred = e;
// 当前结点的下一个结点为空,即为最后一个结点
if ((e = e.next) == null) {
// 新生一个结点并且赋值给next域
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 结点为红黑树结点类型
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// 将hash、key、value放入红黑树
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
// 赋值结点value值
p.val = value;
}
}
}
}
if (binCount != 0) {
// 如果binCount大于等于转化为红黑树的阈值
if (binCount >= TREEIFY_THRESHOLD)
// 进行转化
treeifyBin(tab, i);
// 旧值不为空
if (oldVal != null)
// 返回旧值
return oldVal;
break;
}
}
}
// 增加binCount的数量
addCount(1L, binCount);
return null;
}
说明:put函数底层调用了putVal进行数据的插入,对于putVal函数的流程大体如下。
① 判断存储的key、value是否为空,若为空,则抛出异常,否则,进入步骤②
② 计算key的hash值,随后进入无限循环,该无限循环可以确保成功插入数据,若table表为空或者长度为0,则初始化table表,否则,进入步骤③
③ 根据key的hash值取出table表中的结点元素,若取出的结点为空(该桶为空),则使用CAS将key、value、hash值生成的结点放入桶中。否则,进入步骤④
④ 若该结点的的hash值为MOVED,则对该桶中的结点进行转移,否则,进入步骤⑤
⑤ 对桶中的第一个结点(即table表中的结点)进行加锁,对该桶进行遍历,桶中的结点的hash值与key值与给定的hash值和key值相等,则根据标识选择是否进行更新操作(用给定的value值
替换该结点的value值),若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点。进入步骤⑥
⑥ 若binCount值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储,最后,增加binCount的值。
在putVal函数中会涉及到如下几个函数:initTable、tabAt、casTabAt、helpTransfer、putTreeVal、treeifyBin、addCount函数。下面对其中涉及到的函数进行分析。
其中 initTable函数源码如下:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 无限循环
if ((sc = sizeCtl) < 0) // sizeCtl小于0,则进行线程让步等待
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 比较sizeCtl的值与sc是否相等,相等则用-1替换
try {
if ((tab = table) == null || tab.length == 0) { // table表为空或者大小为0
// sc的值是否大于0,若是,则n为sc,否则,n为默认初始容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 新生结点数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 赋值给table
table = tab = nt;
// sc为n * 3/4
sc = n - (n >>> 2);
}
} finally {
// 设置sizeCtl的值
sizeCtl = sc;
}
break;
}
}
// 返回table表
return tab;
}
说明:对于table的大小,会根据sizeCtl的值进行设置,如果没有设置szieCtl的值,那么默认生成的table大小为16,否则,会根据sizeCtl的大小设置table大小。
helpTransfer函数源码如下 :
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// table表不为空并且结点类型是ForwardingNode类型,并且结点的nextTable不为空
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) { // 条件判断
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0) //
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // 比较并交换
// 将table的结点转移到nextTab中
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
此函数用于在扩容时将table表中的结点转移到nextTable中。 treeifyBin函数源码如下:
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) { // 表不为空
if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // table表的长度小于最小的长度
// 进行扩容,调整某个桶中结点数量过多的问题(由于某个桶中结点数量超出了阈值,则触发treeifyBin)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { // 桶中存在结点并且结点的hash值大于等于0
synchronized (b) { // 对桶中第一个结点进行加锁
if (tabAt(tab, index) == b) { // 第一个结点没有变化
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) { // 遍历桶中所有结点
// 新生一个TreeNode结点
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null) // 该结点前驱为空
// 设置p为头结点
hd = p;
else
// 尾节点的next域赋值为p
tl.next = p;
// 尾节点赋值为p
tl = p;
}
// 设置table表中下标为index的值为hd
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
get方法解析
复制代码
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算key的hash值
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { // 表不为空并且表的长度大于0并且key所在的桶不为空
if ((eh = e.hash) == h) { // 表中的元素的hash值与key的hash值相等
if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 键相等
// 返回值
return e.val;
}
else if (eh < 0) // 结点hash值小于0
// 在桶(链表/红黑树)中查找
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { // 对于结点hash值大于0的情况
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
说明:get函数根据key的hash值来计算在哪个桶中,再遍历桶,查找元素,若找到则返回该结点,否则,返回null。
replaceNode函数:
final V replaceNode(Object key, V value, Object cv) {
// 计算key的hash值
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) { // 无限循环
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null) // table表为空或者表长度为0或者key所对应的桶为空
// 跳出循环
break;
else if ((fh = f.hash) == MOVED) // 桶中第一个结点的hash值为MOVED
// 转移
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) { // 加锁同步
if (tabAt(tab, i) == f) { // 桶中的第一个结点没有发生变化
if (fh >= 0) { // 结点hash值大于0
validated = true;
for (Node<K,V> e = f, pred = null;;) { // 无限循环
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { // 结点的hash值与指定的hash值相等,并且key也相等
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) { // cv为空或者与结点value相等或者不为空并且相等
// 保存该结点的val值
oldVal = ev;
if (value != null) // value为null
// 设置结点value值
e.val = value;
else if (pred != null) // 前驱不为空
// 前驱的后继为e的后继,即删除了e结点
pred.next = e.next;
else
// 设置table表中下标为index的值为e.next
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) { // 为红黑树结点类型
validated = true;
// 类型转化
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) { // 根节点不为空并且存在与指定hash和key相等的结点
// 保存p结点的value
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) { // cv为空或者与结点value相等或者不为空并且相等
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p)) // 移除p结点
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
// baseCount值减一
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
说明:此函数对remove函数提供支持,remove函数底层是调用的replaceNode函数实现结点的删除。
总结:Hashtable低效主要是因为所有访问Hashtable的线程都争夺一把锁。如果容器有很多把锁,每一把锁控制容器中的一部分数据,那么当多个线程访问容器里的不同部分的数据时,线程之前就 不会存在锁的竞争,这样就可以有效的提高并发的访问效率。 这也正是ConcurrentHashMap使用的分段锁技术。将ConcurrentHashMap容器的数据分段存储,每一段数据分配一个Segment(锁),当线程占用其中一个Segment时,其他线程可正常访问其他段数据。
CopyOnWriteList
CopyOnWriteArrayList
很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
优点:利用读写分离的思想,读操作性能非常高,比较适合读多写少的场景。 缺点:内存压力大(大数据量下容易造成频繁的gc),无法保证实时性,有一定的延时。
线程池
线程池相关文章推荐:Java线程池实现原理及其在美团业务中的实践
线程有几种状态,状态之间的变化是怎么样的?
java线程在运行的声明周期中可能处于6种不同的状态,在给定的一个时刻,线程只可能处理其中的一个状态。
线程池的工作原理
所有任务来了进入任务队列,当线程池中没有空闲线程并且线程数小于coreSize时候,创建新的线程执行队列中的任务。当线程数大于等于coreSize并小于最大线程数的时候,队列如果满了创建新的线程处理任务,如果队列没满进入等待队列。
详细的任务调度流程图
线程池的常用参数及含义
- corePoolSize:线程池里应该有多少个线程(核心线程数)
- maximumPoolSize:如果线程池里的线程不够用了,等待队列还塞满了,此时可能会根据不同的线程池类型创建一些线程出来,但是不会超过maximumPoolSize的数量(最大线程数)
- keepAliveTime+TimeUnit:如果你的线程数量超出了corePoolSize的话,超过corePoolSize指定数量的线程会在空闲keepAliveTime时间后自动释放掉TimeUnit设置时间单位
- workQueue:定义线程池使用的等待队列是什么队列
- threadFactory:线程工厂,指定线程工厂自定义创建线程的方式
- RejectedExecutionHandle:(拒绝策略)如果线程池的线程都在执行任务 ,然后等待队列满了,此时增加额外的线程也达到了maximumPoolSize指定的数量了,这个时候无法承载更多的任务。最好自定义决绝策略 写入磁盘
线程池有哪几种分别的应用场景
newCachedThreadPool
创建一个线程池,如果线程不够会自动创建,如果线程数量过多会自动回收
public static void method() throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int index = i;
Thread.sleep(1000);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + index);
}
});
}
}
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
//如果第一个线程耗时比较久也可能会出现下面的情况
pool-1-thread-1 0
pool-1-thread-2 1
pool-1-thread-2 2
不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处理缺是我们无法控制的; 优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并不会重新创建新的线程,提高了线程的复用率;
newFixedThreadPool
这种方式和newCachedThreadPool类似只是可以设置线程池的最大上限数量
优点:两个结果综合说明,newFixedThreadPool的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器打到最大的使用率,同时又可以保证即使流量突然增大也不会占用服务器过多的资源。
newScheduledThreadPool (场景定时任务)
该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。 该线程池中有以下两种延迟的方法。
scheduleAtFixedRate
第一次任务开始的时间到下一次任务开始的时间间隔 缺点:如果任务执行时间大于间隔时间会影响间隔周期,并且当任务数量比较频繁的时候等待队列过长会导致程序异常。
scheduleWithFixedDelay
第一次任务结束的时间到下一次任务开始的时间间隔 推荐使用
newSingleThreadExecutor
这是一个单线程池,至始至终都由一个线程来执行。
如果线程池中使用无界阻塞队列会发生什么问题(在远程服务异常的情况下,使用无界阻塞队列,是否会导致内存异常飙升)
队列可能会越来越大,内存会飙升,而且可能最终会导致OOM(内存溢出)。
如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办
必然会导致线程池里的积压的任务实际上来说都是会丢失的。
如果说你要提交一个任务到线程池里去,在提交之前,麻烦你先在数据库里插入这个任务的信息,更新他的状态:未提交、已提交、已完成。提交成功之后,更新他的状态是已提交状态
系统重启,后台线程去扫描数据库里的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池里去,继续进行执行
总结
线程池的作用主要是为了提升系统的性能以及使用率。文章刚开始就提到,如果我们使用最简单的方式创建线程,如果用户量比较大,那么就会产生很多创建和销毁线程的动作,这会导致服务器在创建和销毁线程上消耗的性能可能要比处理实际业务花费的时间和性能更多。线程池就是为了解决这种这种问题而出现的。
同样思想的设计还有很多,比如数据库连接池,由于频繁的连接数据库,然而创建连接是一个很消耗性能的事情,所有数据库连接池就出现了。