JAVA里面进行多线程通信的主要方式就是 共享内存
的方式,共享内存主要的关注点有两个:可见性
和 有序性
。加上复合操作的 原子性
,可以认为JAVA的线程安全性问题主要关注点有3个(JAVA内存模型JMM解决了可见性和有序性的问题,而锁解决了原子性的问题):可见性
、有序性
、原子性
① 重排序
指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。从JAVA源码到最终实际执行的指令序列,会经历下面3种重排序(主要流程):
指令重排序分类
② 顺序一致性
顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。顺序一致性特征如下:
Java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,主要提供以下功能:
通过Unsafe类可以分配内存,可以释放内存
类中提供的3个本地方法allocateMemory(申请)、reallocateMemory(扩展)、freeMemory(销毁)分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。
可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的
挂起与恢复
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
CAS操作
是通过compareAndSwapXXX方法实现的
LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞。
LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。
park()和unpark()不会有 Thread.suspend
和 Thread.resume
所可能引发的死锁问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。
CAS(Compare And Swap
,即比较并交换),是解决多线程并行情况下使用锁造成性能损耗的一种机制。其原理是利用sun.misc.Unsafe.java
类通过JNI来调用硬件级别的原子操作来实现CAS(即CAS是借助C来调用CPU底层指令实现的)。
CAS机制=比较并交换+乐观锁机制+锁自旋
设计思想:如果内存位置
的值与 预期原值
相匹配,那么处理器会自动将该位置值更新为新值,否则处理器不做任何操作。
ReentrantLock、ReentrantReadWriteLock 都是基于 AbstractQueuedSynchronizer (AQS),而 AQS 又是基于 CAS。CAS 的全称是 Compare And Swap(比较与交换),它是一种无锁算法。synchronized和Lock都采用了悲观锁的机制,而CAS是一种乐观锁的实现。乐观锁的原理就是每次不加锁去执行某项操作,如果发生冲突则失败并重试,直到成功为止,其实本质上不算锁,所以很多地方也称之为自旋。乐观锁用到的主要机制就是CAS(Compare And Swap)。
CAS特性
CAS缺陷
AtomicStampedReference
解决AtomicReference
解决AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,如常用的ReentrantLock、Semaphore、CountDownLatch等。
AQS定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,虽然大多数开发者可能永远不会使用AQS实现自己的同步器(JUC包下提供的同步器基本足够应对日常开发),但是知道AQS的原理对于架构设计还是很有帮助的,面试还可以吹吹牛,下面是AQS的组成结构。
三部分组成:volatile int state同步状态
、Node组成的CLH队列
、ConditionObject条件变量
(包含Node组成的条件单向队列)。
状态
getState()
:返回同步状态setState(int newState)
:设置同步状态compareAndSetState(int expect, int update)
:使用CAS设置同步状态isHeldExclusively()
:当前线程是否持有资源独占资源(不响应线程中断)
tryAcquire(int arg)
:独占式获取资源,子类实现acquire(int arg)
:独占式获取资源模板tryRelease(int arg)
:独占式释放资源,子类实现release(int arg)
:独占式释放资源模板共享资源(不响应线程中断)
tryAcquireShared(int arg)
:共享式获取资源,返回值大于等于0则表示获取成功,否则获取失败,子类实现acquireShared(int arg)
:共享形获取资源模板tryReleaseShared(int arg)
:共享式释放资源,子类实现releaseShared(int arg)
:共享式释放资源模板在AQS中维护了一个同步状态变量state,getState函数获取同步状态,setState、compareAndSetState函数修改同步状态,对于AQS来说,线程同步的关键是对state的操作,可以说获取、释放资源是否成功都是由state决定的,比如state>0代表可获取资源,否则无法获取,所以state的具体语义由实现者去定义,现有的ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch定义的state语义都不一样。
CLH是AQS内部维护的FIFO(先进先出)双端双向队列(方便尾部节点插入),基于链表数据结构,当一个线程竞争资源失败,就会将等待资源的线程封装成一个Node节点,通过CAS原子操作插入队列尾部,最终不同的Node节点连接组成了一个CLH队列,所以说AQS通过CLH队列管理竞争资源的线程,个人总结CLH队列具有如下几个优点:
Node
是AQS
的内部类,每个等待资源的线程都会封装成Node
节点组成CLH
队列、等待队列,所以说Node
是非常重要的部分,理解它是理解AQS
的第一步。
nextWaiter特殊标记
Node
在CLH
队列时,nextWaiter
表示共享式或独占式标记Node
在条件队列时,nextWaiter
表示下个Node
节点指针线程获取资源失败,封装成Node
节点从CLH
队列尾部入队并阻塞线程,某线程释放资源时会把CLH
队列首部Node
节点关联的线程唤醒(此处的首部是指第二个节点,后面会细说),再次获取资源。
获取资源失败的线程需要封装成Node
节点,接着尾部入队,在AQS
中提供addWaiter
函数完成Node
节点的创建与入队。
/**
* @description: Node节点入队-CLH队列
* @param mode 标记Node.EXCLUSIVE独占式 or Node.SHARED共享式
*/
private Node addWaiter(Node mode) {
// 根据当前线程创建节点,等待状态为0
Node node = new Node(Thread.currentThread(), mode);
// 获取尾节点
Node pred = tail;
if (pred != null) {
// 如果尾节点不等于null,把当前节点的前驱节点指向尾节点
node.prev = pred;
// 通过CAS把尾节点指向当前节点
if (compareAndSetTail(pred, node)) {
// 之前尾节点的下个节点指向当前节点
pred.next = node;
return node;
}
}
// 如果添加失败或队列不存在,执行end函数
enq(node);
return node;
}
添加节点的时候,如果从CLH
队列已经存在,通过CAS
快速将当前节点添加到队列尾部,如果添加失败或队列不存在,则指向enq
函数自旋入队。
/**
* @description: 自旋cas入队
* @param node 节点
*/
private Node enq(final Node node) {
for (;;) { //循环
//获取尾节点
Node t = tail;
if (t == null) {
//如果尾节点为空,创建哨兵节点,通过cas把头节点指向哨兵节点
if (compareAndSetHead(new Node()))
//cas成功,尾节点指向哨兵节点
tail = head;
} else {
//当前节点的前驱节点设指向之前尾节点
node.prev = t;
//cas设置把尾节点指向当前节点
if (compareAndSetTail(t, node)) {
//cas成功,之前尾节点的下个节点指向当前节点
t.next = node;
return t;
}
}
}
}
通过自旋CAS
尝试往队列尾部插入节点,直到成功,自旋过程如果发现CLH
队列不存在时会初始化CLH
队列,入队过程流程如下图:
第一次循环
第二次循环
CLH
队列中的节点都是获取资源失败的线程节点,当持有资源的线程释放资源时,会将head.next
指向的线程节点唤醒(CLH
队列的第二个节点),如果唤醒的线程节点获取资源成功,线程节点清空信息设置为头部节点(新哨兵节点),原头部节点出队(原哨兵节点)acquireQueued函数中的部分代码
//1.获取前驱节点
final Node p = node.predecessor();
//如果前驱节点是首节点,获取资源(子类实现)
if (p == head && tryAcquire(arg)) {
//2.获取资源成功,设置当前节点为头节点,清空当前节点的信息,把当前节点变成哨兵节点
setHead(node);
//3.原来首节点下个节点指向为null
p.next = null; // help GC
//4.非异常状态,防止指向finally逻辑
failed = false;
//5.返回线程中断状态
return interrupted;
}
private void setHead(Node node) {
//节点设置为头部
head = node;
//清空线程
node.thread = null;
//清空前驱节点
node.prev = null;
}
只需要关注1~3
步骤即可,过程非常简单,假设获取资源成功,更换头部节点,并把头部节点的信息清除变成哨兵节点,注意这个过程是不需要使用CAS
来保证,因为只有一个线程能够成功获取到资源。
Object的wait、notify函数是配合Synchronized锁实现线程间同步协作的功能,A Q S的ConditionObject条件变量也提供这样的功能,通过ConditionObject的await和signal两类函数完成。不同于Synchronized锁,一个A Q S可以对应多个条件变量,而Synchronized只有一个。
如上图所示,ConditionObject内部维护着一个单向条件队列,不同于C H L队列,条件队列只入队执行await的线程节点,并且加入条件队列的节点,不能在C H L队列, 条件队列出队的节点,会入队到C H L队列。
当某个线程执行了ConditionObject的await函数,阻塞当前线程,线程会被封装成Node节点添加到条件队列的末端,其他线程执行ConditionObject的signal函数,会将条件队列头部线程节点转移到C H L队列参与竞争资源,具体流程如下图
AQS
采用了模板方法设计模式,提供了两类模板,一类是独占式模板,另一类是共享形模式,对应的模板函数如下
acquire
获取资源release
释放资源acquireShared
获取资源releaseShared
释放资源acquire
是个模板函数,模板流程就是线程获取共享资源,如果获取资源成功,线程直接返回,否则进入CLH
队列,直到获取资源成功为止,且整个过程忽略中断的影响,acquire
函数代码如下
acquire
函数的大致流程都清楚了,下面来分析下acquireQueued
函数,线程封装成节点后,是如何自旋阻塞等待获取资源的,代码如下:
/**
* @description: 自旋机制等待获取资源
* @param node
* @param arg
* @return: boolean
*/
final boolean acquireQueued(final Node node, int arg) {
//异常状态,默认是
boolean failed = true;
try {
//该线程是否中断过,默认否
boolean interrupted = false;
for (;;) {//自旋
//获取前驱节点
final Node p = node.predecessor();
//如果前驱节点是首节点,获取资源(子类实现)
if (p == head && tryAcquire(arg)) {
//获取资源成功,设置当前节点为头节点,清空当前节点的信息,把当前节点变成哨兵节点
setHead(node);
//原来首节点下个节点指向为null
p.next = null; // help GC
//非异常状态,防止指向finally逻辑
failed = false;
//返回线程中断状态
return interrupted;
}
/**
* 如果前驱节点不是首节点,先执行shouldParkAfterFailedAcquire函数,shouldParkAfterFailedAcquire做了三件事
* 1.如果前驱节点的等待状态是SIGNAL,返回true,执行parkAndCheckInterrupt函数,返回false
* 2.如果前驱节点的等大状态是CANCELLED,把CANCELLED节点全部移出队列(条件节点)
* 3.以上两者都不符合,更新前驱节点的等待状态为SIGNAL,返回false
*/
if (shouldParkAfterFailedAcquire(p, node) &&
//使用LockSupport类的静态方法park挂起当前线程,直到被唤醒,唤醒后检查当前线程是否被中断,返回该线程中断状态并重置中断状态
parkAndCheckInterrupt())
//该线程被中断过
interrupted = true;
}
} finally {
// 尝试获取资源失败并执行异常,取消请求,将当前节点从队列中移除
if (failed)
cancelAcquire(node);
}
}
一图胜千言,核心流程图如下:
有获取资源,自然就少不了释放资源,A Q S
中提供了release
模板函数来释放资源,模板流程就是线程释放资源成功,唤醒CLH
队列的第二个线程节点(首节点的下个节点),代码如下
/**
* @description: 独占式-释放资源模板函数
* @param arg
* @return: boolean
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {//释放资源成功,tryRelease子类实现
//获取头部线程节点
Node h = head;
if (h != null && h.waitStatus != 0) //头部线程节点不为null,并且等待状态不为0
//唤醒CHL队列第二个线程节点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//获取节点等待状态
int ws = node.waitStatus;
if (ws < 0)
//cas更新节点状态为0
compareAndSetWaitStatus(node, ws, 0);
//获取下个线程节点
Node s = node.next;
if (s == null || s.waitStatus > 0) { //如果下个节点信息异常,从尾节点循环向前获取到正常的节点为止,正常情况不会执行
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒线程节点
LockSupport.unpark(s.thread);
}
}
release
逻辑非常简单,流程图如下:
acquireShared
是个模板函数,模板流程就是线程获取共享资源,如果获取到资源,线程直接返回,否则进入CLH
队列,直到获取到资源为止,且整个过程忽略中断的影响,acquireShared
函数代码如下
/**
* @description: 共享式-获取资源模板函数
* @param arg
* @return: void
*/
public final void acquireShared(int arg) {
/**
* 1.负数表示失败
* 2.0表示成功,但没有剩余可用资源
* 3.正数表示成功且有剩余资源
*/
if (tryAcquireShared(arg) < 0) //获取资源失败,tryAcquireShared子类实现
//自旋阻塞等待获取资源
doAcquireShared(arg);
}
doAcquireShared
函数与独占式的acquireQueued
函数逻辑基本一致,唯一的区别就是下图红框部分
>0
,代表还有资源可获取,所以需要做后续线程节点的唤醒AQS
中提供了releaseShared
模板函数来释放资源,模板流程就是线程释放资源成功,唤醒CHL队列的第二个线程节点(首节点的下个节点),代码如下
/**
* @description: 共享式-释放资源模板函数
* @param arg
* @return: boolean
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//释放资源成功,tryReleaseShared子类实现
//唤醒后继节点
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
//获取头节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {//如果头节点等待状态为SIGNAL
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//更新头节点等待状态为0
continue; // loop to recheck cases
//唤醒头节点下个线程节点
unparkSuccessor(h);
}
//如果后继节点暂时不需要被唤醒,更新头节点等待状态为PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
与独占式释放资源区别不大,都是唤醒头节点的下个节点。
什么是AQS?
AQS
的全称是 AbstractQueuedSynchronizer
,即抽象队列同步器
。是Java并发工具的基础,采用乐观锁,通过CAS与自旋轻量级的获取锁。维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。很多JUC包,比如ReentrantLock、Semaphore、CountDownLatch等并发类均是继承AQS,通过AQS的模板方法,来实现的。
核心思想
工作原理
AQS = 同步状态(volatile int state)
+ 同步队列(即等待队列,FIFO的CLH队列)
+ 条件队列(ConditionObject)
volatile
保证并发读,CAS
保证并发写实现原理
volatile
的int
类型的成员变量state
表示同步状态state
的修改AQS定义两种资源共享方式。无论是独占锁还是共享锁,本质上都是对AQS内部的一个变量state
的获取。state
是一个原子的int变量,用来表示锁状态、资源数等。
① 独占锁(Exclusive
)模式:只能被一个线程获取到(Reentrantlock
)。
② 共享锁(Share
)模式:可以被多个线程同时获取(Semaphore/CountDownLatch/ReadWriteLock
)。
提供volatile
变量state
,用于同步线程之间的共享状态。通过 CAS
和 volatile
保证其原子性和可见性。核心要点:
volatile
修饰,保证多线程中的可见性getState()
和 setState()
方法采用final修饰,限制AQS的子类重写它们两compareAndSetState()
方法采用乐观锁思想的CAS算法,也是采用final修饰的,不允许子类重写state应用案例:
案例 | 描述 |
---|---|
Semaphore |
使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数 |
CountDownLatch |
使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过 |
ReentrantReadWriteLock |
使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数 |
ThreadPoolExecutor |
Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease) |
ReentrantLock |
使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理 |
注意
同步队列是用来管理多个线程的休眠与唤醒。
同步队列依赖一个双向链表(CHL)来完成同步状态的管理,当前线程获取同步状态失败后,同步器会将线程构建成一个节点,并将其加入同步队列中。通过signal
或signalAll
将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)
如果没有锁竞争,线程可以直接获取到锁,就不会进入同步队列。即没有锁竞争时,同步队列(syncQueue)是空的,当存在锁竞争时,线程会进入到同步队列中。一旦进入到同步队列中,就会有线程切换。标准的 CHL 无锁队列是单向链表,同步队列(syncQueue) 在 CHL 基础上做了改进:
同步队列是双向链表。和二叉树一样,双向链表目前也没有无锁算法的实现。双向链表需要同时设置前驱和后继结点,这两次操作只能保证一个是原子性的
node.pre一定可以遍历所有结点,是线程安全的。而后继结点 node.next 则是线程不安全的。也就是说,node.pre 一定可以遍历整个链表,而 node.next 则不一定
条件队列是类似wait与signal作用,实现在使用锁时对线程管理。且由于实现了Condition,对线程的管理可更加细化。
当线程存在于同步队列的头结点时,调用 await
方法进行阻塞(从同步队列转化到条件队列)。Condition条件队列(waitQueue
)要比Lock同步队列(syncQueue
)简单很多,最重要的原因是 waitQueue
的操作都是在获取锁的线程中执行,不存在数据竞争的问题。
ConditionObject重要的方法说明:
常见问题
问题1:state为什么要提供setState和compareAndSetState两种修改状态的方法?
这个问题,关键是修改状态时是否存在数据竞争,如果有则必须使用 compareAndSetState。
问题2:AQS为什么选择node.prev前驱结点的原子性,而node.next后继结点则是辅助结点?
问题3:AQS明知道node.next有可见性问题,为什么还要设计成双向链表?
唤醒同步线程时,如果有后继结点,那么时间复杂为 O(1)。否则只能只反向遍历,时间复杂度为 O(n)。以下两种情况,则认为 node.next 不可靠,需要从 tail 反向遍历。
Condition的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
volatile特性与原理
使用场景
状态标记量
DCL(Double Check Lock)
CPU缓存
CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多。按照读取顺序与CPU结合的紧密程度,CPU缓存可分为:
volatile的特性
函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型。使用@FunctionalInterface注解修饰的类,编译器会检测该类是否只有一个抽象方法或接口,否则,会报错。可以有多个默认方法,静态方法。JAVA8自带的常用函数式接口:
函数接口 | 抽象方法 | 功能 | 参数 | 返回类型 | 示例 |
---|---|---|---|---|---|
Predicate | test(T t) | 判断真假 | T | boolean | 身高大于185cm吗? |
Consumer | accept(T t) | 消费消息 | T | void | 输出一个值 |
Function | R apply(T t) | 将T映射为R(转换功能) | T | R | 取student对象的名字 |
Supplier | T get() | 生产消息 | None | T | 工厂方法 |
UnaryOperator | T apply(T t) | 一元操作 | T | T | 逻辑非(!) |
BinaryOperator | apply(T t, U u) | 二元操作 | (T,T) | (T) | 求两个数的乘积(*) |
将流转换为集合。有toList()、toSet()、toMap()等,及早求值。
public class TestCase {
public static void main(String[] args) {
List<Student> studentList = Stream.of(new Student("路飞", 22, 175),
new Student("红发", 40, 180), new Student("白胡子", 50, 185)).collect(Collectors.toList());
System.out.println(studentList);
}
}
// 输出结果
// [Student{name='路飞', age=22, stature=175, specialities=null},
// Student{name='红发', age=40, stature=180, specialities=null},
// Student{name='白胡子', age=50, stature=185, specialities=null}]
顾名思义,起过滤筛选的作用。内部就是Predicate接口。惰性求值。
public class TestCase {
public static void main(String[] args) {
List<Student> students = new ArrayList<>(3);
students.add(new Student("路飞", 22, 175));
students.add(new Student("红发", 40, 180));
students.add(new Student("白胡子", 50, 185));
List<Student> list = students.stream()
.filter(stu -> stu.getStature() < 180)
.collect(Collectors.toList());
System.out.println(list);
}
}
// 输出结果
// [Student{name='路飞', age=22, stature=175, specialities=null}]
转换功能,内部就是Function接口。惰性求值。
public class TestCase {
public static void main(String[] args) {
List<Student> students = new ArrayList<>(3);
students.add(new Student("路飞", 22, 175));
students.add(new Student("红发", 40, 180));
students.add(new Student("白胡子", 50, 185));
List<String> names = students.stream().map(student -> student.getName())
.collect(Collectors.toList());
System.out.println(names);
}
}
// 输出结果
// [路飞, 红发, 白胡子]
例子中将student对象转换为String对象,获取student的名字。
将多个Stream合并为一个Stream。惰性求值。
public class TestCase {
public static void main(String[] args) {
List<Student> students = new ArrayList<>(3);
students.add(new Student("路飞", 22, 175));
students.add(new Student("红发", 40, 180));
students.add(new Student("白胡子", 50, 185));
List<Student> studentList = Stream.of(students,
asList(new Student("艾斯", 25, 183),
new Student("雷利", 48, 176)))
.flatMap(students1 -> students1.stream()).collect(Collectors.toList());
System.out.println(studentList);
}
}
// 输出结果
// [Student{name='路飞', age=22, stature=175, specialities=null},
// Student{name='红发', age=40, stature=180, specialities=null},
// Student{name='白胡子', age=50, stature=185, specialities=null},
// Student{name='艾斯', age=25, stature=183, specialities=null},
// Student{name='雷利', age=48, stature=176, specialities=null}]
调用Stream.of的静态方法将两个list转换为Stream,再通过flatMap将两个流合并为一个。
我们经常会在集合中求最大或最小值,使用流就很方便。及早求值。
public class TestCase {
public static void main(String[] args) {
List<Student> students = new ArrayList<>(3);
students.add(new Student("路飞", 22, 175));
students.add(new Student("红发", 40, 180));
students.add(new Student("白胡子", 50, 185));
Optional<Student> max = students.stream()
.max(Comparator.comparing(stu -> stu.getAge()));
Optional<Student> min = students.stream()
.min(Comparator.comparing(stu -> stu.getAge()));
//判断是否有值
if (max.isPresent()) {
System.out.println(max.get());
}
if (min.isPresent()) {
System.out.println(min.get());
}
}
}
// 输出结果
// Student{name='白胡子', age=50, stature=185, specialities=null}
// Student{name='路飞', age=22, stature=175, specialities=null}
max、min接收一个Comparator(例子中使用java8自带的静态函数,只需要传进需要比较值即可),并且返回一个Optional对象,该对象是java8新增的类,专门为了防止null引发的空指针异常。可以使用max.isPresent()判断是否有值;可以使用max.orElse(new Student()),当值为null时就使用给定值;也可以使用max.orElseGet(() -> new Student());这需要传入一个Supplier的lambda表达式。
统计功能,一般都是结合filter使用,因为先筛选出我们需要的再统计即可。及早求值。
public class TestCase {
public static void main(String[] args) {
List<Student> students = new ArrayList<>(3);
students.add(new Student("路飞", 22, 175));
students.add(new Student("红发", 40, 180));
students.add(new Student("白胡子", 50, 185));
long count = students.stream().filter(s1 -> s1.getAge() < 45).count();
System.out.println("年龄小于45岁的人数是:" + count);
}
}
// 输出结果
// 年龄小于45岁的人数是:2
reduce 操作可以实现从一组值中生成一个值。在上述例子中用到的 count 、 min 和 max 方法,因为常用而被纳入标准库中。事实上,这些方法都是 reduce 操作。及早求值。
public class TestCase {
public static void main(String[] args) {
Integer reduce = Stream.of(1, 2, 3, 4).reduce(0, (acc, x) -> acc+ x);
System.out.println(reduce);
}
}
// 输出结果:10
我们看得reduce接收了一个初始值为0的累加器,依次取出值与累加器相加,最后累加器的值就是最终的结果。
**收集器,一种通用的、从流生成复杂值的结构。**只要将它传给 collect 方法,所有的流就都可以使用它了。标准类库已经提供了一些有用的收集器,以下示例代码中的收集器都是从 java.util.stream.Collectors 类中静态导入的。
public class CollectorsTest {
public static void main(String[] args) {
List<Student> students1 = new ArrayList<>(3);
students1.add(new Student("路飞", 23, 175));
students1.add(new Student("红发", 40, 180));
students1.add(new Student("白胡子", 50, 185));
OutstandingClass ostClass1 = new OutstandingClass("一班", students1);
//复制students1,并移除一个学生
List<Student> students2 = new ArrayList<>(students1);
students2.remove(1);
OutstandingClass ostClass2 = new OutstandingClass("二班", students2);
//将ostClass1、ostClass2转换为Stream
Stream<OutstandingClass> classStream = Stream.of(ostClass1, ostClass2);
OutstandingClass outstandingClass = biggestGroup(classStream);
System.out.println("人数最多的班级是:" + outstandingClass.getName());
System.out.println("一班平均年龄是:" + averageNumberOfStudent(students1));
}
/**
* 获取人数最多的班级
*/
private static OutstandingClass biggestGroup(Stream<OutstandingClass> outstandingClasses) {
return outstandingClasses.collect(
maxBy(comparing(ostClass -> ostClass.getStudents().size())))
.orElseGet(OutstandingClass::new);
}
/**
* 计算平均年龄
*/
private static double averageNumberOfStudent(List<Student> students) {
return students.stream().collect(averagingInt(Student::getAge));
}
}
// 输出结果
// 人数最多的班级是:一班
// 一班平均年龄是:37.666666666666664
maxBy或者minBy就是求最大值与最小值。
常用的流操作是将其分解成两个集合,Collectors.partitioningBy帮我们实现了,接收一个Predicate函数式接口。
将示例学生分为会唱歌与不会唱歌的两个集合。
public class PartitioningByTest {
public static void main(String[] args) {
// 省略List students的初始化
Map<Boolean, List<Student>> listMap = students.stream().collect(
Collectors.partitioningBy(student -> student.getSpecialities().
contains(SpecialityEnum.SING)));
}
}
数据分组是一种更自然的分割数据操作,与将数据分成 ture 和 false 两部分不同,可以使用任意值对数据分组。Collectors.groupingBy接收一个Function做转换。
**如图,使用groupingBy将根据进行分组为圆形一组,三角形一组,正方形一组。**例子:根据学生第一个特长进行分组
public class GroupingByTest {
public static void main(String[] args) {
//省略List students的初始化
Map<SpecialityEnum, List<Student>> listMap =
students.stream().collect(
Collectors.groupingBy(student -> student.getSpecialities().get(0)));
}
}
Collectors.groupingBy与SQL 中的 group by 操作是一样的。
如果将所有学生的名字拼接起来,怎么做呢?通常只能创建一个StringBuilder,循环拼接。使用Stream,使用Collectors.joining()简单容易。
public class JoiningTest {
public static void main(String[] args) {
List<Student> students = new ArrayList<>(3);
students.add(new Student("路飞", 22, 175));
students.add(new Student("红发", 40, 180));
students.add(new Student("白胡子", 50, 185));
String names = students.stream()
.map(Student::getName).collect(Collectors.joining(",","[","]"));
System.out.println(names);
}
}
//输出结果
//[路飞,红发,白胡子]
joining接收三个参数,第一个是分界符,第二个是前缀符,第三个是结束符。也可以不传入参数Collectors.joining(),这样就是直接拼接。
Striped64的设计思路是在竞争激烈的时候尽量分散竞争。
Striping(条带化)
大多数磁盘系统都对访问次数(每秒的 I/O 操作,IOPS)和数据传输率(每秒传输的数据量,TPS)有限制。当达到这些限制时,后续需要访问磁盘的进程就需要等待,这就是所谓的磁盘冲突。当多个进程同时访问一个磁盘时,可能会出现磁盘冲突。因此,避免磁盘冲突是优化 I/O 性能的一个重要目标。
条带(strip)是把连续的数据分割成相同大小的数据块,把每段数据分别写入到阵列中的不同磁盘上的方法。使用条带化技术使得多个进程同时访问数据的多个不同部分而不会造成磁盘冲突,而且在需要对这种数据进行顺序访问的时候可以获得最大程度上的 I/O 并行能力,从而获得非常好的性能。
Striped64设计
Striped64通过维护一个原子更新Cell表和一个base字段,并使用每个线程的探针字段作为哈希码映射到表的指定Cell。当竞争激烈时,将多线程的更新分散到不同Cell进行,有效降低了高并发下CAS更新的竞争,从而最大限度地提高了Striped64的吞吐量。Striped64为实现高吞吐量的并发计数组件奠定了基础,其中LongAdder就是基于Striped64实现,此外Java8中ConcurrentHashMap实现的并发计数功能也是基于Striped64的设计理念,还有hystrix、guava等实现的并发计数组件也离不开Striped64。
LongAdder是JDK1.8开始出现的,所提供的API基本上可替换掉原先AtomicLong。LongAdder所使用思想就是热点分离,这点可以类比一下ConcurrentHashMap的设计思想。就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度。如下图所示:
LonAdder和AtomicLong性能测试对比:
LongAdder就是基于Striped64实现,用于并发计数时,若不存在竞争或竞争比较低时,LongAdder具有和AtomicLong差不多的效率。但是,高并发环境下,竞争比较严重时,LongAdder的cells表发挥作用,将并发更新分散到不同Cell进行,有效降低了CAS更新的竞争,从而极大提高了LongAdder的并发计数能力。因此,高并发场景下,要实现吞吐量更高的计数器,推荐使用LongAdder。
Semaphore是一个计数信号量,它的本质是一个"共享锁"。信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
数据结构
CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
比较CountDownLatch和CyclicBarrier
数据结构
CyclicBarrier是包含了"ReentrantLock对象lock"和"Condition对象trip",它是通过独占锁实现的。下面通过源码去分析到底是如何实现的。
CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
CountDownLatch和CyclicBarrier的区别
数据结构
CountDownLatch的数据结构很简单,它是通过"共享锁"实现的。它包含了sync对象,sync是Sync类型。Sync是实例类,它继承于AQS。
CompletableFuture是Java 8新增的一个类,用于异步编程,继承了Future和CompletionStage。Future主要具备对请求结果独立处理的功能,CompletionStage用于实现流式处理,实现异步请求的各个阶段组合或链式处理,因此CompletableFuture能实现整个异步调用接口的扁平化和流式处理,解决原有Future处理一系列链式异步请求时的复杂编码:
Future的局限性
Future 的结果在非阻塞的情况下,不能执行更进一步的操作
我们知道,使用Future时只能通过isDone()方法判断任务是否完成,或者通过get()方法阻塞线程等待结果返回,它不能非阻塞的情况下,执行更进一步的操作
不能组合多个Future的结果
假设你有多个Future异步任务,你希望最快的任务执行完时,或者所有任务都执行完后,进行一些其他操作
多个Future不能组成链式调用
当异步任务之间有依赖关系时,Future不能将一个任务的结果传给另一个异步任务,多个Future无法创建链式的工作流
没有异常处理
注意事项
CompletableFuture默认线程池是否满足使用
前面提到创建CompletableFuture异步任务的静态方法runAsync和supplyAsync等,可以指定使用的线程池,不指定则用CompletableFuture的默认线程池:
private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
可以看到,CompletableFuture默认线程池是调用ForkJoinPool的commonPool()方法创建,这个默认线程池的核心线程数量根据CPU核数而定,公式为Runtime.getRuntime().availableProcessors() - 1
,以4核双槽CPU为例,核心线程数量就是4*2-1=7
个。这样的设置满足CPU密集型的应用,但对于业务都是IO密集型的应用来说,是有风险的,当qps较高时,线程数量可能就设的太少了,会导致线上故障。所以可以根据业务情况自定义线程池使用。
get设置超时时间不能串行get,不然会导致接口延时线程数量\*超时时间
① 创建异步任务
通常可以使用下面几个CompletableFuture的静态方法创建一个异步任务
// 创建无返回值的异步任务
public static CompletableFuture<Void> runAsync(Runnable runnable);
// 无返回值,可指定线程池(默认使用ForkJoinPool.commonPool)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
// 创建有返回值的异步任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
// 有返回值,可指定线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
使用示例:
Executor executor = Executors.newFixedThreadPool(10);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
//do something
}, executor);
int poiId = 111;
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
PoiDTO poi = poiService.loadById(poiId);
return poi.getName();
});
// Block and get the result of the Future
String poiName = future.get();
② 使用回调方法
通过future.get()
方法获取异步任务的结果,还是会阻塞的等待任务完成
CompletableFuture提供了几个回调方法,可以不阻塞主线程,在异步任务完成后自动执行回调方法中的代码
// 无参数、无返回值
public CompletableFuture<Void> thenRun(Runnable runnable);
// 接受参数,无返回值
public CompletableFuture<Void> thenAccept(Consumer<? super T> action);
// 接受参数T,有返回值U
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn);
使用示例:
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenRun(() -> System.out.println("do other things. 比如异步打印日志或发送消息"));
// 如果只想在一个CompletableFuture任务执行完后,进行一些后续的处理,不需要返回值,那么可以用thenRun回调方法来完成。
// 如果主线程不依赖thenRun中的代码执行完成,也不需要使用get()方法阻塞主线程。
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenAccept((s) -> System.out.println(s + " world"));
// 输出:Hello world
// 回调方法希望使用异步任务的结果,并不需要返回值,那么可以使用thenAccept方法
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
PoiDTO poi = poiService.loadById(poiId);
return poi.getMainCategory();
}).thenApply((s) -> isMainPoi(s)); // boolean isMainPoi(int poiId);
future.get();
// 希望将异步任务的结果做进一步处理,并需要返回值,则使用thenApply方法。
// 如果主线程要获取回调方法的返回,还是要用get()方法阻塞得到
③ 组合两个异步任务
// thenCompose方法中的异步任务依赖调用该方法的异步任务
public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
// 用于两个独立的异步任务都完成的时候
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn);
使用示例:
CompletableFuture<List<Integer>> poiFuture = CompletableFuture.supplyAsync(
() -> poiService.queryPoiIds(cityId, poiId)
);
// 第二个任务是返回CompletableFuture的异步方法
CompletableFuture<List<DealGroupDTO>> getDeal(List<Integer> poiIds){
return CompletableFuture.supplyAsync(() -> poiService.queryPoiIds(poiIds));
}
// thenCompose
CompletableFuture<List<DealGroupDTO>> resultFuture = poiFuture.thenCompose(poiIds -> getDeal(poiIds));
resultFuture.get();
thenCompose和thenApply的功能类似,两者区别在于thenCompose接受一个返回CompletableFuture
的Function,当想从回调方法返回的CompletableFuture
中直接获取结果U时,就用thenCompose。如果使用thenApply,返回结果resultFuture的类型是CompletableFuture
,而不是CompletableFuture
>
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenCombine(CompletableFuture.supplyAsync(() -> "world"), (s1, s2) -> s1 + s2);
// future.get()
④ 组合多个CompletableFuture
当需要多个异步任务都完成时,再进行后续处理,可以使用allOf方法:
CompletableFuture<Void> poiIDTOFuture = CompletableFuture
.supplyAsync(() -> poiService.loadPoi(poiId))
.thenAccept(poi -> {
model.setModelTitle(poi.getShopName());
// do more thing
});
CompletableFuture<Void> productFuture = CompletableFuture
.supplyAsync(() -> productService.findAllByPoiIdOrderByUpdateTimeDesc(poiId))
.thenAccept(list -> {
model.setDefaultCount(list.size());
model.setMoreDesc("more");
});
// future3等更多异步任务,这里就不一一写出来了
// allOf组合所有异步任务,并使用join获取结果
CompletableFuture.allOf(poiIDTOFuture, productFuture, future3, ...).join();
该方法挺适合C端的业务,通过poiId异步的从多个服务拿门店信息,然后组装成自己需要的模型,最后所有门店信息都填充完后返回。这里使用了join方法获取结果,它和get方法一样阻塞的等待任务完成。多个异步任务有任意一个完成时就返回结果,可以使用anyOf方法:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 2";
});
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
return "Result of Future 3";
});
CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2, future3);
System.out.println(anyOfFuture.get()); // Result of Future 2
⑤ 异常处理
Integer age = -1;
CompletableFuture<Void> maturityFuture = CompletableFuture.supplyAsync(() -> {
if(age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if(age > 18) {
return "Adult";
} else {
return "Child";
}
}).exceptionally(ex -> {
System.out.println("Oops! We have an exception - " + ex.getMessage());
return "Unknown!";
}).thenAccept(s -> System.out.print(s));
// Unkown!
exceptionally方法可以处理异步任务的异常,在出现异常时,给异步任务链一个从错误中恢复的机会,可以在这里记录异常或返回一个默认值。使用handler方法也可以处理异常,并且无论是否发生异常它都会被调用:
Integer age = -1;
CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
if(age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if(age > 18) {
return "Adult";
} else {
return "Child";
}
}).handle((res, ex) -> {
if(ex != null) {
System.out.println("Oops! We have an exception - " + ex.getMessage());
return "Unknown!";
}
return res;
});
⑥ 分片处理
分片和并行处理:分片借助stream实现,然后通过CompletableFuture实现并行执行,最后做数据聚合(其实也是stream的方法)。CompletableFuture并不提供单独的分片api,但可以借助stream的分片聚合功能实现。举个例子:
// 请求商品数量过多时,做分批异步处理
List<List<Long>> skuBaseIdsList = ListUtils.partition(skuIdList, 10);//分片
// 并行
List<CompletableFuture<List<SkuSales>>> futureList = Lists.newArrayList();
for (List<Long> skuId : skuBaseIdsList) {
CompletableFuture<List<SkuSales>> tmpFuture = getSkuSales(skuId);
futureList.add(tmpFuture);
}
// 聚合
futureList.stream().map(CompletalbleFuture::join).collent(Collectors.toList());
⑦ 应用案例
首先还是需要先完成分工方案,在下面的程序中,我们分了3个任务:
// 任务1:洗水壶->烧开水
CompletableFuture f1 =
CompletableFuture.runAsync(()->{
System.out.println("T1:洗水壶...");
sleep(1, TimeUnit.SECONDS);
System.out.println("T1:烧开水...");
sleep(15, TimeUnit.SECONDS);
});
// 任务2:洗茶壶->洗茶杯->拿茶叶
CompletableFuture f2 =
CompletableFuture.supplyAsync(()->{
System.out.println("T2:洗茶壶...");
sleep(1, TimeUnit.SECONDS);
System.out.println("T2:洗茶杯...");
sleep(2, TimeUnit.SECONDS);
System.out.println("T2:拿茶叶...");
sleep(1, TimeUnit.SECONDS);
return "龙井";
});
// 任务3:任务1和任务2完成后执行:泡茶
CompletableFuture f3 =
f1.thenCombine(f2, (__, tf)->{
System.out.println("T3:拿到茶叶:" + tf);
System.out.println("T3:泡茶...");
return "上茶:" + tf;
});
// 等待任务3执行结果
System.out.println(f3.join());
void sleep(int t, TimeUnit u) {
try {
u.sleep(t);
}catch(InterruptedException e){}
}
// 一次执行结果:
T1:洗水壶...
T2:洗茶壶...
T1:烧开水...
T2:洗茶杯...
T2:拿茶叶...
T3:拿到茶叶:龙井
T3:泡茶...
上茶:龙井
ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。
它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。
Copy-On-Write是什么?
顾名思义,在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行写
操作,而是将内存拷贝一份,在新的内存中进行写
操作,写
完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉。
CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口,支持高并发。和ArrayList一样,它是个可变数组,但是和ArrayList不同的时,它具有以下特性:
CopyOnWriteArrayList数据结构
CopyOnWriteArrayList原理
下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。
CopyOnWriteArrayList的“动态数组”机制
它内部有个“volatile数组”(array)来保持数据。
在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因。CopyOnWriteArrayList就是通过这种方式实现的动态数组,不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很
低。但是单单只是进行遍历查找的话,效率比较高
CopyOnWriteArrayList的“线程安全”机制
是通过volatile和互斥锁来实现的。
**HashSet 基于 HashMap,底层是通过 HashMap 的API来实现的。**哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法,如果 equals 结果为 true ,HashSet 就视为同一个元素;如果 equals 为 false 就不是同一个元素。hashcode相同,equals不相等,则使用链表存储。
底层通过TreeMap来实现,非线程安全,具有排序功能(自然排序(默认)和自定义排序)。它继承AbstractSet,实现NavigableSet(搜索功能), Cloneable(克隆), Serializable(序列化,可用hessian协议来传输)接口。
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。
CopyOnWriteArraySet是线程安全的无序的集合,可以将它理解成线程安全的HashSet。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。
CopyOnWriteArraySet特性
数据结构
ConcurrentSkipListSet是线程安全的有序的集合,适用于高并发的场景。ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。但是,第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的。
数据结构
工作原理
HashMap(数组+链表+红黑树)。HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。我们用下面这张图来介绍HashMap 的结构。
hashCode 是定位的,存储位置;equals是定性的,比较两者是否相等。
put()
get()
大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
HashMap具有如下特性:
共存
的key
,会强制性的判别出个高低,目的是为了决定向左还是向右放置数据root
节点和链表的头节点 跟table[i]
节点融合成一个RBT
类似,找个合适的节点来填充已删除的节点root
节点不一定
跟table[i]
也就是链表的头节点是同一个,三者同步是靠MoveRootToFront
实现的。而HashIterator.remove()
会在调用removeNode
的时候movable=false
TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException 类型的异常。
Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
如何实现LRUCache?
LRU(Least Recently Used)最近最少使用算法。LruCache就是利用 LinkedHashMap 的一个特性( accessOrder=true
基于访问顺序 )再加上对 LinkedHashMap 的数据操作上锁
实现的缓存策略。
LruCache
是通过LinkedHashMap
构造方法的第三个参数的accessOrder=true
实现了LinkedHashMap
的数据排序基于访问顺序 (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除,从而实现了LRU数据缓存机制
然后在每次 LruCache.get(K key)
方法里都会调用LinkedHashMap.get(Object key)
如上述设置了 accessOrder=true
后,每次 LinkedHashMap.get(Object key)
都会进行 LinkedHashMap.makeTail(LinkedEntry
LinkedHashMap 是双向循环链表
,然后每次 LruCache.get -> LinkedHashMap.get
的数据就被放到最末尾了
在 put 和 trimToSize
的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部)
LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了)
LruCache 自身并没有释放内存,将LinkedHashMap的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存
覆写entryRemoved方法能知道 LruCache 数据移除时是否发生了冲突,也可以去手动释放资源
maxSize 和 sizeOf(K key, V value) 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
/**
* LruCache的构造方法:需要传入最大缓存个数
*/
public LruCache(int maxSize) {
...
this.maxSize = maxSize;
/*
* 初始化LinkedHashMap
* 第一个参数:initialCapacity,初始大小
* 第二个参数:loadFactor,负载因子=0.75f,即到75%容量的时候就会扩容
* 第三个参数:①accessOrder=true基于访问顺序排序②accessOrder=false基于插入顺序排序
*/
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
下述的 get 方法表面并没有看出哪里有实现了 LRU 的缓存策略。主要在 mapValue = map.get(key);里,调用了 LinkedHashMap 的 get 方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true。
/**
* 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
* 如果值返回了,那么它将被移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回 null。
*/
public final V get(K key) {
...
V mapValue;
synchronized (this) {
// 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序
mapValue = map.get(key);
// 计算 命中次数
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 计算 丢失次数
missCount++;
}
/*
* 官方解释:
* 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
* 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/***************************
* 不覆写create方法走不到下面 *
***************************/
/*
* 正常情况走不到这里
* 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
* 因为默认的 create(K key) 逻辑为null
*/
synchronized (this) {
// 记录 create 的次数
createCount++;
// 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value,即有冲突。
if (mapValue != null) {
/*
* 有冲突
* 所以 撤销 刚才的 操作
* 将 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到键值对,计算出在容量中的相对长度,然后加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判断出了 将要放入的值发生冲突
if (mapValue != null) {
/*
* 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 进行了 size += 操作 所以这里要重整长度
trimToSize(maxSize);
return createdValue;
}
}
主要看if (accessOrder)
逻辑即可,如果accessOrder=true
那么每次get都会执行N次 makeTail(LinkedEntry
。多了一步mainTail动作,把获取的数据,移到双向链表的尾部tail。
/**
* Returns the value of the mapping with the specified key.
*
* @param key the key.
* @return the value of the mapping with the specified key, or {@code null} if no mapping for the specified key is found.
*/
@Override
public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e); //把访问的节点迁移到链表的尾部
return e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { //从数组中获取
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e); //把访问的节点迁移到链表尾部。
return e.value;
}
}
return null;
}
/**
* Relinks the given entry to the tail of the list. Under access ordering,
* this method is invoked whenever the value of a pre-existing entry is
* read by Map.get or modified by Map.put.
*/
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e 在链表中删除该节点e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail 在尾部添加
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
public final V put(K key, V value) {
...
synchronized (this) {
...
// 拿到键值对,计算出在容量中的相对长度,然后加上
size += safeSizeOf(key, value);
...
}
...
trimToSize(maxSize);
return previous;
}
注意:
put
开始的时候确实是把值放入 LinkedHashMap
了,不管超不超过你设定的缓存容量safeSizeOf
方法计算 此次添加数据的容量是多少,并且加到 size
里safeSizeOf
就要讲到 sizeOf(K key, V value)
会计算出此次添加数据的大小put
要结束时,进行了 trimToSize
才判断 size
是否 大于 maxSize
然后进行最近很少访问数据的移除会判断之前 size
是否大于 maxSize
。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由 makeTail
图已知,最近经常访问的数据在最末尾。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。
public void trimToSize(int maxSize) {
/*
* 这是一个死循环,
* 1.只有 扩容 的情况下能立即跳出
* 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
*/
while (true) {
K key;
V value;
synchronized (this) {
// 在重新调整容量大小前,本身容量就为空的话,会出异常的。
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
// 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
// 拿到键值对,计算出在容量中的相对长度,然后减去。
size -= safeSizeOf(key, value);
// 添加一次收回次数
evictionCount++;
}
/*
* 将最后一次删除的最少访问数据回调出去
*/
entryRemoved(true, key, value, null);
}
}
ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);它继承于AbstractMap类,并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的,它支持并发。
Segment 段
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个segment。
线程安全(Segment继承ReentrantLock加锁)
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
并行度(默认16)
concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
Java8 实现 (引入了红黑树)
Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。
ConcurrentHashMap并发
ConcurrentHashMap在1.7和1.8的区别
整体结构
Segment + HashEntry + Unsafe
Synchronized + CAS + Node + Unsafe
put()
1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起
rehash值的高位
和 segments数组大小-1
相与得到在 segments中的位置key的rehash值
和 table数组大小-1
相与得到在table中的位置1.8:由于移除了Segment,可根据 rehash值
直接定位到桶,拿到table[i]的 首节点first
后进行判断:
null
,通过 CAS
的方式把 value put进去非null
,并且 first.hash == -1
,说明其他线程在扩容,参与一起扩容非null
,并且 first.hash != -1
,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。get()
resize()
new个2倍数组
2.遍历old数组节点搬去新数组
),避免了HashMap在1.7中扩容时死循环的问题,保证线程安全size()
Count
的和baseCount
变量来记录ConcurrentHashMap 当前 节点的个数
。
baseCount
CELLSBUSY
置1,成功则可以把 baseCount变化的次数
暂存到一个数组 counterCells
里,后续数组 counterCells
的值会加到 baseCount
中CELLSBUSY
置1失败又会反复进行CASbaseCount
和 CAScounterCells
数组ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap)。它继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发。
数据结构
ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。关于跳表(Skip List),它是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。
Queue(队列)是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,只允许在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。
每个元素总是从队列的rear端进入队列,然后等待该元素之前的所有元素出队之后,当前元素才能出对,遵循先进先出(FIFO)原则。下面是Queue类的继承关系图:
图中我们可以看到,最上层是Collection接口,Queue满足集合类的所有方法:
Queue:队列的上层接口,提供了插入、删除、获取元素这3种类型的方法,而且对每一种类型都提供了两种方式。
插入方法
add和offer作为插入方法的唯一不同就在于队列满了之后的处理方式。add抛出异常,而offer返回false。
删除和获取元素方法(和插入方法类似)
如果队列是空,remove和element方法会抛出异常,而poll和peek返回null。当然,Queue只是单向队列,为了提供更强大的功能,JDK在1.6的时候新增了一个双向队列Deque,用来实现更灵活的队列操作。
Deque在Queue的基础上,增加了以下几个方法:
其实很多方法的效果都是一样的,只不过名字不同。比如Deque为了实现Stack的语义,定义了push和pop两个方法。
BlockingQueue(阻塞队列),在Queue的基础上实现了阻塞等待的功能。它是JDK 1.5中加入的接口,它是指这样的一个队列:当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素但队列为空时,消费者会被阻塞。
先给出BlockingQueue新增的方法:
BlockingQueue最重要的也就是关于阻塞等待的几个方法,而这几个方法正好可以用来实现生产-消费的模型。
从图中我们可以知道实现了BlockingQueue的类有以下几个:
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。
特性
数据结构 —— 数组
通常,队列的实现方式有数组和链表两种方式。对于数组这种实现方式来说,我们可以通过维护一个队尾指针,使得在入队的时候可以在O(1)的时间内完成。但是对于出队操作,在删除队头元素之后,必须将数组中的所有元素都往前移动一个位置,这个操作的复杂度达到了O(n),效果并不是很好。如下图所示:
数据结构 —— 环型结构
为了解决这个问题,我们可以使用另外一种逻辑结构来处理数组中各个位置之间的关系。假设现在我们有一个数组A[1…n],我们可以把它想象成一个环型结构,即A[n]之后是A[1],相信了解过一致性Hash算法的应该很容易能够理解。如下图所示:
我们可以使用两个指针,分别维护队头和队尾两个位置,使入队和出队操作都可以在O(1)的时间内完成。当然,这个环形结构只是逻辑上的结构,实际的物理结构还是一个普通的数据。
入队方法
ArrayBlockingQueue 提供了多种入队操作的实现来满足不同情况下的需求,入队操作有如下几种:
boolean add(E e):其调用的是父类,即AbstractQueue的add方法,实际上调用的就是offer方法
void put(E e):在count等于items长度时,一直等待,直到被其他线程唤醒。唤醒后调用enqueue方法放入队列
boolean offer(E e):offer方法在队列满了的时候返回false,否则调用enqueue方法插入元素,并返回true。
enqueue:方法首先把元素放在items的putIndex位置,接着判断在putIndex+1等于队列的长度时把putIndex设置为0,也就是上面提到的圆环的index操作。最后唤醒等待获取元素的线程。
boolean offer(E e, long timeout, TimeUnit unit):只是在offer(E e)的基础上增加了超时时间的概念
出队方法
ArrayBlockingQueue提供了多种出队操作的实现来满足不同情况下的需求,如下:
E poll():poll方法是非阻塞方法,如果队列没有元素返回null,否则调用dequeue把队首的元素出队列。
dequeue:会根据takeIndex获取到该位置的元素,并把该位置置为null,接着利用圆环原理,在takeIndex到达列表长度时设置为0,最后唤醒等待元素放入队列的线程。
E poll(long timeout, TimeUnit unit):该方法是poll()的可配置超时等待方法,和上面的offer一样,使用while循环+Condition的awaitNanos来进行等待,等待时间到后执行dequeue获取元素
E take():
获取元素方法
删除元素方法
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE
,也就是无界队列,所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。此队列按照先进先出的原则对元素进行排序。
LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。
LinkedBlockingQueue和ArrayBlockingQueue的不同点
入队方法
LinkedBlockingQueue提供了多种入队操作的实现来满足不同情况下的需求,入队操作有如下几种:
出队方法
入队列的方法说完后,我们来说说出队列的方法。LinkedBlockingQueue提供了多种出队操作的实现来满足不同情况下的需求,如下:
获取元素方法
删除元素方法
PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:
**BlockingDeque(阻塞双端队列)**在Deque的基础上实现了双端阻塞等待的功能。和第2节说的类似,BlockingDeque也提供了双端队列该有的阻塞等待方法:
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。LinkedBlockingDeque
是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE
。
相比于其它阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。
LinkedBlockingDeque和LinkedBlockingQueue的相同点
LinkedBlockingDeque和LinkedBlockingQueue的不同点
入队方法
LinkedBlockingDeque提供了多种入队操作的实现来满足不同情况下的需求,入队操作有如下几种:
出队方法
入队列的方法说完后,我们来说说出队列的方法。LinkedBlockingDeque提供了多种出队操作的实现来满足不同情况下的需求,如下:
获取元素方法
获取元素前加锁,防止并发问题导致数据不一致。利用first和last节点直接可以获得元素。
删除元素方法
删除元素是从头/尾向两边进行遍历比较,故时间复杂度为O(n),最后调用unlink把要移除元素的prev和next进行关联,把要移除的元素从链中脱离,等待下次GC回收。
TransferQueue是JDK 1.7对于并发类库新增加的一个接口,它扩展自BlockingQueue,所以保持着阻塞队列的所有特性。
TransferQueue对比与BlockingQueue更强大的一点是,生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。新添加的transfer方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递(以建立Java内存模型中的happens-before关系的方式)。
该接口提供的标准方法:
LinkedTransferQueue 是单向链表结构的无界阻塞队列。
LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。新添加的 transfer 方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程 transfer 到另一个线程的过程中,它有效地实现了元素在线程之间的传递(以建立 Java 内存模型中的 happens-before 关系的方式)。**Doug Lea 说从功能角度来讲,LinkedTransferQueue 实际上是 ConcurrentLinkedQueue、SynchronousQueue(公平模式)和 LinkedBlockingQueue 的超集。**而且 LinkedTransferQueue 更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。
什么是进程 ?
进程是资源分配的最小单位。(资源包括各种表格、内存空间、磁盘空间) 同一进程中的多条线程将共享该进程中的全部系统资源。
什么是线程 ?
线程是CPU调度的最小单位。线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表组成。 而寄存器可被用来存储线程内的局部变量。
什么是并行和并发 ?
线程优缺点
优点
缺点
实现线程只有一种方式:
实现线程执行内容有两种方式:
更多实现线程执行内容的方式,只需在此基础上进行封装:
Thread、Runnable和Callable的区别
Thread.sleep(0)的作用是什么?
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
wait()和sleep()的区别?
Linux中线程状态一共有5种:
Java中线程状态一共有6种(生命周期):
如果想要确定线程当前的状态,可以通过 getState() 方法,并且线程在任何时刻只可能处于 1 种状态。线程状态切换:
New 表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,所以也没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New 变成 Runnable,也就是状态转换图中中间的这个大方框里的内容。
Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
所以,如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。
首先来看最简单的 Blocked,从箭头的流转方向可以看出,从 Runnable 状态进入 Blocked 状态只有一种可能,就是进入 synchronized 保护的代码时没有抢到 monitor 锁,无论是进入 synchronized 代码块,还是 synchronized 方法,都是一样。当处于 Blocked 的线程抢到 monitor 锁,就会从 Blocked 状态回到Runnable 状态。
线程进入 Waiting 状态有三种可能性:
Blocked 仅仅针对 synchronized monitor 锁,可是在 Java 中还有很多其他的锁,比如 ReentrantLock,如果线程在获取这种锁时没有抢到该锁就会进入 Waiting 状态,因为本质上它执行了 LockSupport.park() 方法,所以会进入 Waiting 状态。同样,Object.wait() 和 Thread.join() 也会让线程进入 Waiting 状态。
Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。
在 Waiting 上面是 Timed Waiting 状态,这两个状态是非常相似的,区别仅在于有没有时间限制,Timed Waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。以下情况会让线程进入 Timed Waiting 状态。
线程会以下面三种方式结束,结束后就是终止状态:
正常结束:run()或 call()方法执行完成,线程正常结束
异常结束:线程抛出一个未捕获的 Exception 或 Error
调用 stop:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
线程安全是指当多个线程访问某个类时,该类始终都表现正确行为,则称该类是线程安全的。
出现线程安全问题的原因?
在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写⼊入已经开始,但还没结束),其他线程对这个写了一半的资源进⾏了读操作,或者对这个写了一半的资源进⾏了写操作,导致此资源出现数据错误。
如何避免线程安全问题?
多个线程操作一个资源的情况下,导致资源数据前后不一致。这样就需要协调线程的调度,即线程同步。线程同步的方式:
多线程通讯的方式主要包括以下几种:
sleep()
yield()
join()
一般需要配合synchronized一起使用。Object的主要方法如下:
使用显式的 Lock 和 Condition 对象:
await():当前线程进入等待状态,直到被通知(signal/signalAll)、中断或超时
signal():唤醒一个等待在Condition上的线程,将该线程从等待队列中转移到同步队列中
signalAll():能够唤醒所有等待在Condition上的线程
造成死锁的原因?
多个线程竞争共享资源,由于资源被占用、资源不足或进程推进顺序不当等原因造成线程处于永久阻塞状态,从而引发死锁。死锁形成的原因:
死锁的解决办法?
守护线程(daemon=true):当线程只剩下守护线程的时候,JVM就会退出;但是如果还有其它的任意一个用户线程还在,JVM就不会退出。在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
sleep与wait区别
start与run区别
yield跟sleep区别
把ThreadLocal看成一个全局Map
ThreadLocal常用的方法
然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据ThreadLocal对象的hash值,定位到table中的位置i
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value
if (k == key) {
e.value = value;
return;
}
// 如果当前位置是空的,就初始化一个Entry对象放在位置i上
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
// 如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。
while (e != null) {
ThreadLocal<?> k = e.get();
// 相等就直接返回,不相等就继续查找,找到相等位置。
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
如果想共享线程的ThreadLocal数据怎么办?
使用InheritableThreadLocal
可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal
的实例,然后在子线程中得到这个InheritableThreadLocal
实例设置的值。
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
弱引用:只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
解决:在finally中remove即可。
**那为什么ThreadLocalMap的key要设计成弱引用?**key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
InheritableThreadLocal
是 JDK 本身自带的一种线程传递解决方案。顾名思义,由当前线程创建的线程,将会继承当前线程里 ThreadLocal 保存的值。
JDK
的InheritableThreadLocal
类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal
值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal
值传递到 任务执行时。
TransmittableThreadLocal 是Alibaba开源的、用于解决 “在使用线程池等会缓存线程的组件情况下传递ThreadLocal” 问题的 InheritableThreadLocal 扩展。若希望 TransmittableThreadLocal 在线程池与主线程间传递,需配合 TtlRunnable 和 TtlCallable使用。
使用场景
ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
线程池(Thread Pool)
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中。
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。当然,使用线程池可以带来一系列好处:
线程池要解决的两个问题
多线程优缺点
线程池类型
JDK默认提供四种线程池:
corePoolSize(核心线程数,默认值为1)
maximumPoolSize(最大线程数,默认值为Integer.MAX_VALUE)
keepAliveTime(线程空闲时间,Single和Fixed默认值为0ms,Cached默认值为60s)
unit(时间单位):用于设置keepAliveTime的时间单位
workQueue(任务队列容量,阻塞队列,默认值为Integer.MAX_VALUE)
threadFactory( 新建线程工厂)
allowCoreThreadTimeout:允许核心线程超时,默认值为false
handler(任务拒绝处理器,默认值为策略为AbortPolicy)
以下两种情况会拒绝处理任务:
线程池最重要的5种状态:
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
hreadPoolExecutor的运行状态有5种,分别为:
其生命周期转换如下入所示:
线程池分配线程时,其执行过程如下:
任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。使用不同的队列可以实现不一样的任务存取策略。以下是阻塞队列的成员:
由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。
线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:
getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。拒绝策略是一个接口,其设计如下:
CPU密集型(CPU-bound)
CPU密集型(又称计算密集型),是指任务需要进行大量复杂的运算,几乎没有阻塞,需要CPU长时间高速运行。在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
线程数 = CPU核数 + 1
JVM可运行的CPU核数可以通过Runtime.getRuntime().availableProcessors()
查看。也可设置成CPU核数×2 ,这还是要看JDK的使用版本以及CPU配置(服务器的CPU有超线程)。对于JDK1.8来说,里面增加了一个并行计算,因此计算密集型的较理想:
线程数 = CPU内核线程数×2
IO密集型(IO-bound)
对于IO密集型的应用,我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输或磁盘读写,线程花费更多的时间在IO阻塞上,而不是CPU运算。如与数据库和缓存之间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束且数据准备好后,线程才会继续执行。因此对于IO密集型的应用,我们可以多设置一些线程池中线程的数量(但不宜过多,因为线程上下文切换是有代价的),这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。对于IO密集型应用:
线程数=CPU数/(1-阻塞系数)
阻塞系数=线程等待时间/(线程等待时间+CPU处理时间)
或 线程数=CPU核数×2 + 1
这个阻塞系数一般为 0.8~0.9 之间,也可以取0.8或者0.9。套用公式,对于双核CPU来说,它比较理想的线程数就是:2÷(1-0.9)=20,当然这都不是绝对的,需要根据实际情况以及实际业务来调整。
目标假设
参数计算
每秒需要多少个线程处理(corePoolSize) = tasks ÷ (1 ÷ taskCost)
① corePoolSize = tasks ÷ (1 ÷ taskCost) = (500~1000) ÷ (1 ÷ 0.1) = 50~100个线程
② 根据2/8原则,如果每秒任务数80%都小于800,那么corePoolSize设置为80即可
线程池缓冲队列大小(queueCapacity) = (coreSizePool ÷ taskCost) × responseTime
① queueCapacity = 80÷0.1×1 = 80,即队列里的线程可以等待1s,超过了的需要新开线程来执行
② 禁止设置为Integer.MAX_VALUE,否则CPU Load会飙满,耗时会变长,内存也会OOM
最大线程数(maximumPoolSize )= (Max(tasks) - queueCapacity) ÷ (1÷taskCost)
① 计算可得 maximumPoolSize = (1000 - 80) ÷ 10 = 92
rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则利用一些缓冲机制来处理
keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
prestartAllCoreThreads:调用线程池的prestartAllCoreThreads方法,可以实现提前创建并启动好所有基本线程
注意: 以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器CPU Load已经满了,则需要通过升级硬件和优化代码,降低taskCost来处理。
synchronized
是依赖监视器 Monitor
实现方法同步或代码块同步的,代码块同步使用的是 monitorenter
和 monitorexit
指令来实现的:
任何对象都有一个 Monitor 与之关联,当且一个 Monitor 被持有后,它将处于锁定状态。synchronized是一种 互斥锁,它通过字节码指令monitorenter和monitorexist隐式的来使用lock和unlock操作,synchronized 具有 原子性 和 可见性 。
共享资源代码段又称为临界区(critical section
),保证临界区互斥,是指执行临界区(critical section
)的只能有一个线程执行,其他线程阻塞等待,达到排队效果。
synchronize缺点
synchronized和lock的区别
compare | synchronized | lock |
---|---|---|
哪层面 | 虚拟机层面 | 代码层面 |
锁类型 | 可重入、不可中断、非公平 | 可重入、可中断、可公平 |
锁获取 | A线程获取锁,B线程等待 | 可以尝试获取锁,不需要一直等待 |
锁释放 | 由JVM 释放锁 | 在finally中手动释放。如不释放,会造成死锁 |
锁状态 | 无法判断 | 可以判断 |
在多线程的 JAVA程序中,实现线程之间的同步,就要说说 Monitor。 Monitor是 Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。下面这个图,描述了线程和 Monitor之间关系,以及线程的状态转换图:
从图中可以看出,一个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。 先看 “Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。
Synchronized
的使用方式有三种:
monitor
)便是对象实例(this
)monitor
)便是对象的Class
实例(每个对象只有一个Class
实例)monitor
)是指定对象实例修饰普通函数
修饰静态函数
修饰代码块
Jdk 1.5
以后对Synchronized
关键字做了各种的优化,经过优化后Synchronized
已经变得原来越快了,这也是为什么官方建议使用Synchronized
的原因,具体的优化点如下:
Java
中每个对象都拥有对象头,对象头由Mark World
、指向类的指针、以及数组长度三部分组成,本文,我们只需要关心Mark World
即可,Mark World
记录了对象的HashCode
、分代年龄和锁标志位信息。Mark World简化结构:
锁状态 | 存储内容 | 锁标记 |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
锁的升级变化,体现在锁对象的对象头Mark World
部分,也就是说Mark World
的内容会随着锁升级而改变。Java1.5
以后为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,Synchronized
的升级顺序是 「无锁–>偏向锁–>轻量级锁–>重量级锁,只会升级不会降级」
HotSpot 作者经过研究实践发现,在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得的,为了让线程获得锁的代价更低,于是就引进了偏向锁。
偏向锁(Biased Locking)指的是,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下会给线程加一个偏向锁。
偏向锁执行流程
当一个线程访问同步代码块并获取锁时,会在对象头的 Mark Word 里存储锁偏向的线程 ID,在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁,如果 Mark Word 中的线程 ID 和访问的线程 ID 一致,则可以直接进入同步块进行代码执行,如果线程 ID 不同,则使用 CAS 尝试获取锁,如果获取成功则进入同步块执行代码,否则会将锁的状态升级为轻量级锁。具体流程如下:
偏向锁的优点
偏向锁是为了在无多线程竞争的情况下,尽量减少不必要的锁切换而设计的,因为锁的获取及释放要依赖多次 CAS 原子指令,而偏向锁只需要在置换线程 ID 的时候执行一次 CAS 原子指令即可。
Mark Word
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为以下 3 个区域:
对象头中又包含了:
对象在内存中的布局如下:
在 JDK 1.6 中默认是开启偏向锁的,可以通过“-XX:-UseBiasedLocking=false”命令来禁用偏向锁。
轻量级锁考虑的是竞争锁对象的线程不多,持有锁时间也不长的场景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失,所以干脆不阻塞这个线程,让它自旋一段时间等待锁释放。当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
注意事项
需要强调一点:轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果同一时间多个线程同时访问时,就会导致轻量级锁膨胀为重量级锁。
轻量级锁膨胀之后,就升级为重量级锁,重量级锁是依赖操作系统的MutexLock(互斥锁)来实现的,需要从用户态转到内核态,这个成本非常高,这就是为什么Java1.6之前Synchronized效率低的原因。
升级为重量级锁时,锁标志位的状态值变为10,此时Mark Word中存储内容的是重量级锁的指针,等待锁的线程都会进入阻塞状态,下面是简化版的锁升级过程。
ReentrantLock的底层是借助AbstractQueuedSynchronizer实现,所以其数据结构依附于AbstractQueuedSynchronizer的数据结构,关于AQS的数据结构,在前一篇已经介绍过,不再累赘。
ReentrantLock和synchronized比较
数据结构
锁的升级流程
Java对象头
Hotspot虚拟机的对象头主要包括两部分数据(synchronized的锁就是存在Java对象头中):
Monitor
锁状态升级流程
优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
指同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。适用于只有一个线程访问同步块场景。
指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。适用于追求响应时间,同步块执行速度非常快。
指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其它申请的线程进入阻塞,性能降低。适用于追求吞吐量,同步块执行速度较长。
指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。普通自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。其特点如下:
简单代码实现:
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁未被占用,则设置当前线程为锁的拥有者
while (owner.compareAndSet(null, currentThread)) {
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有锁的拥有者才能释放锁
owner.compareAndSet(currentThread, null);
}
}
缺点
TicketLock即为排队自旋锁。思路:类似银行办业务,先取一个号,然后等待叫号叫到自己。好处:保证FIFO,先取号的肯定先进入。而普通的SpinLock,大家都在转圈,锁释放后谁刚好转到这谁进入。简单的实现:
public class TicketLock {
private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
private AtomicInteger ticketNum = new AtomicInteger(); // 排队号
public int lock() {
// 首先原子性地获得一个排队号
int myTicketNum = ticketNum.getAndIncrement();
// 只要当前服务号不是自己的就不断轮询
while (serviceNum.get() != myTicketNum) {
}
return myTicketNum;
}
public void unlock(int myTicket) {
// 只有当前线程拥有者才能释放锁
int next = myTicket + 1;
serviceNum.compareAndSet(myTicket, next);
}
}
缺点
Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
MCS SpinLock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
public class MCSLock {
public static class MCSNode {
MCSNode next;
boolean isLocked = true; // 默认是在等待锁
}
volatile MCSNode queue ;// 指向最后一个申请锁的MCSNode
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER =
AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode. class, "queue" );
public void lock(MCSNode currentThread) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
if (predecessor != null) {
predecessor.next = currentThread;// step 2
while (currentThread.isLocked ) {// step 3
}
}
}
public void unlock(MCSNode currentThread) {
if ( UPDATER.get( this ) == currentThread) {// 锁拥有者进行释放锁才有意义
if (currentThread.next == null) {// 检查是否有人排在自己后面
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isLocked = false;
currentThread.next = null;// for GC
}
}
}
CLH(Craig, Landin, and Hagersten Locks)锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
public class CLHLock {
public static class CLHNode {
private boolean isLocked = true; // 默认是在等待锁
}
@SuppressWarnings("unused" )
private volatile CLHNode tail ;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode .class , "tail" );
public void lock(CLHNode currentThread) {
CLHNode preNode = UPDATER.getAndSet( this, currentThread);
if(preNode != null) {//已有线程占用了锁,进入自旋
while(preNode.isLocked ) {
}
}
}
public void unlock(CLHNode currentThread) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThread, null)) {
// 还有后续线程
currentThread. isLocked = false ;// 改变状态,让后续线程结束自旋
}
}
}
CLH优势
CLH锁与MCS锁的比较
悲观锁
当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候总会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized。
乐观锁
每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否回去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,比如CAS是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁大。
公平锁
指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁 比如ReentrantLock(底层是同步队列FIFO: First Input First Output来实现)
非公平锁
获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronized、ReentrantLock。
小结:非公平锁性能高于公平锁,更能重复利用CPU的时间。ReentrantLock中可以通过构造方法指定是否为公平锁,默认为非公平锁!synchronized无法指定为公平锁,一直都是非公平锁。
可重入锁
也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。一个线程获取锁之后再尝试获取锁时会自动获取锁,可重入锁的优点是避免死锁。
不可重入锁
若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
小结:可重入锁能一定程度的避免死锁 synchronized、ReentrantLock都是可重入锁。
独享锁
指锁一次只能被一个线程持有。也叫X锁/排它锁/写锁/独享锁:该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得独享锁的线程即能读数据又能修改数据!
共享锁
指锁一次可以被多个线程持有。也叫S锁/读锁,能查看数据,但无法修改和删除数据的一种锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享!
小结:ReentrantLock和synchronized都是独享锁,ReadWriteLock的读锁是共享锁,写锁是独享锁。
与独享锁/共享锁的概念差不多,是独享锁/共享锁的具体实现。
ReentrantLock和synchronized都是互斥锁,ReadWriteLock是读写锁
自旋锁
常见的自旋锁:TicketLock,CLHLock,MSCLock
偏向锁
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了 让线程获得锁的代价更低
而引入了偏向锁,让该线程会自动获取锁,减少不必要的CAS操作。
轻量级锁
对于轻量级锁,其性能提升依据:“对于绝大部分锁,在整个生命周期内都是不会存在竞争的
”。轻量级锁的目标:减少无实际竞争情况下,使用重量级锁产生的性能消耗
,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
背景
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
定义
自旋锁就是 让该线程等待(即执行一段无意义的循环为自旋)固定的一段时间
,不会被立即挂起,看持有锁的线程是否会很快释放锁。
弊端
自旋可以避免线程切换带来的开销,但它占用了处理器(CPU)的时间。长时间的自旋而不处理任何事,就会浪费资源,所以需要设置自旋等待时间(即自旋次数)。自旋的次数虽然可以通过参数-XX:PreBlockSpin来调整(默认为10次),但固定的自旋次数,会对部分场景(如只需要自旋一两次就可获得锁)造成浪费
,因此JDK1.6引入了自适应自旋锁。
所谓自适应就意味着 自旋的次数不再是固定的
,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除,锁消除的依据是 逃逸分析
的数据支持。
锁消除主要是解决我们使用JDK内置API时存在的 隐形加锁操作
。如StringBuffer、Vector、HashTable等,StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
在使用同步锁的时候,需要让同步块的作用范围尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数情况下,上述观点是正确的。但如果 一系列的连续加锁解锁操作,可能会导致不必要的性能损耗
,所以引入锁粗化的概念。锁粗化概念比较好理解,就是 将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
。
如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
检查异常(CheckedException)。一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:
运行时异常(RuntimeException)。如 :NullPointerException 、 ClassCastException ;一个是检查异常CheckedException,如I/O错误导致的IOException、SQLException。 RuntimeException 是那些可能在Java虚拟机正常运行期间抛出的异常的超类。 如果出现 RuntimeException,那么一定是程序员的错。
抛出异常有三种形式:
throw 和 throws 的区别
Java 类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类。根据定义的方式不同,内部类分为静态内部类、成员内部类、局部内部类和匿名内部类四种。
使用static修饰的内部类我们称之为静态内部类,不过我们更喜欢称之为嵌套内部类。静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。没有这个引用就意味着:
public class OuterClass {
private String sex;
public static String name = "chenssy";
/**
*静态内部类
*/
static class InnerClass1{
/* 在静态内部类中可以存在静态成员 */
public static String _name1 = "chenssy_static";
public void display(){
/*
* 静态内部类只能访问外围类的静态成员变量和方法
* 不能访问外围类的非静态成员变量和方法
*/
System.out.println("OutClass name :" + name);
}
}
/**
* 非静态内部类
*/
class InnerClass2{
/* 非静态内部类中不能存在静态成员 */
public String _name2 = "chenssy_inner";
/* 非静态内部类中可以调用外围类的任何成员,不管是静态的还是非静态的 */
public void display(){
System.out.println("OuterClass name:" + name);
}
}
/**
* @desc 外围类方法
* @author chenssy
* @data 2013-10-25
* @return void
*/
public void display(){
/* 外围类访问静态内部类:内部类. */
System.out.println(InnerClass1._name1);
/* 静态内部类 可以直接创建实例不需要依赖于外围类 */
new InnerClass1().display();
/* 非静态内部的创建需要依赖于外围类 */
OuterClass.InnerClass2 inner2 = new OuterClass().new InnerClass2();
/* 方位非静态内部类的成员需要使用非静态内部类的实例 */
System.out.println(inner2._name2);
inner2.display();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.display();
}
}
成员内部类也是最普通的内部类,它是外围类的一个成员,所以他是可以无限制的访问外围类的所有 成员属性和方法,尽管是private的,但是外围类要访问内部类的成员属性和方法则需要通过内部类实例来访问。在成员内部类中要注意两点:
public class OuterClass {
private String str;
public class InnerClass{
public void innerDisplay(){
//使用外围内的属性
str = "chenssy...";
System.out.println(str);
//使用外围内的方法
outerDisplay();
}
}
/* 推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时 */
public InnerClass getInnerClass(){
return new InnerClass();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.getInnerClass();
inner.innerDisplay();
}
}
有这样一种内部类,它是嵌套在方法和作用于内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。
定义在方法里:
public class Parcel5 {
public Destionation destionation(String str){
class PDestionation implements Destionation{
private String label;
private PDestionation(String whereTo){
label = whereTo;
}
public String readLabel(){
return label;
}
}
return new PDestionation(str);
}
public static void main(String[] args) {
Parcel5 parcel5 = new Parcel5();
Destionation d = parcel5.destionation("chenssy");
}
}
定义在作用域内:
public class Parcel6 {
private void internalTracking(boolean b){
if(b){
class TrackingSlip{
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip(){
return id;
}
}
TrackingSlip ts = new TrackingSlip("chenssy");
String string = ts.getSlip();
}
}
public void track(){
internalTracking(true);
}
public static void main(String[] args) {
Parcel6 parcel6 = new Parcel6();
parcel6.track();
}
}
button2.addActionListener(
new ActionListener(){
public void actionPerformed(ActionEvent e) {
System.out.println("你按了按钮二");
}
});
获取当前类上的泛型类型方式如下:
public class DefaultTargetType<T> {
private Type type;
private Class<T> classType;
@SuppressWarnings("unchecked")
public DefaultTargetType() {
Type superClass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
if (this.type instanceof ParameterizedType) {
this.classType = (Class<T>) ((ParameterizedType) this.type).getRawType();
} else {
this.classType = (Class<T>) this.type;
}
}
}
获取到泛型中的类型方式:
Class<List<User>> classType = new DefaultTargetType<List<User>>() {}.getClassType();
获取当前类的父类接口上的泛型类型方式如下:
public class DefaultTargetType implements TargetType<T> {
private Type type;
private Class<T> classType;
@SuppressWarnings("unchecked")
public DefaultTargetType() {
Type superClass = getClass().getGenericInterfaces()[0];
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
if (this.type instanceof ParameterizedType) {
this.classType = (Class<T>) ((ParameterizedType) this.type).getRawType();
} else {
this.classType = (Class<T>) this.type;
}
}
}
获取到泛型中的类型方式:
Class<List<User>> classType = new DefaultTargetType<List<User>>() {}.getClassType();
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。