目录
volatile
-
不保证原子性
package volatileTest;
class MyData {
volatile int number = 0;
public void addOne() {
number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
case2();
}
//验证原子性
public static void case2() {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addOne();
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t number value:" + myData.number);
}
}
执行结果:
Connected to the target VM, address: '127.0.0.1:49209', transport: 'socket'
main number value:19000
Disconnected from the target VM, address: '127.0.0.1:49209', transport: 'socket'
分析:多次试验结果不等于20000且并不一致,因此说明 volatile 不能保证原子性。
解决方法:
- 加锁:使用 synchronized 加锁
- 使用 AtomicInteger
-
线程可见性
验证代码:
package volatileTest;
import java.util.concurrent.TimeUnit;
/**
* @Description: 验证:volatile的线程可见性
* @Author: zhangkai
* @Date: 2020/3/8 17:20
*/
public class ThreadVisibility {
private static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
while (flag) {
//do sth
}
System.out.println("end");
}, "server").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}
}
执行结果:执行1秒后自动停止
Connected to the target VM, address: '127.0.0.1:51481', transport: 'socket'
end
Disconnected from the target VM, address: '127.0.0.1:51481', transport: 'socket'
分析:flag字段如果不添加volatile关键字,程序将永远无法停止,因为主程序执行后,另一个线程一直在执行并不会获取到flag的变化。添加volatile后,flag将变的其他线程可见,那么1秒后主线程将flag置位false,另一个线程自然会跳出循环而停止。
-
禁止指令重排序
验证会出现指令重排序代码
package volatileTest;
/**
* @Description: 验证:下面程序会出现指令重排序的情况
* @Author: zhangkai
* @Date: 2020/3/8 17:38
*/
public class Disorder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(new Runnable() {
@Override
public void run() {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
// System.out.println(result);
}
}
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
执行结果:
Connected to the target VM, address: '127.0.0.1:52075', transport: 'socket'
第701171次 (0,0)
Disconnected from the target VM, address: '127.0.0.1:52075', transport: 'socket'
分析:经过足够多的次数,此处在第701171次出现了x=0,y=0的情况,那么只有在线程one中x=b和a=1调换顺序,线程two中b=1和y=a调换顺序的情况,x和y才可能全部为0,说明这里jvm底层对代码的执行进行了优化,导致了指令重排序。如果在某些业务场景下这种排序会造成状态不一致,那么就需要volatile来禁止指令重排序,即对x,y,a,b都添加volatile修饰。
-
volatile如何解决指令重排序
1. 添加volatile修饰符
2.底层ACC_VOLATILE
3.JVM的内存屏障
在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
4.hotspot实现
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
-
传统DCL单例增加volatile改进
原因在于instance = new Singleton(); 可以分为三步:
1. memory=allocate();//分配内存空间
2. instance(memory);//初始化对象
3. instance=memory;//设置instance指向分配的内存地址,分配成功后,instance!=null
由于步骤2和步骤3不存在数据依赖关系,且无论重排序与否执行结果在单线程中没有改变,因此这两个步骤的重排序是允许的。也就是说指令重排序只会保证单线程串行语义的一致性(as-if-serial),但是不会关心多线程间的语义一致性。
因此,重排序之后,先执行3会导致instance!=null,但是对象还未被初始化。此时,别的线程在调用时,获取了一个未初始化的对象。
因此,在声明 instance 时,使用 volatile 进行修饰,禁止指令重排序。改进的代码如下:
package volatileTest;
/**
* @Description: 改进传统DCL单例模式
* @Author: zhangkai
* @Date: 2020/3/8 18:15
*/
public class Instance {
private String str = "";
private int a = 0;
private volatile static Instance instance;
/**
* 构造方法私有化
*/
private Instance() {
str = "hello";
a = 20;
}
/**
* DCL方式获取单例
*/
public static Instance getInstance() {
if (instance == null) {
synchronized (Instance.class) {
if (instance == null) {
//此处可能存在instance初始化不完整导致其他线程获取到不完整的实例
//instance增加volatile修饰:对volatile字段的写操作happen-before后续的对同一个字段的读操作
instance = new Instance();
}
}
}
return instance;
}
}
synchronized
-
底层实现
lock cmpxchg
-
锁升级过程
无锁 - 偏向锁 - 轻量级锁 (自旋锁,自适应自旋)- 重量级锁
synchronized优化的过程和markword息息相关:
用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位
-
Object o = new Object() 锁 = 0 01 无锁态
-
o.hashCode() 001 + hashcode
00000001 10101101 00110100 00110110 01011001 00000000 00000000 00000000
little endian big endian
00000000 00000000 00000000 01011001 00110110 00110100 10101101 00000000
-
默认synchronized(o) 00 -> 轻量级锁 默认情况 偏向锁有个时延,默认是4秒 why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
-XX:BiasedLockingStartupDelay=0
-
如果设定上述参数 new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock 打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101
-
如果有线程上锁 上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程 偏向锁不可重偏向 批量偏向 批量撤销
-
如果有线程竞争 撤销偏向锁,升级轻量级锁 线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁
-
如果竞争加剧 竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制 升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间
(以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)
偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数
偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁
有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。
对象内存布局
markword锁升级状态
-
能否在多线程同时调用同步和非同步方法
public class T1 {
/**
* m1加锁
*/
public synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m1 end");
}
/**
* m2不加锁
*/
public void m2() {
System.out.println("m2 start");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2 end");
}
public static void main(String[] args) {
T1 t1 = new T1();
new Thread(t1::m1, "m1").start();
new Thread(t1::m2, "m2").start();
}
执行结果:m1同步方法被调用后再调用非同步方法,可以同时执行。
-
synchronized是不是可重入锁
package synchronizedTest;
import java.util.concurrent.TimeUnit;
/**
* @Description: 验证:synchronized是不是可重入锁
* @Author: zhangkai
* @Date: 2020/3/8 15:04
*/
public class T2 {
/**
* m1加锁
*/
public synchronized void m1() {
System.out.println("m1");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}
/**
* m2加锁
*/
public synchronized void m2() {
System.out.println("m2");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new T2().m1();
}
}
执行结果显示:synchronized是可重入锁
Connected to the target VM, address: '127.0.0.1:62582', transport: 'socket'
m1
m2
Disconnected from the target VM, address: '127.0.0.1:62582', transport: 'socket'
分析:对m1 m2分别加锁,在m1中调用m2,二者拿到的是同一把锁,所以调用m2是可以执行的。从父子类都有锁也可以说明。
-
synchronized vs Lock (CAS)
在高争用 高耗时的环境下synchronized效率更高
在低争用 低耗时的环境下CAS效率更高
synchronized到重量级之后是等待队列(不消耗CPU)
CAS(等待期间消耗CPU)
一切以实测为准
-
锁消除 lock eliminate
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
-
锁粗化 lock coarsening
public String test(String str){ int i = 0; StringBuffer sb = new StringBuffer(): while(i < 100){ sb.append(str); i++; } return sb.toString(): }
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
-
锁降级
参考博客:Hotspot JVM锁是否可以降级
CAS
-
基本概念
什么是CAS? 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁
CAS 的全程是 CompareAndSwap,是一条 CPU 并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。
因为经常配合循环操作,直到完成为止,所以泛指一类操作。
-
底层解析
在原子类中,CAS 操作都是通过 Unsafe 类来完成的。
//AtomicInteger i++
public final int getAndIncrement(){
return unsafe.getAndAddInt(this,valueoffset,1);
}
复制代码
其中 this 是当前对象, valueoffset 是一个 long ,代表地址的偏移量。
//AtomicInteger.java
private static final Unsafe unsfae=Unsafe.getUnsafe();//unsafe对象
private static final long valueOffset;//地址偏移量
static{
try{
valueoffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value");
}catch(Excepthion ex){throw new Error(ex);}
}
private volatile int value;//存储的数值
复制代码
- Unsafe
Unsafe 类是 rt.jar 下的 sun.misc 包下的一个类,基于该类可以直接操作特定内存的数据。
Java方法无法直接访问底层系统,需要使用 native 方法访问,Unsafe 类的内部方法都是 native 方法,其中的方法可以像C的指针一样直接操作内存,Java 中的 CAS 操作的执行都依赖于 Unsafe 类的方法。
- valueOffset
该变量表示变量值在内存中的偏移地址, Unsafe 就是根据内存偏移地址获取数据的。
-
ABA问题
类比:你的女朋友在离开你的这段儿时间经历了别的人,自旋就是你空转等待,一直等到她接纳你为止
解决办法:增加版本号(版本号 AtomicStampedReference),基础类型简单值不需要版本号
package cas;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @Description: 验证ABA问题解决方案:CAS通过新增版本号的机制来解决。在这里可以使用 AtomicStampedReference 来解决。
* @Author: zhangkai
* @Date: 2020/3/9 0:11
*/
public class SolveABADemo {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 版本号:" + stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 版本号:" + atomicStampedReference.getStamp());
}, "t1").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 版本号:" + stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean ret = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t" + ret
+ " stamp:" + atomicStampedReference.getStamp()
+ " value:" + atomicStampedReference.getReference());
}, "t2").start();
}
}
结果:
t2 版本号:1
t1 版本号:1
t1 版本号:2
t1 版本号:3
t2 false stamp:3 value:100
Disconnected from the target VM, address: '127.0.0.1:49930', transport: 'socket'
分析:t1先将100改为101,后又改为100,t2再去比较原值虽然还是100,但是它上次的版本号是1,现在版本号已经变成3了,所以无法正确修改,返回false。
unsafe
-
底层原理
参考美团的这篇文章,讲的很全面、透彻:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
测试小程序:
package unsafeTest;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @Description: 测试unsafe.compareAndSwapInt
* @Author: zhangkai
* @Date: 2020/3/8 22:15
*/
public class T1 {
int i = 0;
private static T1 t1 = new T1();
public static void main(String[] args) {
try {
//通过反射获取单例对象theUnsafe。
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//通过反射获取i的内存偏移量
Field ii = T1.class.getDeclaredField("i");
long iOffset = unsafe.objectFieldOffset(ii);
System.out.println("iOffset:" + iOffset);
//CAS底层使用的是原子指令lock cmpxchg,所以不会造成数据不一致问题
if (unsafe.compareAndSwapInt(t1, iOffset, 0, 1)) {
//如果内存位置的值(val[ioffset])==预期原值(0),则更新内存位置的值为新值(1),否则不做任何操作
System.out.println("i:" + t1.i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果:
iOffset:12
i:1
Process finished with exit code 0
-
AtomicInteger 中实现
下边是 AtomicInteger 中实现 i++ 功能所调用的 Unsafe 类的函数。
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1,long var2,int var4){
int var5;
do{
//获取当前的值的地址
var5=this.getIntVolatile(var1,var2);
//var1代表对象,var2和var5分别代表当前对象的真实值和期望值,如果二者相等,更新为var5+var4
}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4);
return var5;
}
复制代码
在 getAndAddInt 函数中,var1 代表了 AtomicInteger 对象, var2 代表了该对象在内存中的地址, var4 代表了期望增加的数值。
首先通过 var1 和 var2 获取到当前的主内存中真实的 int 值,也就是 var5。
然后通过循环来进行数据更改,当比较到真实值和对象的当前值相等,则更新,退出循环;否则再次获取当前的真实值,继续尝试,直到成功。
在 CAS 中通过自旋而不是加锁来保证一致性,同时和加锁相比,提高了并发性。
具体情境来说:线程A和线程B并发执行 AtomicInteger 的自增操作:
- AtomicInteger 中的 value 原始值为 3。主内存中 value 为 3, 线程A和线程B的工作内存中有 value 为 3 的副本;
- 线程 A 通过 getIntVolatile() 获取到 value 的值为3,并被挂起。
- 线程 B 也获取到 value 的值为3,然后执行 compareAndSwapInt 方法,比较到内存真实值也是 3,因此成功修改内存值为4.
- 此时线程 A 继续执行比较,发现对象中的 value 3 和主内存中的 value 4 不一致,说明已经被修改,A 重新进入循环。
- 线程 A 重新获取 value,由于 value 被 volatile 修饰,所以线程 A 此时 value 为4,和主内存中 value 相等,修改成功。
JMM
JMM(Java 内存模型)是一种抽象的概念,描述了一组规则或规范,定义了程序中各个变量的访问方式。
JVM运行程序的实体是线程,每个线程创建时 JVM 都会为其创建一个工作内存,是线程的私有数据区域。JMM中规定所有变量都存储在主内存,主内存是共享内存。线程对变量的操作在工作内存中进行,首先将变量从主内存拷贝到工作内存,操作完成后写会主内存。不同线程间无法访问对方的工作内存,线程通信(传值)通过主内存来完成。
JMM 对于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
Java锁
-
公平锁和非公平锁
ReentrantLock 可以指定构造函数的 boolean 类型得到公平或非公平锁,默认是非公平锁,synchronized也是非公平锁。
公平锁是多个线程按照申请锁的顺序获取锁,是 FIFO 的。并发环境中,每个线程在获取锁时先查看锁维护的等待队列,为空则占有,否则加入队列。
非公平锁是指多个线程不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。高并发情况下可能导致优先级反转或者饥饿现象。并发环境中,上来尝试占有锁,尝试失败,再加入等待队列。
-
可重入锁
可冲入锁指的是同一线程外层函数获取锁之后,内层递归函数自动获取锁。也就是线程能进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLock 和 synchronized 都是可重入锁。
可重入锁最大的作用用来避免死锁。
-
自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁。好处是减少线程上下文切换的消耗,缺点是循环时会消耗CPU资源。CAS采用的就是自旋锁。
-
读写锁
- 独占锁:该锁一次只能被一个线程持有,如 ReentrantLock 和 synchronized。
- 共享锁:该锁可以被多个线程持有。
- ReentrantReadWriteLock 中,读锁是共享锁,写锁时独占锁。读读共享保证并发性,读写互斥。
-
Synchronized 和 Lock 的区别
- 原始构成
- Synchronized 是关键字,属于JVM层面,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成。由于 wait/notify 方法也依赖于 monitor 对象,因此只有在同步块或方法中才能调用这些方法。
- Lock 是 java.util.concurrent.locks.lock 包下的,是 api层面的锁。
- 使用方法
- Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁
- ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁。
- 等待是否可以中断
- Synchronized 不可中断,除非抛出异常或者正常运行完成
- ReentrantLock 可以中断。一种是通过
tryLock(long timeout, TimeUnit unit)
,另一种是lockInterruptibly()
放代码块中,调用interrupt()
方法进行中断。
- 加锁是否公平
- synchronized 是非公平锁
- ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁。
- 锁绑定多个 Condition
- Synchronized 只有一个阻塞队列,只能随机唤醒一个线程或者唤醒全部线程。
- ReentrantLock 用来实现分组唤醒,可以精确唤醒。
阻塞队列
-
概念
阻塞队列首先是一个队列,所起的作用如下:
- 当阻塞队列为空,从队列中获取元素的操作将会被阻塞
- 当阻塞队列为满,向队列中添加元素的操作将会被阻塞
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到元素其他线程向空的队列中插入新的。同样的,试图向已满的阻塞队列中添加新元素的线程同样会被阻塞,直到其他线程从队列中移除元素使得队列重新变得空闲起来并后序新增。
阻塞:阻塞是指在某些情况下会挂起线程,即阻塞,一旦条件满足,被挂起的线程又会自动被唤醒。
优点:BlockingQueue 能帮助我们进行线程的阻塞和唤醒,而无需关心何时需要阻塞线程,何时需要唤醒线程。同时兼顾了效率和线程安全。
-
阻塞队列的架构
BlokcingQueue 接口实现了 Queue 接口,该接口有如下的实现类:
- ArrayBlockingQueue: 由数组组成的有界阻塞队列
- LinkedBlockingQueue: 由链表组成的有界阻塞队列(默认大小为 Integer.MAX_VALUE)
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue: 不存储元素的阻塞队列,单个元素的队列,同步提交队列
- LinkedTransferQueue:链表组成的无界阻塞队列
- LinkedBlockingDeque:链表组成的双向阻塞队列
-
阻塞队列的方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 无 | 无 |
- 抛出异常:当队列满,
add(e)
会抛出异常IllegalStateException: Queue full
;当队列空,remove()
和element()
会抛出异常NoSuchElementException
- 特殊值:
offer(e)
会返回 true/false。peek()
会返回队列元素或者null。 - 阻塞:队列满,
put(e)
会阻塞直到成功或中断;队列空take()
会阻塞直到成功。 - 超时:阻塞直到超时后退出,返回值和特殊值中的情况一样。
-
生产者消费者模式
模式1:使用lock
package blockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description: 使用阻塞队列实现生产者消费者模式,方案1-使用lock
* @Author: zhangkai
* @Date: 2020/3/9 23:36
*/
class ShareData {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws Exception {
lock.lock();
try {
//判断
while (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName() + " produce\t" + number);
//通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws Exception {
lock.lock();
try {
//判断
while (number == 0) {
condition.await();
}
//干活
number--;
System.out.println(Thread.currentThread().getName() + " consume\t" + number);
//通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 一个初始值为0的变量,两个线程交替操作,一个加1一个减1,重复5次
* 1. 线程 操作 资源类
* 2. 判断 干活 通知
* 3. 防止虚假唤醒机制:判断的时候要用while而不是用if
*/
public class ProduceConsumeTraditionalDemo {
public static void main(String[] args) {
ShareData data = new ShareData();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
执行结果:
A produce 1
B consume 0
A produce 1
B consume 0
A produce 1
B consume 0
A produce 1
B consume 0
A produce 1
B consume 0
Disconnected from the target VM, address: '127.0.0.1:65070', transport: 'socket'
模式2:使用阻塞队列
package blockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description: 使用阻塞队列实现生产者消费者模式,方案1-使用阻塞队列
* @Author: zhangkai
* @Date: 2020/3/9 23:48
*/
public class ProduceConsumeBlockingQueueDemo {
public static void main(String[] args) {
SharedData data = new SharedData(new ArrayBlockingQueue<>(10));
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t生产线程启动");
try {
data.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Producer").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
try {
data.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Consumer").start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.stop();
System.out.println("停止");
}
}
class SharedData {
private volatile boolean FLAG = true;
private AtomicInteger atomicInteger = new AtomicInteger();
BlockingQueue<String> blockingQueue = null;
public SharedData(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void produce() throws InterruptedException {
String data = null;
boolean ret;
while (FLAG) {
data = "" + atomicInteger.incrementAndGet();
ret = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (ret) {
System.out.println(Thread.currentThread().getName() + "\t插入" + data + "成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t插入" + data + "失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println("生产结束,FLAG=false");
}
public void consume() throws InterruptedException {
String ret = null;
while (FLAG) {
ret = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (null == ret || ret.equalsIgnoreCase("")) {
System.out.println(FLAG = false);
System.out.println(Thread.currentThread().getName() + "\t消费等待超时退出");
return;
}
System.out.println(Thread.currentThread().getName() + "\t消费" + ret + "成功");
}
}
public void stop() {
FLAG = false;
}
}
执行结果:
java.util.concurrent.ArrayBlockingQueue
Producer 生产线程启动
Producer 插入1成功
Consumer 消费线程启动
Consumer 消费1成功
Consumer 消费2成功
Producer 插入2成功
Consumer 消费3成功
Producer 插入3成功
停止
生产结束,FLAG=false
false
Consumer 消费等待超时退出
Disconnected from the target VM, address: '127.0.0.1:65515', transport: 'socket'
Process finished with exit code 0
线程池
-
创建线程
- 实现 Runnable 接口
- 实现 Callable 接口
- 继承 Thread 类
- 使用线程池
Thread的构造函数中并没有传入 Callable 的方式,但是可以传入 Runnable 接口: Thread thread=new Thread(Runnable runnable, String name);
。为了使用 Callable 接口,我们需要使用到 FutureTask 类。 FutureTask 类实现了 RunnableFuture 这一接口,而 RunnableFutre 又是 Future 的子接口,因此 FutureTask 可以作为参数使用上述的 Thread 构造函数。同时, FutureTask 本身构造函数可以传入 Callable 。
class MyThread implements Callable<Integer>{
@Override
public Integer call() {
System.out.println("come in callable");
return 2019;
}
}
class Main{
public static void main(String [] args){
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
Thread t1=new Thread(futureTask,"A");
}
}
复制代码
-
线程池架构
除此之外,还有 Executors 工具类。
-
ThreadPoolExecutor
线程池有七大参数:
public ThreadPoolExecutor(
int corePoolSize,//线程池常驻核心线程数
int maximumPoolSize,//线程池能容纳同时执行最大线程数
long keepAliveTime,//多余的空闲线程的存活时间,当前线程池线程数量超过core,空闲时间达到keepAliveTime,多余空闲线程会被销毁直到只剩下core个
TimeUnit unit,
BlockingQueue<Runnable> workQueue,//被提交尚未被执行的任务队列
ThreadFactory threadFactory,//创建线程的线程工厂
RejectedExecutionHandler handler//拒绝策略
)
{...}
复制代码
处理流程如下:
- 创建线程池,等待提交过来的任务请求。
- 添加请求任务
- 如果运行线程数小于 corePoolSize,创建线程运行该任务
- 如果运行线程数大于等于 corePoolSize,将任务放入队列
- 队列满,且运行线程数量小于 maximumPoolSize,创建非核心线程运行任务
- 队列满,且运行线程数量大于等于 maximumPoolSize,线程池会启动饱和拒绝策略执行。
- 线程完成任务,会从队列中取下一个任务来执行
- 一个线程无事可做超过 keepAliveTime 时:
- 如果当前运行线程数大于 corePoolSize,该线程被停掉
- 线程池的所有任务完成后最终会收缩到 corePoolSize 的大小。
-
拒绝策略
在 JDK 中有四种内置的拒绝策略,均实现了 RejectedExecutionHandler 接口。
- AbortPolicy: 直接抛出 RejectedExecutionException 异常,是默认的拒绝策略。
- DiscardPolicy: 直接丢弃任务,不予处理也不抛出异常。如果允许任务丢失,是最好的处理策略。
- DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列尝试再次提交。
- CallerRunsPolicy: 调用者运行。该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者。
-
三种常用线程池
- Executors.newFixedThreadPool(int)
创建固定容量的线程池,控制最大并发数,超出的线程在队列中等待。
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
复制代码
其中 corePoolSize 和 maximumPoolSize 值是相等的,并且使用的是 LinkedBlockingQueue。
适用于执行长期的任务,性能比较高。
- Executors.newSingleThreadExecutor()
创建了一个单线程的线程池,只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
复制代码
其中 corePoolSize 和 maximumPoolSize 都设置为1,使用的也是 LinkedBlockingQueue。
适用于一个任务一个任务执行的场景。
- Executors.newCachedThreadPool()
创建了一个可缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没有可以回收的,则新建线程。
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
复制代码
设置 corePoolSize 为0, maximumPoolSize 设置为 Integer.MAX_VALUE,使用的是 SynchronousQueue。来了任务就创建线程执行,线程空闲超过60秒后销毁。
适用于执行很多短期异步的小程序或者负载比较轻的服务器。
-
工作中使用什么样的线程池
在阿里巴巴Java开发手册中有如下规定:
- 线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
- 说明:使用线程池的好处是减少在创建和销毁线程上消耗的时间和系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程导致消耗完内存或者过度切换。
- 线程池不允许使用 Executors 去创建,也就是不能使用上述的三种线程池,而是要通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源韩进的风险。
- FixedThreadPool 和 SingleThreadPool 都采用了 LinkedBlockingQueue,其允许的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,导致OOM。
- CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能创建大量的线程,导致OOM。
-
如何设置线程池的线程数目
Runtime.getRuntime().availableProcessors()
获取当前设备的CPU个数。
- CPU密集型任务
- CPU 密集的含义是任务需要大量的运算,而没有阻塞,CPU一致全速运行
- CPU 密集任务只有在真正的多核 CPU 上才能得到加速(通过多线程),而在单核 CPU 上,无论开几个模拟的多线程都不能得到加速
- CPU 密集型任务配置尽可能少的线程数量,一般设置为
CPU核心数+1
- IO 密集型任务
- IO 密集型,是指该任务需要大量的IO,大量的阻塞
- 单线程上运行 IO 密集型的任务会导致浪费大量的 CPU 运算能力浪费在等待上
- IO 密集型任务使用多线程可以大大加速程序运行,利用了被浪费掉的阻塞时间
- IO 密集型时,大部分线程都阻塞,需要多配置线程数,可以采用
CPU核心数*2
,或者采用CPU 核心数 / (1 - 阻塞系数)
,阻塞系数在0.8 ~ 0.9之间
死锁
-
产生死锁的原因
死锁是指两个或两个以上的进程在执行过程中,因为争夺资源造成的互相等待的现象。
死锁需要满足的四大条件如下:
- 互斥
- 循环等待
- 不可抢占
- 占有并等待
产生死锁的主要原因有:
- 系统资源不足
- 进程运行推进顺序不当
- 资源分配不当
-
死锁实例
-
package deadLock; /** * @Description: 死锁示例 * @Author: zhangkai * @Date: 2020/3/10 23:33 */ class HoldLockThread implements Runnable { private String lock1; private String lock2; public HoldLockThread(String lock1, String lock2) { this.lock1 = lock1; this.lock2 = lock2; } @Override public void run() { synchronized (lock1) { System.out.println(Thread.currentThread().getName() + "\t持有" + lock1 + "\t尝试获取" + lock2); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println(Thread.currentThread().getName() + "\t持有" + lock1 + "\t尝试获取" + lock2); } } } } public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; new Thread(new HoldLockThread(lockA, lockB), "Thread1").start(); new Thread(new HoldLockThread(lockB, lockA), "Thread2").start(); } }
执行结果:
Connected to the target VM, address: '127.0.0.1:55686', transport: 'socket'
Thread1 持有lockA 尝试获取lockB
Thread2 持有lockB 尝试获取lockA
Disconnected from the target VM, address: '127.0.0.1:55686', transport: 'socket'
死锁定位分析:
使用 jps ,类似于 linux 中的 ps 命令。
在上述 java 文件中,使用 IDEA 中的 open In Terminal,或者在该文件目录下使用 cmd 命令行工具。
首先使用 jps -l
命令,类似于ls -l
命令,输出当前运行的 java 线程,从中能得知 DeadLockDemo 线程的线程号。
15796 deadLock.DeadLockDemo
然后,使用jstack threadId
来查看栈信息。输出如下:
Found one Java-level deadlock:
=============================
"Thread2":
waiting to lock monitor 0x0000000002b0cbb8 (object 0x00000000d77b0720, a java.lang.String),
which is held by "Thread1"
"Thread1":
waiting to lock monitor 0x0000000002b08b18 (object 0x00000000d77b0758, a java.lang.String),
which is held by "Thread2"
Java stack information for the threads listed above:
===================================================
"Thread2":
at deadLock.HoldLockThread.run(DeadLockDemo.java:28)
- waiting to lock <0x00000000d77b0720> (a java.lang.String)
- locked <0x00000000d77b0758> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread1":
at deadLock.HoldLockThread.run(DeadLockDemo.java:28)
- waiting to lock <0x00000000d77b0758> (a java.lang.String)
- locked <0x00000000d77b0720> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.