JMM
5.1Java内存模型
JMM即Java Memory Model,他定义了主存,工作内存抽象概念,底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等
JMM体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会手cpu缓存的影响
- 有序性- 保证指令不会受cpu指令并行优化的影响
可见性
退不出的循环
先来看一个现象,main线程对run变量的修改对于t1不可见,导致了t线程无法停止
static boolean run = true;
public static void main(String[] args) throw InterruptedException{
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; //线程t不会如预想的停下来
}
为什么?分析一下
- 初始状态,t线程刚开始从主内存读取了run的值到工作内存
- 因为t线程要频繁的从主存中读取run的值,JIT编译器会将run的值缓存至自己工作内存的高速缓存中,减少对主存中run的访问,提高效率
- 1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
volatile(易变关键字)
他可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
// 易变
volatile static boolean run = true;
public static void main(String[] args) throw InterruptedException{
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; //线程t不会如预想的停下来
}
可见性 vs 原子性
前面例子体现的实际就是可见性,他保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况
上例从字节码体现是这样的
getstatic run // 线程t获取run true
getstatic run // 线程t获取run true
getstatic run // 线程t获取run true
getstatic run // 线程t获取run true
putstatic run // 线程main修改run为false,仅此一次
getstatic run // 线程t获取run false
比较一下之前我们线程安全时举的例子:两个线程一个i++,一个i–,只能保证看到最新值,不能解决指令交错
注意
synchronized语句块既可以保证代码的原子性,也同时保证代码库内变量的可见性。但缺点时synchronized时属于重量级操作,性能相对较低
终止模式之两阶段终止模式
Two Phase Termination
在同一个线程t1中如何"优雅"的结束另一个线程t2?这里的【优雅】是指给t2一个料理后事的机会
1. 错误思路
- 使用线程对象的stop()方法结束线程
a)stop方法会真正杀死线程,如果这是线程锁住了共享资源,那么当他被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁 - 随时用System.exit(int)方法停止线程
a)目的仅是停止一个线程,但这个操作会让整个进程都停止
使用volitale优化两阶段终止模式
@Slf4j
public class TwoPhaseTermintionInvatile {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTerminationVolatile twoPhaseTerminationVolatile = new TwoPhaseTerminationVolatile();
twoPhaseTerminationVolatile.start();
Thread.sleep(3500);
log.debug("停止监控");
twoPhaseTerminationVolatile.stop();
}
}
@Slf4j
class TwoPhaseTerminationVolatile{
private Thread monitor;
static volatile boolean stop;
//启动监控线程
public void start(){
monitor = new Thread(()->{
while(true){
if(stop){
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
}
}
});
monitor.start();
}
public void stop(){
stop = true;
monitor.interrupt();
}
}
设计模式:犹豫模式
Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需在做了,直接结束返回
例如
@Slf4j
class TwoPhaseTerminationVolatile{
private Thread monitor;
static volatile boolean stop;
private boolean starting = false;
//启动监控线程
public void start(){
synchronized (this){
if(starting){
return;
}
starting = true;
}
monitor = new Thread(()->{
while(true){
if(stop){
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
}
}
});
monitor.start();
}
public void stop(){
stop = true;
monitor.interrupt();
}
}
当前端页面多次调用start时,也不会多次输出
5.2 有序性
JVM会在不影响正确性的前提下,可以调整语句的执行顺序,例如一下一段代码
static int i;
static int j;
// 在某个线程内进行赋值操作
i = ...;
j = ...;
可以发现,不管是先执行i还是先执行j,对最终的结果都不会造成影响。所以,上面代码真正执行的时候,既可以是
i = ...;
j = ...;
也可以是
j = ...;
i = ...;
这种特性称之为【指令重排】,多线程下的【指令重排】会影响正确性。所以为什么要有指令重排这项优化呢?
指令重排序优化
事实上,现代处理器会设计一个时钟周期完成一条执行时间最长的CPU指令,为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为:取指令——指令译码——执行指令——内存访问——数据协会 这五个阶段
术语参考
instruction fetch(IF)
instruction decode(ID)
execute(EX)
memory access(MEM)
regester write back(WB)
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在八十九十年代占据了计算架构的重要地位。
分阶段,分工是提升效率的关键
指令重排的前提是,重排指令不能影响结果,例
// 可以重排的例子
int a = 10;
int b = 20;
System.out.println(a+b);
// 不能重排的例子
int a = 10;
int b = a - 5;
5.3 volatile原理
volatile的底层原理是内存屏障,Memory Barrier(Memory Fence)
对volatile变量的写指令后会加入写屏障
对volatile变量的读指令前会加入读屏障
5.3.1 如何保证可见性
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存中
public void actor(I_Result r){
num = 2;
ready = true; //ready是volatile赋值带写屏障
// 写屏障
}
读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中的数据
public void actor1(I_Reuslt r){
//读屏障
//ready是volatile读取值带读屏障
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
5.3.2 如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r){
num = 2;
ready = true; // ready是volatile赋值带写屏障
}
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Reuslt r){
//读屏障
//ready是volatile读取值带读屏障
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
不过不能够解决指令交错
- 写屏障仅仅是保证之后的能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序
5.3.3 double-checked locking问题
以著名的double-checked locking单例模式为例
public final class Singleton(){
private Singleton(){ }
private static Singletion INSTANCE = null;
public static Singleton getInstance(){
if(INSTANCE == null){
//首次访问会同步,而之后的使用没有synchronized
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
以上的实现特点是
- 懒惰实例化
- 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁
- 有隐含的,但很关键的一点:第一个if使用了INSTANCE变量。在同步块之外
但是在多线程环境下,以上的代码是有问题的,getInstance方法对应的字节码是
其中 - 17表示创建对象,将对象引用入栈 // new Singleton
- 20表示复制一份对象引用 //引用地址
- 21表示利用一个对象引用,调用构造方法
- 24表示利用一个对象引用,赋值给static INSTANCE
也许jvm会优化为:先执行24,再执行21。如果两个线程他t1,t2按如下时间进行:
关键在于0:getstatic这行代码在monitor控制之外,可以越过monitor读取INSTANCE变量的值
这时t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的将是一个未初始化完毕的单例
对INSTANCE使用volatile修饰即可,可以禁用指令重排,但要注意在JDK5以上的版本的volatile才会真正有效
5.3.4 double-checked locking解决
public final class Singleton(){
private Singleton(){ }
private static volatile Singletion INSTANCE = null; //加一个volatile即可解决
public static Singleton getInstance(){
if(INSTANCE == null){
//首次访问会同步,而之后的使用没有synchronized
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
字节码上看不出来volatile的效果
happens-before规则
happens-before规定了对共享变量的写操作对其他线程的读操作可见,他是可见性与有序性的一套规则总结,抛开以下happens-before规则,JVM并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见
- 线程解锁m之前对对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m){
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m){
System.out.println(x);
}
},"t2").start();
- 线程对volatile变量的写,对接下来的其他线程对该变量可见
volatile int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
- 线程start前对变量的写,对该线程开始后对该变量可见
volatile int x;
x = 10;
new Thread(()->{
System.out.println(x);;
},"t1").start();
- 线程结束前对变量的写,对其他线程得知他结束后的读可见(比如调用join或者isAlive等待他结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.join();
System.out.println(x);
- 线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted()或者t2.isInterrupted())
static int x = 0;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()){
Thread.yield();
}
System.out.println(x);
}
- 对变量默认值(0,false,null)的写,对其他线程对该变量的读可见
- 具有传递性,如果x hb -> y并且有y hb -> z 那么有x hb -> z ,配合volatile的防指令重拍,有下面的例子
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
// x=20对t2可见,同时y=10也 对t2可见
System.out.println(x);
},"t2").start();
6. 共享模型之无锁
6.1 问题提出
有如下需求,保证account.withdraw取款方法的线程安全
有锁的解决方案
public class TestAccount {
public static void main(String[] args) {
AccountUnsafe accountUnsafe = new AccountUnsafe(10000);
Account.demo(accountUnsafe);
}
}
class AccountUnsafe implements Account{
private Integer balance;
public AccountUnsafe(Integer balance){
this.balance = balance;
}
@Override
public Integer getBalancer() {
synchronized (this){
return this.balance;
}
}
@Override
public void withdraw(Integer amount) {
synchronized (this){
this.balance -= amount;
}
}
}
interface Account{
Integer getBalancer();
void withdraw(Integer amount);
// 方法内启动一千个线程,每次取款十块钱,如果出示余额是10000,那么正确的结果应当是0
static void demo(Account account){
List<Thread> ts = new ArrayList<>();
for (int i = 0 ; i < 1000 ; i++){
ts.add(new Thread(()->{
account.withdraw(10);
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalancer() + "cost:" + (end - start)/1000_000 + "ms");
}
}
无锁的解决方法
public class TestAccount {
public static void main(String[] args) {
AccountCas accountCas = new AccountCas(10000);
Account.demo(accountCas);
}
}
class AccountCas implements Account{
private AtomicInteger balance;
public AccountCas(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalancer() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while(true){
// 获取余额的最新值
int prev = balance.get();
// 修改后的余额
int next = prev - amount;
// 真正修改
if(balance.compareAndSet(prev,next)){
break;
}
}
}
}
interface Account{
Integer getBalancer();
void withdraw(Integer amount);
// 方法内启动一千个线程,每次取款十块钱,如果出示余额是10000,那么正确的结果应当是0
static void demo(Account account){
List<Thread> ts = new ArrayList<>();
for (int i = 0 ; i < 1000 ; i++){
ts.add(new Thread(()->{
account.withdraw(10);
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalancer() + "cost:" + (end - start)/1000_000 + "ms");
}
}
6.2 CAS与volatile
前面看到的AtomicInteger的解决方法,内部并没有用锁来保护共享变量的线程安全。那么他是如何实现的呢?
public void withdraw(Integer amount) {
while(true){
// 获取余额的最新值
int prev = balance.get();
// 修改后的余额
int next = prev - amount;
// 真正修改(比较并设置值)
if(balance.compareAndSet(prev,next)){
break;
}
}
其中的关键是compareAndSet,他的简称就是CAS(也有Compare And Swap的说法),他必须是原子操作。
注意
其实CAS的底层是lock cmpxchg指令(X86架构),在单核CPU和多核CPU下都能够保证【比较-交换】的原子性。
在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
volatile
获取共享变量时,为了保证该变量的可见性,需要用volatile修饰。
他可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须要主存中去获取他的值,线程操作volatile变量都是直接操作主存。即一个线程对volatile变量的修改,对另一个线程可见。
注意
volatile仅仅保证了共享变量的可见性,让其他线程能够看到最新值,但是并不能够解决指令交错的问题(不能保证原子性)
CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果
为什么无锁效率高
- 无锁情况下,即是重试失败,线程始终在高速运行,没有停歇,但是synchronized会让线程在没有获得锁的时候,发生上下文切换,进入堵塞。
CAS特点
结合CAS和volatile可以实现无锁并发,适用于线程较少,多核CPU的场景下
- CAS是基于乐观锁的尝试:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试
- synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
- CAS体现的是无锁并发,无阻塞并发
a)因为没有使用synchronized,所以线程不会进入堵塞,这是效率得到提高的因素之一
b)但如果竞争激烈,可以想到重试必然发生,反而效率会收到影响
6.3 原子整数
J.U.C并发包提供了:
- AtomicBoolean
- AtomicInteger
- AtomicLong
以AtomicInteger为例
public class AtomicIntegerTest {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(5);
i.incrementAndGet(); // ++i
i.getAndIncrement(); // i++
i.getAndAdd(5); //先get再Add
i.addAndGet(5); //先Add再get
// 读取到 设置
i.updateAndGet(value -> value * 10);
i.get(); // 读取
}
}
6.4原子引用
为什么需要原子引用类型?
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
用法与原子整数类似
ABA问题
@Slf4j
public class ABAProblem {
// 这个共享变量是否被其他线程修改过
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
String prev = ref.get();
other();
Thread.sleep(1000);
log.debug("change A -> C {}",ref.compareAndSet(prev,"C"));
}
private static void other(){
new Thread(()->{
ref.compareAndSet(ref.get(),"B");
},"t1").start();
new Thread(()->{
ref.compareAndSet(ref.get(),"A");
},"t2").start();
}
}
主线程仅能判断出共享变量的值与最初的值A是否相同,不能感知到这种从A到B再从B到A的情况,如果主线程希望:
只要有其他线程【动过】共享变量,那么自己的cas就算失败,这时,仅比较值是不够的,需要再增加一个版本号
AtomicStampedReference
@Slf4j
public class AtomicStampedReferenceTest {
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
String prev = ref.getReference();
int stamp = ref.getStamp();
log.debug("{}",stamp);
other();
Thread.sleep(1000);
log.debug("change A->C {}", ref.compareAndSet(prev,"C",stamp,stamp+1));
}
private static void other(){
new Thread(()->{
ref.compareAndSet(ref.getReference(),"B",ref.getStamp(),ref.getStamp()+1);
},"t1").start();
new Thread(()->{
ref.compareAndSet(ref.getReference(),"A",ref.getStamp(),ref.getStamp()+1);
},"t2").start();
}
}
AtomicStampedReference可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A->B->A->C,通过AtomicStampedReference,我们可以知道,引用变量途中被更改了几次
但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否被更改过,所以就有了AtomicMarkableReference
6.5 原子数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
6.6 字段更新器
- AtomicReferenceFieldUpdater // 域 字段
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(field)进行原子操作,只能配合volatile修饰的字段的使用,否则会出现异常。
public class AtomicReferenceFieldUpdaterTest {
public static void main(String[] args) {
Student student = new Student();
// 三个参数分别为 类名,字段的类型,字段的名称
AtomicReferenceFieldUpdater updater =
AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
// 三个参数分别为 操作的对象 原本的值 想要操作后的值
System.out.println(updater.compareAndSet(student, null, "张三"));
System.out.println(student.toString());
}
}
class Student{
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
6.7 原子累加器
LongAdder
源码之LongAdder
LongAdder类有几个关键域
// 累加单元数组,懒惰初始化
transient volatile Cell[] cells;
//基础值,如果没有竞争,则用cas累加这个域
transient volatile long base;
//在cells创建或扩容时,置为1,表示加锁
transient volatile int cellsBusy;
原理之伪共享
其中Cell即为累加单元
// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell{
volatile long value;
Cell(long x){ valuee = x;}
// 最重要的方法,用来cas方式进行累加,prev表示旧值,next表示新值
final boolean cas(long prev,long next){
return UNSAFE.compareandSawpLong(this,valueOffset,prev,next);
}
// 省略不重要代码
}
得从缓存说起,缓存与内存的速度比较
因为CPU与内存的速度差异很大,需要靠预读数据到缓存来提高效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。
CPU要保证数据的一致性,如果某个CPU核心更改了数据,其他CPU核心对应的整个行必须失效
因为Cell是数组形式,在内存中是连续存储的,一个Cell为24字节(16字节的对象头和8字节的value),因此缓存行可以存下2个的Cell对象。这样问题来了
- Core-0要修改Cell[0]
- Core-1要修改Cell[1]
无论谁修改成功,都是导致对方的缓存行失效
6.8 Unsafe
概述
Unsafe对象提供了非常底层的,操作内存,线程的方法,Unsafe对象不能直接调用,只能通过反射获得
Unsafe CAS操作
public class UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//1. 获取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
Teacher t = new Teacher();
//2. 执行cas操作
unsafe.compareAndSwapInt(t,idOffset,0,1);
unsafe.compareAndSwapObject(t,nameOffset,null,"张三");
//验证
System.out.println(t);
}
}
@Data
class Teacher{
volatile int id;
volatile String name;
}
7.不可变对象
7.1不可变设计
一个大家很熟悉的String类是不可变的,以他为例,说明一下不可变设计的要素
public final class String{
implements java.io.Seriailzable,Comparable<String>,CharSequence{
/** the value is used for character storage */
private final char valie[];
/** Cache the hash code for the string */
private int hash //Default to 0
// ...
}
}
final的使用
发现该类,类中的所有属性都是final的
- 属性用final修饰保证了该属性是可读的,不能修改
- 类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
保护性拷贝
但有人会说,使用字符串时,也有一些跟修改相关的方法,比如substring等,那么下面就看一看这些方法是怎么实现的,以substring为例。
public String substring(int beginIndex){
if(beginIndex < 0){
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if(subLen < 0){
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value,beginIndex,subLen);
}
发现其内部是调用String的构造方法创建了一个新的字符串,再进入这个构造看看,是否对final char[] value做出了修改:
public String(char value[],int offset,int count){
if(offset < 0){
throw new StringIndexOutOfBoundsException(offset);
}
if(count <= 0){
if(count < 0){
throw new StringIndexOutOfBoundsException(count);
}
if(offset <= value.length){
this.value = "".value;
return;
}
}
if(offset > value.length - count){
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value,offset,offset+count);
}
结果发现也没有,构造新字符串对象时,会生成新的char[] value,对内容进行复制。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
7.2 享元模式
7.2.1 简介
定义
英文名称:Flyweight pattern 当需要重用数量有限的同一类对象的时候
归类
Structual patterns
7.2.2 体现
包装类
在JDK中的Boolean,Byte,Short,Integer,Long,Character.等包装类都提供了valueOf方法,例如Long的valueOf会缓存-128-127之间的Long对象,在这个范围之间会重用对象,大于这个范围,才会新建Long对象
public static Long valueOf(long l){
final int offset = 128;
if(l >= -128 && l <= 127){ // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(1);
}
7.2.3 DIY
例如:一个线上商城应用,QPS达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大地影响。这时预先创建好一批连接发,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再返回连接池,这样既节约了链接的创建和关闭时间,也实现了链接的重用,不至于让庞大的数据库连接压垮数据库。
public class DIYFlyweightPattern {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(()->{
Connection conn = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}
}
}
@Slf4j
class Pool{
// 1.连接池大小
private final int poolSize;
// 2.链接对象数组
private Connection[] connections;
// 3.链接状态数组 0表示空闲1表示繁忙
private AtomicIntegerArray states;
// 4 构造方法
public Pool(int poolSize){
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("链接" + i);
}
}
// 5 借连接
public Connection borrow(){
while(true){
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if (states.get(i) == 0) {
if (states.compareAndSet(i,0,1)) {
log.debug("borrow{}",connections[i]);
return connections[i];
}
}
}
// 如果没有空闲连接,当前线程进行等待
synchronized (this){
try {
log.debug("waiting...");
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
// 6 归还连接
public void free(Connection connection){
for (int i = 0; i < poolSize; i++) {
if(connections[i] == connection){
states.set(i,0);
synchronized (this){
log.debug("free{}",connection);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection{
private String name;
public MockConnection(String name){
this.name = name;
}
@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}
@Override
public Statement createStatement() throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return null;
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return null;
}
@Override
public String nativeSQL(String sql) throws SQLException {
return null;
}
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
}
@Override
public boolean getAutoCommit() throws SQLException {
return false;
}
@Override
public void commit() throws SQLException {
}
@Override
public void rollback() throws SQLException {
}
@Override
public void close() throws SQLException {
}
@Override
public boolean isClosed() throws SQLException {
return false;
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
return null;
}
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
}
@Override
public boolean isReadOnly() throws SQLException {
return false;
}
@Override
public void setCatalog(String catalog) throws SQLException {
}
@Override
public String getCatalog() throws SQLException {
return null;
}
@Override
public void setTransactionIsolation(int level) throws SQLException {
}
@Override
public int getTransactionIsolation() throws SQLException {
return 0;
}
@Override
public SQLWarning getWarnings() throws SQLException {
return null;
}
@Override
public void clearWarnings() throws SQLException {
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}
@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
return null;
}
@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
}
@Override
public void setHoldability(int holdability) throws SQLException {
}
@Override
public int getHoldability() throws SQLException {
return 0;
}
@Override
public Savepoint setSavepoint() throws SQLException {
return null;
}
@Override
public Savepoint setSavepoint(String name) throws SQLException {
return null;
}
@Override
public void rollback(Savepoint savepoint) throws SQLException {
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
return null;
}
@Override
public Clob createClob() throws SQLException {
return null;
}
@Override
public Blob createBlob() throws SQLException {
return null;
}
@Override
public NClob createNClob() throws SQLException {
return null;
}
@Override
public SQLXML createSQLXML() throws SQLException {
return null;
}
@Override
public boolean isValid(int timeout) throws SQLException {
return false;
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
}
@Override
public String getClientInfo(String name) throws SQLException {
return null;
}
@Override
public Properties getClientInfo() throws SQLException {
return null;
}
@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
return null;
}
@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
return null;
}
@Override
public void setSchema(String schema) throws SQLException {
}
@Override
public String getSchema() throws SQLException {
return null;
}
@Override
public void abort(Executor executor) throws SQLException {
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
}
@Override
public int getNetworkTimeout() throws SQLException {
return 0;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
7.2.4 final原理
设置final变量的原理
理解了volatile原理,再对比final的实现就比较简单了
public class TestFinal{
final int a = 20;
}
字节码
0:aload_0
1:invokespecial #1 //Method java/lang/Object. "<init>":()V
4:aload_0
5:bipush 20
7:putfield #2 //Field a:I
<-- 写屏障
10:return
发现final变量的赋值也会通过putfield指令来完成,同样在这条指令之后也会加入写屏障,保证在其他线程读到他的值的时候不会出现为0的情况
8
8.1 自定义线程池
8.1.1 自定义阻塞队列
@Slf4j
class BlockingQueue<T>{
// 1.任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2.锁
private ReentrantLock lock = new ReentrantLock();
// 3.生产着条件变量
private Condition fullWaitSet = lock.newCondition();
// 4.消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5.容量
private int capcity;
public BlockingQueue(int capcity){
this.capcity = capcity;
}
// 带超时的阻塞获取
public T poll(long timeout, TimeUnit unit){
lock.lock();
try {
// 将超时时间统一转换为纳秒
long nanos = unit.toNanos(timeout);
while(queue.isEmpty()){
try {
// 返回值为剩余的时间
if(nanos <= 0){
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
// 阻塞获取
public T take(){
lock.lock();
try {
while(queue.isEmpty()){
try {
emptyWaitSet.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
// 阻塞添加
public void put(T task){
lock.lock();
try {
while(queue.size() == capcity){
try {
log.debug("等待加入任务队列{}...",task);
fullWaitSet.await();
}catch (InterruptedException e){
e.printStackTrace();
}
}
log.debug("加入任务队列{}",task);
queue.addLast(task);
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
// 带超时时间的阻塞添加
public boolean offer(T task,long timeout,TimeUnit timeUnit){
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while(queue.size() == capcity){
try {
log.debug("等待加入任务队列{}...",task);
if(nanos <= 0){
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
}catch (InterruptedException e){
e.printStackTrace();
}
}
log.debug("加入任务队列{}",task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
}finally {
lock.unlock();
}
}
// 获取大小
public int size(){
lock.lock();
try {
return queue.size();
}finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy,T task){
lock.lock();
try {
// 判断队列是否已满
if(queue.size() == capcity){
rejectPolicy.reject(this,task);
}else{ // 有空闲
log.debug("加入任务队列{}",task);
queue.addLast(task);
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
}
8.1.2 自定义线程池
@Slf4j
class ThreadPool{
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet();
// 核心线程数
private int coreSize;
// 获取任务的超时时间
private long timeout;
private TimeUnit timeUnit;
private RejectPolicy<Runnable> rejectPolicy;
// 执行任务
public void execute(Runnable task){
// 当任务数没有超过coreSize时,直接交给worker对象执行
// 如果超过了,就加入任务队列暂存
synchronized (workers){
if(workers.size() < coreSize){
Worker worker = new Worker(task);
log.debug("新增worker{},{}",worker,task);
workers.add(worker);
worker.start();
}else{
taskQueue.tryPut(rejectPolicy,task);
}
}
}
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit,int queueCapcity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task){
this.task = task;
}
@Override
public void run() {
// 执行任务
// 1)当task不为空,执行任务
// 2)当task执行完毕,再接着从任务队列中获取任务执行
while(task != null || (task = taskQueue.poll(timeout,timeUnit)) != null){
try {
log.debug("正在执行...{}",task);
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
task = null;
}
}
synchronized (workers){
log.debug("worker被移除{}",this);
workers.remove(this);
}
}
}
}
8.1.3 拒绝策略
@Slf4j
public class TestPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(2,
1000, TimeUnit.MILLISECONDS, 10,(queue, task) -> {
// 1)死等
queue.put(task);
// 2)带超时等待
queue.offer(task,500,TimeUnit.MILLISECONDS);
// 3)让调用者放弃任务执行
log.debug("放弃");
// 4)让调用者抛出异常
throw new RuntimeException("任务执行失败" + task);
// 5)让调用者自己执行任务
task.run();
});
for (int i = 0; i < 5; i++) {
int j = i;
threadPool.execute(()->{
log.debug("{}",j);
});
}
}
}
@FunctionalInterface //拒绝策略
interface RejectPolicy<T>{
void reject(BlockingQueue<T> queue,T task);
}
8.2 ThreadPoolExecutor
1) 线程池状态
ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWM > RUNNING
这些信息存储在一个原子变量ctl中,目的是将线程池状态和线程个数合二为一,这样就可以用一次cas原子操作进行赋值
// c为旧值,ctlOf返回结果为新值
ctl.compareandSet(c,ctlOf(targetState,workerCountOf(c)));
// rs 为高 3 位 代表线程状态,wc为低29位代表线程个数,ctl是合并他们
private static int ctlOf(int rs,int wc){ return rs | wc;}
2)构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUint unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectdExecutionHandler handler)
- corePoolSize 核心线程数目(最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位-针对救急线程
- workQueue 阻塞队列
- threadFactory线程工厂-可以为线程创建时起个好名字
- handler 拒绝策略
工作方式
- 线程池中刚开始没有线程,当一个任务提交给线程池之后,线程池会创建一个新线程来执行任务
- 当线程数达到corePoolSize并没有空闲线程,这时再加入任务,新加的任务会被加入workQueue队列排,直到有空闲的线程
- 如果队列选择了有界队列,那么任务超过了队列大小时,会创建maximumPoolSize-corePoolSize数目的线程来救急
- 如果线程到达了maximumPoolSize仍然有新任务这时会执行拒绝策略。拒绝策略jdk提供了四种实现,其他著名框架也提供了实现
a) AbortPolicy让调用者抛出RejectedExecutionException异常,这是默认策略
b) CallerRunsPolicy让调用者运行任务
c) DiscardPolicy放弃本次任务
d) DiscardOldestPolicy放弃队列中最早的任务,本任务取代之
e) Dubbo的实现,在抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题
f) Netty的实现,是创建一个新线程来执行任务
g) ActiveMQ的实现,带超时等待(60s)尝试放入队列,类似于自定义拒绝
h) PinPoint的实现,他使用了一个拒绝策略链,会逐一尝试策略链中的每条拒绝策略 - 当高峰过去之后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime和unit来控制
根据这个构造方法,JDK Executors类中提供了众多工厂方法来创建各种用途的线程池
3) newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
特点:
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务
评价
适用于任务量已知,相对耗时的任务
4) newCachedThreadPool
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
特点
- 核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,意味着
a) 全部都是救急线程(60s后可回收)
b) 救急线程可以无线创建 - 队列采用了SynchronousQueu实现,特点是,他没有容量,没有线程来取是放不下去的(一手交钱一手交货)
评价
整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲一分钟后释放线程
适合任务数比较密集,但每个任务的执行时间较短的情况
5)newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
使用场景:
希望多个任务排队执行。线程数固定为1,任务数多于1时,会放入无界队列进行排队。任务执行完毕,这唯一的线程也不会被释放
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任务补救措施,而线程池还会创建一个新的线程,保证池的正常工作
- Executor.newSingleThreadExecutor()线程个数始终为1,不能修改
a) FinalizableDelegatedExecutorService应用的是装饰器模式,只对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法 - Executor.newFixedThreadPool(1)初始时为1,以后还可以修改
a) 对外暴露的是ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改
6) 提交任务sumbit
@Slf4j
public class ThreadPoolExecutor {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<String> future = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("running");
Thread.sleep(1000);
return "ok";
}
});
log.debug("{}",future.get());
}
}
7)提交任务invokeAll
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<String>> futures = pool.invokeAll(Arrays.asList(
() -> {
log.debug("begin");
Thread.sleep(1000);
return "1";
},
() -> {
log.debug("begin");
Thread.sleep(500);
return "2";
},
() -> {
log.debug("begin");
Thread.sleep(2000);
return "3";
}
));
futures.forEach(f -> {
try {
log.debug("{}",f.get());
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
});
}
8)提交任务invokeAny(执行所有任务,但是只会返回第一完成的任务的返回值 )
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
String result = pool.invokeAny(Arrays.asList(
() -> {
log.debug("begin");
Thread.sleep(1000);
return "1";
},
() -> {
log.debug("begin");
Thread.sleep(500);
return "2";
},
() -> {
log.debug("begin");
Thread.sleep(2000);
return "3";
}
));
log.debug("{}",result);
}
9) 关闭线程池
shutdown
线程池状态变为SHUTDOWN
- 不会接受新任务
- 但已提交任务会执行完
- 此方法不会阻塞调用线程的执行
void shutdown();
8.3异步模式之工作线程
8.3.1 定义
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,他的典型实现就是线程池,也体现了经典设计中的享元模式。
例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)
注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。
例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工。
8.3.2 饥饿
固定大小线程池会有饥饿现象
- 两个工人是同一个线程池中的两个线程
- 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的动作
a) 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
b) 后厨做菜:做菜就完了 - 比如工人A处理了点餐任务,接下来他要等工人B把菜做好,然后做菜,他俩配合的也挺好
- 但现在同时来了两个客人,这时工人A和工人B都去处理点餐了,这时没人做饭了,死锁。
8.3.3 创建多少线程池合适
- 过小会导致程序不能充分的利用系统资源,容易导致饥饿
- 过大会导致更多的线程的上下文切换,。占用更多内存
CPU密集型运算
通常采用cpu 核数 + 1 能够实现最优的CPU利用率,+ 1 是保证当前线程由于页缺失故障(操作系统)或其他原因导致暂停时,额外的这个线程就能顶上去,保证CPU时钟周期不被浪费
I/O密集型运算
CPU不总是处于繁忙状态,例如,当你执行业务计算时,这时会使用CPU资源,但当你执行I/O操作时,远程RPC调用时,包括进行数据库操作时,这时候CPU就闲下来了,你可以利用多线程提高的他的利用率
经验公式如下:
线程数 = 核数 * 期望 cpu利用率 * 总时间(CPU计算时间 + 等待时间) / CPU计算时间
8.3.4 tomcat 线程池
Tomcat在哪里用到了线程池呢
- LimitMatch用来限流,可以控制最大连接个数,类似J.U.C中的Semaphore(后面讲)
- Acceptor只负责【接受新的socket链接】
- Poller只负责监听socket channel是否有【可读的I/O事件】
- 一旦可读,封装一个任务对象(socketProsser),提交给Executor线程池处理
- Executor线程池中的工作线程最终负责【处理请求】
Tomcat线程池扩展到了ThreadPoolExecutor,行为稍有不同 - 如果总线程数达到maximumPoolSize
a) 这时不会立即抛出RejectedExecutionException异常
b) 而是再次尝试将任务放入队列,如果还失败,才抛出RejectedExecutionException异常
8.3.5fork/join
概念
fork/join是jdk1.7加入的新的线程池实现,他体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算
所谓的任务拆分,是将一个大任务拆分成算法上相同的小任务,直至不能拆分可以求解。跟递归的一些计算,如归并排序,斐波那契数列,都可以使用分治的思想进行求解
fork/join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
fork/join默认会创建与cpu核心数大小相同的线程池
使用
提交给fork/join线程池的任务需要继承RecursiveTask(有返回值)或者RecursiveAction(没有返回值),例如下面定义了一个对1-n之间的整数求和的任务
public class ForkJoinTest {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new MyTask(5)));
}
}
// 1-n之间整数的和
class MyTask extends RecursiveTask<Integer>{
private int n;
public MyTask(int n){
this.n = n;
}
@Override
protected Integer compute() {
if(n == 1){
return 1;
}
MyTask myTask = new MyTask(n - 1);
myTask.fork(); // 让一个线程去执行这个任务
int res = n + myTask.join();
return res;
}
}
8.4 J.U.C
8.4.1 AQS
概述
全称是AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点
- 用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
a) getState-获取state状态
b) setState-设置state状态
c) compareAndSetState-乐观锁机制设置state状态
d) 独占模式是只有一个线程能够访问资源,而共享模式允许多个线程访问资源 - 提供了基于FIFO的等待队列,类似于Monitor的EntryList
- 条件变量来实现等待,唤醒机制,支持多个条件变量,类似于Monitor的WaitSet
- 子类主要实现这样一些方法(默认抛出UnsupportedOperationExcption)
a) tryAcquire
b) tryRelease
c) tryAcquireShared
d) tryReleasedShared
e) isHeldExclusively - 获取锁的姿势
// 如果获取锁失败
if(!tryAcquire(arg)){
//入队,可以选择阻塞当前线程
}
- 释放锁的姿势
// 如果释放锁成功
if(tryRelease(arg)){
// 让阻塞线程恢复运行
}
8.4.2 AQS自定义锁
@Slf4j
public class AQSTest {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(()->{
lock.lock();
try {
log.debug("加锁成功");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("解锁成功");
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
try {
log.debug("加锁成功");
} finally {
log.debug("解锁成功");
lock.unlock();
}
},"t2").start();
}
}
// 自定义锁(不可重入锁)
class MyLock implements Lock{
// 独占锁 同步器类
class MySync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)){
// 加锁成功,并设置owner为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override // 是否持有独占锁
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
@Override // 加锁(不成功会进入队列等待)
public void lock() {
sync.acquire(1);
}
@Override // 加锁,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override // 尝试加锁(一次)
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override // 尝试加锁,带超时
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override //解锁
public void unlock() {
sync.release(1);
}
@Override // 创建条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
8.4.3 ReentrantLock原理
#### 非公平锁实现原理
加锁解锁流程
先从构造器开始看,默认为非公平锁实现
public ReentrantLock(){
sync = new NonFairSync();
}
NonfairSync继承自 AQS
没有竞争时
第一个竞争出现时
Thread-1执行了
- CAS尝试将state由0改为1,结果失败
- 进入tryAcquire逻辑,这时state已经是1,结果仍然失败
- 接下来进入addWaiter逻辑,构造Node队列
a) 图中黄色三角形表示该Node的waitStatus状态,其中0为默认正常状态
b) Node的创建是懒惰的
c) 其中第一个Node成为Dummy(哑元)或者哨兵,用来占位,并不关联线程
当前线程进入acquireQueued逻辑 - acquireQueued会在一个死循环中不断尝试获得锁,失败后进入park阻塞
- 如果自己时紧邻着head(排在第二位),那么再次tryAcquire尝试获取锁,当然这时state为1,失败
- 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为-1,这次返回false
- shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquire尝试获取锁,当时这时state仍为1,失败
- 当再次进入shouldParkAfterFailedAcquire时,这时因为其前驱的node的waitStatus已经是-1,这次返回true
- 进入parkAndCheckInterrupt,Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
Thread-0释放锁,进入tryRelease流程,如果成功
a) 设置exclusiveOwnerThread为null
b) state为0
当前队列不为null,并且head的waitStatus= -1,进入unparkSuccessor流程
找到队列中离head最近的一个Node(没取消的),unpark回复其运行,本例中即为Thread-1
回到Thread-1的acquireQueued流程
如果加锁成功(没有竞争),会设置 - exclusiveOwnerThread为Thread-1,state为1
- head指向刚刚Thread-1所在的Node,该Node清空Thread
- 原本的head因为从链表断开,而可被垃圾回收
如果这时候有其他的线程来竞争(非公平的体现),例如这时有Thread-4来了
如果不巧又被Thread-4 占了先
Thread-4 被设置为exclusiveOwnerThread,state=1
Thread-1 再次进入acquireQueued流程,获取锁失败,重新进入park阻塞
8.5 条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是ConditionObject
await流程
开始Thread-0持有锁,调用await,进入ConditionObject的addConditionWaiter流程,创建新的Node状态为-2(Node.CONDITION),关联Thread-0,加入等待队列尾部
接下来进入AQS的fullyRelease流程,释放同步器上的锁
unpark AQS队列的下一个节点,竞争锁,假设没有其它竞争线程,那么Thread-1竞争成功
park阻塞Thread-0
signal流程
假设Thread-1要来唤醒Thread-0
进入ConditionObject的doSignal流程,取得等待队列的第一个Node,即Thread-0所在Node
执行transferForSignal流程,将该Node加入AQS队列尾部,将Thread-0的waitStatus改为0,Thread-3的waitStatus改为-1
Thead-1释放锁,进入unlock流程,略。
8.6 读写锁
8.6.1 ReentrantReadWriteLock
当读操作远远高于写操作的时候,这时候使用读写锁让读 - 读可以并发,提高性能。
类似于数据库中的select … from … lock in share mode
提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法
// 读写,写写互斥,读读可并发
@Slf4j
class DateContainer{
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read(){
log.debug("获取读锁...");
r.lock();
try {
log.debug("读取");
Thread.sleep(1000);
return data;
} catch (InterruptedException e) {
throw new RuntimeException();
} finally {
log.debug("释放读锁...");
r.unlock();
}
}
public void write(){
log.debug("获取写锁...");
w.lock();
try {
log.debug("写入");
}finally {
log.debug("释放写锁...");
r.unlock();
}
}
}
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致读取写锁永久等待
r.lock();
try{
// ...
w.lock();
try{
// ...
} finally {
w.unlock();
}
}finally{
r.unlock();
}
- 重入时降级支持:即持有写锁的情况下去获取读锁
8.6.2 读写锁原理
图解流程
读写锁用的时同一个Sync同步器,因此等待队列,state等也是同一个
**t1 w.lock() , t2 r.lock() **
-
t1成功上锁,流程与ReentrantLock加锁相比没有特殊之处,不同时写锁状态占了state的低16位,而读锁使用的是state的高16位
-
t2执行r.lock(),这时进入读锁的sync.acquireShared(1)流程,首先会进入tryAcquireShared流程。如果有写锁占据,那么tryAcquireShared返回-1表示失败
tryAcquireShared返回值表示- -1表示失败
- 0表示失败,但后继节点不会继续唤醒
- 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回1
-
这时会进入sync.doAcquireShared(1)流程,首先也是调用addWaiter添加节点,不同之处在于节点被设置为Node.SHARED模式而非Node.EXCLUSIVE模式,注意此时t2仍处于活跃状态
-
t2会看看自己的节点是不是老二,如果是,还会再次调用tryAcquireShared(1)来尝试获取锁
-
如果没有成功,在doAcquireShared内for(;;)循环一次,把前驱节点的waitStatus改为-1,再for(;;)循环一次尝试tryAcquireShared(1),如果还不成功,那么在parkAndCheckInterrupt()出park
** t3 r.lock , t4 w.lock **
这种状况下,假设又有t3加读锁和t4加写锁,这期间t1仍然持有锁,就变成了下面这个样子
** t1 w.unlock **
这时会走到写锁的sync.release(1)流程,调用sync.tryRelease(1)成功,变成下面的样子
接下来执行唤醒流程sync.unparkSuccessor,即让老二恢复运行,这是t2在doAcquireShared内parkAndCheckInterrupt()处恢复运行
这回再来一次for(;;)执行tryAcquireShared成功则让读锁计数加一
这时t2已经回复运行,接下来t2调用setHeadAndPropagate(node,1),它原本所在节点被设置为头节点
事情还没完,在setHeadAndPropagate方法内还会检查下一个节点是否是head,如果是则调用doReleaseShared()将head的状态从-1改为0并唤醒老二,这是t3在doAcquireShared内parkAndCheckInterrupt()处恢复运行
还回再来一次for(;;)执行tryAcquireShared成功则让读锁计数加一
这时t3已经恢复运行,接下来t3调用setHeadAndPropogate(node,1),它原本所在节点被设置为头节点
下一个节点不是shared了,因此不会唤醒t4所在节点
** t2 r.unlock , t3 r.unlock **
t2进入sync.releaseShared(1)中,调用tryReleaseShared(1)让计数减一,但由于计数还不为零
t3进入sync.releaseShared(1)中,调用tryReleaseShared(1)让计数减一,这回计数为零了,进入doReleaseShared()将头节点从-1改为0,并且唤醒老二,即
之后t4在acquireQueued中parkAndCheckInterrupt处恢复运行,再次for(;;)这次自己是老二,并且没有其他竞争,tryAcquire(1)成功,修改头节点,流程结束
8.7 stampedLock
该类自JDK8加入,是为了进一步优化性能,他的特点是在使用读锁,写锁时都必须配合【戳】使用
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock支持tryOptimisticRead()方法(乐观读),读取完毕后需要做一次戳校验,如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
演示
@Slf4j
class DateContainerStamped{
private int data;
private final StampedLock lock = new StampedLock();
public DateContainerStamped(int data){
this.data = data;
}
public int read(int readTime) throws InterruptedException {
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking {}",stamp);
Thread.sleep(readTime);
if(lock.validate(stamp)){
log.debug("read finish...{}",stamp);
return data;
}
// 锁升级 - 读锁
log.debug("updating to read lock...{}",stamp);
try {
stamp = lock.readLock();
log.debug("read lock {}",stamp);
Thread.sleep(readTime);
log.debug("read finish... {}",stamp);
return data;
}finally {
log.debug("read unlock{}",stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) throws InterruptedException {
long stamp = lock.writeLock();
log.debug("write lock {}",stamp);
try {
Thread.sleep(2000);
this.data = newData;
} finally {
log.debug("write unlock {}",stamp);
lock.unlockWrite(stamp);
}
}
}
注:StampedLock不支持条件变量和锁重入
8.8 Semaphore
信号量,用来限制能同时访问共享资源的线程上线
@Slf4j
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore s = new Semaphore(3); // 限制上限为3
for (int i = 0; i < 10; i++) {
new Thread(()->{
try{
s.acquire();
log.debug("我是线程 {}", Thread.currentThread().getName());
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
s.release();
}
}).start();
}
}
}
输出为
Semaphore应用
- 使用Semaphore限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放即可,当然他只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比Tomcat LimitLatch)的实现
- 用Semaphore实现简单连接池,对于【享元模式】下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
@Slf4j
class SemaphorePool{
// 连接池大小
private int poolSize;
// 连接数组
private Connection[] connections;
// 连接是否被占用标记
private AtomicBoolean[] busys;
// 信号量限流
private Semaphore semaphore;
// 连接状态数组
private AtomicIntegerArray states;
// 初始化连接池
public SemaphorePool(int poolSize){
this.poolSize = poolSize;
// 让许可数与资源数一致
semaphore = new Semaphore(poolSize);
connections = new Connection[poolSize];
busys = new AtomicBoolean[poolSize];
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i + 1));
}
}
// 借连接
public Connection borrow() throws InterruptedException {
// 获取许可
semaphore.acquire(); // 没有许可的线程在此等待
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0){
if(states.compareAndSet(i,0,1)){
log.debug("borrow {}",connections[i]);
return connections[i];
}
}
}
// 不会执行到这里
return null;
}
// 归还连接
public void free(Connection conn){
for (int i = 0; i < poolSize; i++) {
if(connections[i] == conn){
states.set(i,0);
log.debug("free {}",conn);
semaphore.release();
break;
}
}
}
}
Semaphore原理
加锁解锁流程
Semaphore有点像一个停车场,permits就好像停车位数量,当线程获得了permits就像是获得了停车位,然后停车场显示空余车位减一
刚开始,permits(state)为3,这时五个线程来获取资源
假设其中Thread-1,Thread-2,Thread-4 cas竞争成功,而Thread-0和Thread-3竞争失败,进入AQS队列park阻塞
这是Thread-4释放了permits,状态如下
接下来Thread-0竞争成功,permits再次设置为0,设置自己为head节点,断开原来的head节点,unpark接下来的Thread-3节点,但是由于permits为0,因此Thread-3在尝试不成功后再次进入park状态
CountdownLatch
用来进行线程同步协作,等待所有线程完成倒计时
其中 构造参数用来初始化等待计数值,await()用来等待计数归零,countDown()用来让计数减一
@Slf4j
public class CountdownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
new Thread(()->{
log.debug("begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
log.debug("end...");
}).start();
new Thread(()->{
log.debug("begin...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
log.debug("end...");
}).start();
new Thread(()->{
log.debug("begin...");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
log.debug("end...");
}).start();
log.debug("waiting...");
latch.await();
log.debug("wait end...");
}
}
使用线程池改进版
@Slf4j
public class CountdownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(()->{
log.debug("begin...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end...{}",latch.getCount());
});
executorService.submit(()->{
log.debug("begin...");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end...{}",latch.getCount());
});
executorService.submit(()->{
log.debug("begin...");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end...{}",latch.getCount());
});
executorService.submit(()->{
try {
log.debug("waiting..");
latch.await();
log.debug("wait end...");
}catch (InterruptedException e){
e.printStackTrace();
}
});
}
CyclicBarrier
循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置【计数个数】,每个线程执行到某个需要“同步”的时刻调用await()方法进行等待,当等待的线程满足即【计数个数】时,继续执行
@Slf4j
public class CyclicBarrierTest {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
CyclicBarrier barrier = new CyclicBarrier(2,()->{
log.debug("task1 task2 finish...");
});
for (int i = 0; i < 3; i++) {
service.submit(()->{
log.debug("task1 begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
log.debug("task1 end...");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
});
service.submit(()->{
log.debug("task2 begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
log.debug("task2 end...");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
});
}
}
}
线程安全类概述
线程安全集合类可以分为三大类
- 遗留的线程安全类如hashtable,Vector
- 使用Collections装饰的线程安全集合
- java.util.concurrent.*
重点介绍下java.util.concurrent.*下的线程安全集合类,可以发现他们有规律,里面包含三类关键词Blocking,CopyOnWrite,Concurrent- Blocking大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite之类容器修改开销相对较重
- Concurrent类型的容器
- 内部使用很多cas优化,一般可以提供较高吞吐量
- 弱一致性
- 遍历时弱一致性,例如,当用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
- 求大小弱一致性,size操作未必是100%准确
- 读取弱一致性
遍历时如果发生了修改,对于非安全容器来讲,使用fail-fast机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历
ConcurrentHashMap原理
JDK7 HashMap并发死链(JDK7为头插,JDK8为尾插)
public static void main(String[] args) {
final HashMap<Integer,Integer> map = new HashMap<>();
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
map.put(6, null);
map.put(7, null);
map.put(8, null);
map.put(9, null);
map.put(10, null);
map.put(16, null);
map.put(35, null);
map.put(1, null);
new Thread(){
@Override
public void run() {
// 放第十三个元素,发生扩容
map.put(50, null);
}
}.start();
new Thread(){
@Override
public void run() {
// 放第十三个元素,发生扩容
map.put(50, null);
}
}.start();
}
JDK8 ConcurrentHashMap
重要属性和内部类
//默认为0
//当初始化时,为-1
//当扩容时,为-(1 + 扩容线程数)
//当初始化或扩容完成后,为下一次的扩容的阈值大小
private transient volatile int sizeCtl;
//整合ConcurrentHashMap 就是一个Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
//hash表
transient volatile Node<K,V>[] table;
//扩容时的新hash表
private transient volatile Node<K,V>[] nextTable;
// 扩容时如果某个bin迁移完毕,用ForwardingNode作为旧table bin的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
//用在compute以及conputeIfAbsent时,用来占位,计算完成后替换成普通的Node
static final class ReservationNode<K,V> extends Node<K,V> {}
//作为treebin的头节点,存储root和first
static final class TreeBin<K,V> extends Node<K,V> {}
//作为treebin的节点,存储parent,left,right
static final class TreeNode<K,V> extends Node<K,V> {}
重要方法
// 获取Node[]中的第i个Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab,int i)
//cas修改Node[]第i个Node的值,c为旧值,v为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab,int i,Node<K,V> c,Node<K,V> v)
//直接修改Node[]中第i个Node的值,v为新值
static final <K,V> void setTabAt(Node<K,V>[] tab,int i,Node<K,V> v)
size计算流程
size计算实际发生在put,remove改变集合元素的操作之中
- 没有竞争发生,像baseCount累加计数
- 有竞争发生,新建counterCells,向其中的一个cell累加计数
- counterCells初始有两个cell
- 如果计数竞争比较激烈,会创建新的cell用来计数
LinkedBlockingQueue原理
基本的出队入队
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>,java.io.Serializable{
static class Node<E>{
E item;
/**
*下列的三种情况
* - 真正的后继节点
* - 自己发生在出队时
* - null,表示是没有后继节点,是最后了
*/
Node<E> next;
Node(E x){ item = x;}
}
}
初始化链表last = head = new Node< E >(null);Dummy节点用来占位,item为null
当一个节点入队last = last.next = node;
再来一个节点入队last = last.next = node;
出队
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
heed = first;
E x = first.item;
first.item = null;
return x;
h = head
first = h.next
h = h.next
head = first
E x = first.item;
first.item = null;
return x;
加锁分析
高明之处在于用了两把锁和dummy节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者和一个消费者)执行
- 消费者与消费者线程仍然串行
- 生产者与生产者线程仍然串行
线程安全分析
- 当节点总数大于2时(包括dummy节点),putLock保证的是last节点的线程安全,takeLock保证的是head节点的安全。两把锁保证了出队和入队没有竞争
- 当节点总数等于2时(即一个dummy节点,一个正常节点),这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于1时(就一个dummy节点)这时take线程会被notEmpty条件阻塞,有竞争,会阻塞
// 用于put(阻塞)offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户take(阻塞)poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
put操作
public void put(E e) throws InterruptionException{
if(e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this,putLock;
// count用来维护数组计数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try{
// 满了等待
while(count.get() == capacity){
// 倒过来读就好:等待notFull
notFull.await();
}
// 有空位,入队且计数加一
enqueue(node);
c = count.getAndIncrement();
// 除了自己put以外,队列还有空位,由自己叫醒其他put线程
if(c + 1 < capacity){
notFull.signal();
}finally{
putLock.unlock();
}
//如果一个队列中有一个元素,叫醒take线程
if(c == 0){
// 这里调用的是notEmpty.signal() 而不是notEmpty.signalAll()是为了减少竞争
signalNotEmpty();
}
}
}
take操作
public E take() throws InterruptionException(){
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try{
while(count.get() == 0){
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if(c > 1)
notEmpty.signal();
}finally{
takeLock.unlock();
}
// 如果队列中只有一个空位时,叫醒put线程
// 如果有多个线程进行出队,第一个线程满足 c == capacity,但后续线程c < capacity
if(c == capacity)
// 这里调用的是notEmpty.signal() 而不是notEmpty.signalAll()是为了减少竞争
signalNotEmpty();
return x;
}
性能比较(LinkedBlockingQueue和ArrayBlockingQueue)
- Linked支持有界,Array强制有界
- Linked的实现是链表,Array是链表
- Linked是懒惰的,而Array需要提前初始化Node数组
- Linked每次入队会生成新的Node,而Array的Node是提前创建好的
- Linked两把锁,Array一把锁
ConcurrentLinkedQueue
ConcurrentLinkedQueue的设计与LinkedBlockingQueue非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy节点的引用让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了cas来实现
事实上,ConcurrentLinkedQueue应用还是非常广泛的
例如之前讲的Tomcat的Connector结构时,Acceptor作为生产者像Poller消费者传递事件信息时,正是采用了ConcurrentLinkedQueue将SocketChannel给Poller使用
CopyOnWriteArrayList
CopyOnWriteArraySet是他的马甲
底层实现采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其他线程的并发读,写分离
以新增为例
public boolean add(E e){
synchronized(lock){
// 获取旧的数组
Obejct[] es = getArray();
int len = es.length;
//拷贝新的数组(这里是比较耗时的操作,但不影响其它的读线程)
es = Arrays.copyOf(es,len+1);
//添加新元素
es[len] = e;
//替换旧的数组
setArray(es);
return true;
}
}
这里的源码用的是jdk11,但在java8中使用的是可重入锁而不是synchronized,其他读操作并为加锁,例如
public void forEach(Consumer<? super E> action){
Objects.requireNonNull(action);
for(Obejct x : getArray()){
@SuppressWarningss("unchecked") E e = (E) x
action.accept(e);
}
}
适合【读多写少】的应用层场景
get弱一致性
不容易测试,但问题确实存在
迭代器弱一致性
不要觉得弱一致性就不好
- 数据库的MVCC都是弱一致性的表现
- 并发高和一致性是矛盾的,需要权恒