JUC高并发
- JUC高并发
- 一、介绍
- 二、CompletableFuture
- 三、锁
- 四、 LockSupport和中断机制
- 五、Java内存模型JMM
- 六、volatile和JMM
- 七、CAS(Compare and swap 比较交换 )
- 八、ThreadLocal
- 九、Java对象内存布局和对象头
- 十、Synchronized与锁升级
- 十一、AQS
- 十二、ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
JUC高并发
一、介绍
Java多线程相关概念
1、一把锁(synchronized)
2、2个并
- 并发(一个线程处理多个任务)
- 并行(多个线程处理多个任务)
3、3个程
- 进程(在系统中运行的一个应用程序就是进程)
- 线程(也被称为
轻量级进程
,在同一个进程内会有1个或多个线程) - 管程(也就是我们平时说的锁,其实就是一种同步机制,他的义务是保证同一时间只有一个线程可以访问被保护的数据和代码)
JVM中同步的实现是基于进入和推出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象
用户线程和守护线程
1、用户线程
是程序的操作线程,他会完成这个程序所需要完成的业务操作。
2、守护线程
是一种特殊的线程,为其它线程服务,在后台默默地完成一些系统性的服务,比如垃圾回收线程就是最典型的例子
守护线程作为一个服务线程,没有服务对象就没有必要继续运行了
可以通过setDeamon(true)来将当前线程设置为守护线程,可以用isDeamon来判断是否是守护线程
二、CompletableFuture
1、实现的类
1. Future接口
功能
Future接口可以为主线程开启一个分支任务,专门为主线程处理耗时和费力的业务
Future接口定义了操作异步任务执行的一些方法,如:
- 1、获取异步任务的执行结果
get() :在主线程调用会导致阻塞,要等到异步任务执行完成之后才会继续运行
get(long timeout,TimeUnit unit) :规定在多长时间内获取方法的结果,如果超时未获取到结果,则抛出异常- 2、取消任务的执行
cancel(boolean b)- 3、判断任务是否被取消
isCancelled();- 4、判断任务是否完成
isDone();
缺陷
Future接口对于结果的获取不太友好,只能通过阻塞或者轮询的方式获得结果。并且不能多任务组合
2. CompletionStage接口
为了满足多个异步任务的拼接,因此实现了CompletionStage接口
2、核心的四大静态方法
这些静态方法可以指定线程池,如果没有指定线程池,那么使用默认的ForkJoinPool线程池
ForkJoinPool线程池时JDK7提供的一个用于并发执行任务的框架,其主旨是将大的任务分成一个个小的任务,计算小的任务再将任务结果汇总得到最终结果,使用的是分治算法,这里不做过多拓展
Q:使用ForkJoinPool还是自己创建的线程池?
A:1、如果是计算密集型的任务,例如大量数据计算或者图像处理,使用ForkJoinPool更好,因为ForkJoinPool的工作窃取算法能够充分发挥多核CPU性能,高效的执行计算密集型任务
2、如果是I/O密集型任务,使用自建线程池,因为I/O操作常常可能会导致线程阻塞,而ForkJoinPool的工作窃取算法在线程阻塞时发挥不了作用,因此使用ForkJoinPool可能会造成资源的浪费,因此使用自建线程池能更好的控制资源的分配和利用。
无返回值
1. public static CompletableFuture<Void> runAsync(Runnable runnable)
2. public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
有返回值
3. public static <U> supplyAsync(Supplier<U> supplier)
4. public static <U> supplyAsync(Supplier<U> supplier,Executor executor)
3、CompletableFuture常用方法
对于以下方法,记住一个窍门
就可以了:
凡是包含accept的都是有入参,无返回结果的;
凡是包含apply的,都是有入参,有返回结果的;
凡是包含run的,都是无入参,无返回结果的;
凡是包含handle的,有入参有返回结果,且入参包括了上一步的结果和异常。
原因:
accept入参是Consumer,这个函数式接口的accpet方法有入参无返回值;
apply入参是Function,这个函数接口的apply方法有入参,也有返回值;
run入参是Runnable,这个函数式接口的run方法无入参,无返回值;
handle入参是BiFunction
,这个函数式接口的apply方法有两个入参,第一个是前面处理的结果,第二个是前面处理发生的异常,有返回值
1. 获取结果和触发计算
a. 获取结果:
- public T get() 抛出编译时异常,需要进行捕获
- public T get(long timeout,TimeUnit unit)
- public T join() 抛出RuntimeException,不需要开发人员进行异常捕获
- public T getNow(T valueIfAbsent) :尝试获取结果,如果没有获取到,则返回默认值
b. 触发计算:
- public boolean complete(T value): 如果异步程序没有完成计算,返回true,并把入参作为异步代码执行结果;如果完成计算,返回false,返回程序执行结果
代码演示:
public class CompletableFutureDemo {
public static void main(String[] args) {
method1();
}
public static void method2(){
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("异步线程" + Thread.currentThread().getName() + "开始执行");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步线程" + Thread.currentThread().getName() + "执行结束");
return "aaa";
});
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步线程是否未执行完成?"+future.complete("bbb")+",执行结果:"+future.join());
}
public static void method1(){
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("异步线程" + Thread.currentThread().getName() + "开始执行");
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步线程" + Thread.currentThread().getName() + "执行结束");
return "aaa";
});
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步线程是否未执行完成?"+future.complete("bbb")+",执行结果:"+future.join());
}
}
method2执行结果
method1执行结果
2. 对计算结果进行处理
- thenApply:计算结果存在依赖,将两个线程串行化;如果有异常,终止下面步骤的执行
- handle(BiFunction<? super T, Throwable, ? extends U> fn):计算结果存在依赖,将两个线程串行化;如果有异常,也可以继续执行
3. 对计算结果进行消费
- thenAccept:消费处理,无返回结果
- thenRun、thenAccept、thenApply对比
1、thenRun(Runnable runnable) 无入参,无返回结果
2、thenAccept(Consumer action)有入参,无返回结果
3、thenApply(Funtion fn)有入参,有返回结果
- CompletableFuture和线程池讲解
4. 对计算速度进行选用
这个就不进行赘述了,记住上面的窍门就好
5. 对计算结果进行合并
- thenCombine:这个方法入参有BiFunction,所以,懂得都懂
三、锁
1、乐观和悲观锁
悲观锁
适合写操作多的场景,先加锁可以保证数据的正确性,但是比较重,比较吃性能
乐观锁
适合读操作多的场景,操作时不会加锁,性能相对较高,但是安全性低
在java中是通过无锁编程来实现,只是在更新数据的时候会判断之前有没有别的线程更新锁
- 乐观锁实现机制
1、通过版本号机制
2、最常采用的算法是CAS算法
2、公平锁和非公平锁
什么是公平锁/非公平锁
- 公平锁
让每个线程能够轮流抢到资源,不会存在某个线程没有抢到资源的情况。
- 非公平锁
不一定每个线程都能抢到资源,谁先抢到谁就占用锁。
公平锁和非公平锁的优缺点
公平锁:保证了每个线程按顺序抢到资源,但是会造成线程间频繁的切换,相较于非公平锁而言,比较耗费内存,速度慢
非公平锁:不能保证所有线程都抢到资源,但是速度快,线程切换不频繁
3、可重入锁(又称为递归锁)
概念
可重入锁指的是获取锁的线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象是同一个对象),不会因为之前已获取还没释放而阻塞。
java中ReentrantLock
和synchronized
都是可重入锁
可重入锁种类
实现原理:
每个锁对象都有一个计数器和指向持有该锁线程的指针。
当执行monitorenter时,如果目标对象的计数器为0,说明它没有被其他线程占用,此时会将当前线程设置为所得持有线程,并将计数器加1
在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,则java虚拟机会将其计数器加1,否则等待,直到释放锁
当执行monitorexit时,java虚拟机会将锁对象的计数器减1。计数器为0代表锁已释放
1. synchronized
synchronized又称为隐式锁
2. ReentrantLock
显示锁,需要代码声明
注意:
一旦
加锁就要解锁
,加锁和解锁总是成对出现的,如果加了锁,没有解锁,那么这个锁就会被永久占用,其他线程再次获取这个锁的时候,会失败,造成阻塞
,甚至死锁
的情况!
4、死锁及排查
两个线程相互持有对方想要获取的资源,造成死锁
第5、6、7、8节内容在之后的章节详细讲述
5、写锁(独占锁)/读锁(共享锁)
6、自旋锁SpinLock
7、无锁->独占锁->读写锁->邮戳锁
8、无锁->偏向锁->轻量锁->重量锁
四、 LockSupport和中断机制
1、中断机制
1. 什么是中断机制?
一个线程不应该由别的线程中断或者停止,而应该由它自己中断。
Java中没有立即中断线程的方法,只有协商机制,即如果需要中断一个耗时线程,可以调用interrupt方法,将线程的标志对象标记成true
2. 中断机制三大api
interrupt
将线程中断标志位设置为true
Thread.interrupted
静态方法,返回线程标志位,并将标志位重置为false
Thread.interrupted
静态方法,判断线程是否被中断
3. 大厂面试题中断机制考点
如何中断一个线程
1、使用volatile修饰符
public static volatile boolean flag = false;
public static void main(String[] args) {
new Thread(()->{
while(!flag){
System.out.println(Thread.currentThread().getName()+"is running......");
}
},"thread1").start();
try {
TimeUnit.MICROSECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("flag="+flag);
new Thread(()->{
flag = true;
System.out.println("flag="+flag);
}).start();
}
2、使用AutomaticBoolean
public static AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread(()->{
while(!flag.get()){
System.out.println(Thread.currentThread().getName()+"is running......");
}
},"thread1").start();
try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("flag="+flag);
new Thread(()->{
flag.set(true);
System.out.println("flag="+flag);
}).start();
}
3、使用线程自带的api
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is running......");
}
}, "thread1");
thread1.start();
try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 is interrupted? "+thread1.isInterrupted());
new Thread(()->{
thread1.interrupt();
System.out.println("thread1 is interrupted");
}).start();
}
4. 说明
interrupt方法仅仅只是将线程的标志位设置成为true,不会对正在运行的线程造成任何影响。
如果处于阻塞状态的线程(wait、join、sleep等)被其他线程中断,线程的中断状态会清除,并退出阻塞状态,抛出InterruptedException
2. LockSupport
1、LockSupport是什么
LockSupport是用来创建锁和其他同步类基本阻塞原语
2、三种让线程阻塞和唤醒的方法
1. wait()和notify()
wait和notify方法必须在同步块或者同步方法中,且成对出现。必须先wait再notify
2. Condition接口await()和singnal()
await和signal必须在锁块中才能使用(lock),必须先要等待,后唤醒,线程才能被唤醒
3. LockSupport类中的park等待和unpark唤醒机制
原理是给线程发放一个许可证,每个线程最多只有一个,重复调用unpark也不会积累permit。线程阻塞需要消耗凭证。
park和unpark无锁块要求,可以先唤醒在等待
五、Java内存模型JMM
计算机硬件存储关系
由于计算机CPU运行速度十分快,主存(物理内存)的读取速度比较慢,所以需要有CPU缓存,CPU先将运算结果放到高速缓存中,高速缓存再通过缓存一致性协议与主存交互
JMM规范试图定义一种Java内存模型(Java Memory)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java在各种平台下都能达到一致的内存访问效果。
JMM内存模型
1、JMM是什么
JMM本身是一种抽象的概念,并不真实存在,他仅仅描述的是一组约定与规范。
2、原则
JMM的关键技术点都是围绕多线程的
原子性
、可见性
和有序性
展开的
3、能干嘛
通过JMM来实现线程和
主内存之间的抽象关系
屏蔽各个硬件平台和操作系统的内存访问差异
以达到让Java程序在各种平台下能达到一致的内存访问效果
JMM三大特性
1、可见性
是指当一个线程修改了共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有变量都存储在主存中
系统主内存中的共享数据被修改写入的时机是不确定的,多线程并发情况下很可能引起“脏读”,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程中使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在自己的工作内存中完成,而不能够直接读写主存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,只能通过主存来完成
2、原子性
指一个操作不能被打断,及多线程环境下,操作不能被其他线程干扰
3、有序性
对于一个线程的执行代码而言,为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化的语义,即只要程序的最终结果与它顺序话执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫做指令的重排序
优缺点:
优点:
根据处理器特性(CPU多级缓存系统、多核处理器等),适当对机器指令进行重排序,使机器指令能够更符合CPU的执行特性,最大限度发挥机器性能
缺点:
多线程不会保持语义一致,可能产生脏读
多线程先行发生原则之happens-before
1、happens-before原则
1、如果一个操作happens-before另一个操作,那么第一个操作的结果将对第二个操作可见,而且第一个操作的执行顺序发生在第二个操作之前
2、两个操作之间存在happens-before关系,并不意味着一定按照happens-before原则指定的顺序来执行。如果两个重排序之后的执行结果与按照happens-before关系来执行的结果相一致,那么这种重排序并不违法。
六、volatile和JMM
1、被volatile修饰的变量有两大特性
1. 特点
可见性(对变量的修改立即可见)
有序性(修改先读后写,不会出现重复写入同一个值)
2. volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量立即刷新到主存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新的共享变量
所以volatile的写内存语义是直接刷新到主存中的,都内存语义是直接从主存中取
3. volatile凭什么可以保证可见性和有序性?
内存屏障Memory Barrier
2、内存屏障(面试重点)
1.内存屏障是什么
内存屏障(也称内存栅栏,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在
生成JVM指令时插入特定的内存屏障指令
,通过这些内存屏障指令volatile实现了JMM中的可见性和有序性,但volatile不能保证原子性
内存屏障之前的所有写操作
都要写回主存中
。
内存屏障之后的所有读操作
都能获取内存屏障之前所有写操作的最新结果
写屏障(Store Memory Barrier) | 读屏障(Load Memory Barrier) |
---|---|
告诉处理器在写屏障之前把所有存储在缓存(store bufferes)中的数据同步到主存。也就是说看到Story屏障指令,就必须把该指令之前的所有写入操作执行完毕才能继续执行下去 | 处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后,就能保证后面的读取数据指令一定能够读取到最新的数据 |
一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读
2.内存屏障分类
粗分两种:
1、读屏障
在读指令之前插入读屏障,让工作内存或CPU高速缓存中的缓存数据失效,重新回到主存中获取最新的数据
在每个volatile读操作的后面插入一个LoadLoad屏障(禁止处理器把上面的volatile读操作和下面的普通读重排序)
在每个volatile读操作的后面插入一个LoadStore屏障(禁止处理器把上面的volatile读操作和下面的普通写重排序)
2、写屏障
在写操作之后插入屏障,强制把缓冲区的数据刷回到主存中
在每个volatile写操作前面插入一个StoreStore屏障(保证在volatile写操作之前前面的所有写操作的结果都已经刷新到主存中)
在每个volatile写操作后面插入一个StoreLoad屏障(避免后面的读/写操作与volatile写重排序)
细分四种:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 保证Load1的读取操作在Load2以及其后的读操作之前执行 |
StoreStore | Store1;StoreStore;Store2 | 保证Store1的写操作在Store2以及其后的写操作前刷新到缓存 |
LoadStore | Load1;LoadStore;Store2 | 保证Load1的读操作在Store2及之后的写操作前结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证Store1的写操作在Load2及之后的的读操作前刷新到缓存 |
3、volatile特性
1.保证可见性
说明:
可见性指的是保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变,所有线程立即可见
2. 没有原子性
举例,使用多线程计算1加到100。
由上图可知,当多线程并发时,如果线程A在获得运算结果(例如5+1=6)但是未写入主存的时候,cpu被挂起,此时线程B读到了和线程A同样的值(5),并且将数据(6)写入到主存中,然后线程A继续写入主存,则会导致数据重复覆盖。
使用volatile变量需要遵守下面两条原则:
1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2、变量不需要与其他状态变量共同参与不变约束
3. 指令禁重排
没有数据依赖的操作可以重排序
有数据依赖的操作禁重排
数据依赖性:如果两个操作其中有一个操作为写操作,那么这两个操作就存在数据依赖性
4、本章总结
1. volatile可见性
2. volatile禁重排
通过volatile屏障实现
3. 为什么java写入一个volatile关键字,系统底层就加入内存屏障?
当发现是volatile操作时,会根据JMM规范,在相应的位置插入内存屏障
4. 内存屏障是什么
是一种屏障指令,它使得CPU或者编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束
5. 内存屏障能干嘛?
阻止指令前后的操作重排序
6. 内存屏障四大指令
LoadLoad、LoadStore、StoreStore、StoreLoad
7. 最终总结
volatile写之前的操作都禁止重排序到volatile之后
volatile读操作之后的操作,都禁止重排序到volatile之前
volatile写之后volatile读,禁止重排序
七、CAS(Compare and swap 比较交换 )
1、原子类 java.util.concurrent.atomic
2、没有CAS之前
多线程环境不使用原子类保证线程安全i++(基本数据类型)
3、有CAS之后
使用原子类保证线程安全
4、CAS原理
类似于乐观锁机制,有三个变量,位置内存值V,旧的预期值A,要修改的更新值B
当要把更新值写入到主存中时,会比较位于主存的位置内存值V是否与A相等,仅当V和A相等时,将内存值改为B,否则什么都不做或者重来,它这种重来的行为就称作自旋
5、谈谈你对UnSafe的理解
1. Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,所有需要本地(native)方法访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe位于sum.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因此Java的CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是用native修饰的,也就意味着Unsafe类中的方法都直接调用操作系统底层资源执行任务
2.原子引用
AtomicReference
CAS提供了一个泛型类,里面可以传入任意对象,也就意味着可以把任意对象的地址作为比较值
Demo
AtomicReference<User> userAtomicReference = new AtomicReference<>();
User zs = new User(23,"张三");
User ls = new User(25, "李四");
userAtomicReference.set(zs);
System.out.println(userAtomicReference.compareAndSet(zs, ls)+"\t"+userAtomicReference.get());
System.out.println(userAtomicReference.compareAndSet(zs,ls)+"\t"+userAtomicReference.get());
6、CAS与自旋锁,借鉴CAS思想
public class CasTest {
AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();
public static void main(String[] args) {
CasTest casTest = new CasTest();
new Thread(()->{
casTest.lock();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
casTest.unlock();
},"a").start();
System.out.println("过渡=======================");
new Thread(()->{
casTest.lock();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
casTest.unlock();
},"b").start();
}
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"----------come in");
while (!threadAtomicReference.compareAndSet(null, thread)) {
}
}
public void unlock(){
Thread thread = Thread.currentThread();
threadAtomicReference.compareAndSet(thread, null);
System.out.println(thread.getName()+"----------task over,unlock---------");
}
}
7、CAS缺点
1. 会导致ABA问题
由于CAS是通过值比较的,所以如果中途有线程将值改变,最后再将值改回来的话,当前线程仍然认为没有其他线程操作过该值,将数据回写进主存。
要解决ABA问题,引入了版本号(戳记流水)
解决办法
使用版本号原子类
AtomicStampedReference
AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(100,0);
new Thread(()->{
int stamp = asr.getStamp();
System.out.println("线程"+Thread.currentThread().getName()+"首次版本号:"+asr.getStamp());
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = asr.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println("b="+b);
boolean c = asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp() + 1);
System.out.println("c="+c);
},"a").start();
new Thread(()->{
int stamp = asr.getStamp();
System.out.println("线程"+Thread.currentThread().getName()+"首次版本号:"+asr.getStamp());
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean d = asr.compareAndSet(100, 101, stamp, stamp+1);
System.out.println("d="+d);
}).start();
2. 循环时间长,开销大
8、原子操作类
1. 基本类型原子类
1、类型
- AtomicInteger
- AtomicLong
- AtomicBoolean
2、case
public static Integer SIZE = 50;
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(0);
CountDownLatch countDownLatch = new CountDownLatch(SIZE);
for(int i=1;i<=SIZE;i++){
new Thread(()->{
try{
for(int k = 0;k<1000;k++){
atomicInteger.getAndIncrement();
}
}finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
System.out.println("线程"+Thread.currentThread().getName()+"获得结果,result="+atomicInteger.get());
2. 引用类型原子类
1、AtomicReference(可能导致ABA)
2、AtomicStampedReference(邮戳类型原子类)
使用版本号,解决ABA问题
3、AtomicMarkableReference(标记类型原子类)
记录是否被修改过,用状态戳(true/false)标记
3. 对象属性修改原子类
1、AtomicIntegerFieldUpdater
原子更新对象中int类型的值
2、AtomicLongFieldUpdater
原子更新对象中Long类型的值
3、AtomicReferenceFIeldUpdater
原子更新对象中引用类型的值
使用要求:
- 变量必须使用public volatile修饰
- 因为对象的属性修改类型原子类是抽象类,所以使用的时候必须调用静态的newUpdater()创建一个更新器,并设置想要的更新类和属性
4、Case
1、使用AtomicIntegerFieldUpdater
@Data
@AllArgsConstructor
class BankAccount{
private String bankName;
public volatile int money;
}
@Data
class BankAccountAdvice {
private BankAccount bankAccount;
AtomicIntegerFieldUpdater<BankAccount> updater;
public BankAccountAdvice(BankAccount bankAccount){
this.bankAccount = bankAccount;
updater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");
}
public void compareAndAdd(){
updater.getAndIncrement(bankAccount);
}
}
public class test{
public static void main(String[] args) throws InterruptedException {
BankAccountAdvice bankAccountAdvice = new BankAccountAdvice(new BankAccount("花旗", 0));
CountDownLatch countDownLatch = new CountDownLatch(1000);
for(int i = 0;i<1000;i++){
new Thread(()->{
try{
bankAccountAdvice.compareAndAdd();
}finally {
countDownLatch.countDown();
}
},"i"+i).start();
}
countDownLatch.await();
System.out.println(bankAccountAdvice);
}
}
9、LongAdder源码分析
LongAdder源码分析
LongAdder的笔记之前没有保存,丢失掉了,以后可能有空会补上,这篇文章讲的不错,可以参考
目前简单讲述一下LongAdder的实现原理
- 创建一个volatile的变量,名称为base,在LongAdder没有线程竞争的情况下,会在这个变量上进行操作。
- 这时来了线程竞争,源码里有个判断,如果线程cas失败了,那么就会创建cells数组,并进行初始化,不同的线程根据算法分配到不同的数组的对象上,从而分散热点,减轻压力。
- cells数组扩容的长度都是2的幂次方。
- 每个cell数组的成员进行单独cas,在调用LongAdder.sum()方法的时候,获取的是当时所有cell的成员对象+base的总和,但是这时cells中的变量可能还在进行cas操作,所以sum()方法获取的不是最终结果。
八、ThreadLocal
1、ThreadLocal简介
1. ThreadLocal是什么?
ThreadLocal提供线程局部变量。这些变量和正常变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或者事务ID)与线程关联起来
2. 能干嘛?
实现每一个线程都有
自己专属的本地变量副本
3. 代码示例
public class ThreadLocalTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
SellHouse sellHouse = new SellHouse();
try {
for(int i=0;i<10;i++){
executorService.submit(()->{
try {
int beforeInt = sellHouse.threadLocal.get();
sellHouse.getVolumn();
System.out.println("线程"+Thread.currentThread().getName()+"在执行前的threadLocal大小是:"+beforeInt+";在执行后的大小是"+sellHouse.threadLocal.get());
}finally {
sellHouse.threadLocal.remove();
}
});
}
}finally {
executorService.shutdown();
}
}
private static void code1() {
SellHouse sellHouse = new SellHouse();
for(int i=0;i<5;i++){
new Thread(()->{
try {
int sellNum = new Random().nextInt(5)+1;
for (int j = 0;j< sellNum;j++){
sellHouse.add();
sellHouse.getVolumn();
}
System.out.println("销售"+Thread.currentThread().getName()+"卖出了"+sellNum+",销售额是"+sellHouse.threadLocal.get());
} finally {
sellHouse.threadLocal.remove();
}
},String.valueOf(i)).start();
}
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("总共卖出了"+sellHouse.num);
}
}
class SellHouse{
public int num = 0;
public synchronized void add(){
num++;
}
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->{
return 0;
});
public void getVolumn(){
threadLocal.set(threadLocal.get()+1*2000);
}
}
4. 以上代码总结
如何才能不争抢
1、加入synchronized或者Lock控制资源的访问顺序
2、人手一份,大家各自安好,没必要抢夺
2、ThreadLocal源码分析
3、ThreadLocal内存泄漏问题
1. 阿里面试题
2. 什么是内存泄漏?
不会被使用的对象或者变量占用的内存不会被回收,就是内存泄漏
3. 内存泄漏的原因
1、强引用
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还”活着“,垃圾回收器就不会回收这种对象。
当一个对象被强引用时,它处于可达状态,不会被垃圾回收器回收。因此强引用是造成Java内存泄露的主要原因之一。
class StrongReference{
@Override
protected void finalize() throws Throwable {
System.out.println("finalize 被触发==========");
}
}
public class ReferenceTest {
public static void main(String[] args) {
StrongReference strongReference = new StrongReference();
System.out.println("before gc strongReference="+strongReference);
strongReference = null;
System.gc();
System.out.println("after gc strongReference="+strongReference);
}
}
2、软引用
软引用是一种相对于强引用弱化了一点的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾回收。
对于软引用对象来说:
- 当系统内存充足时,它不会被回收
- 当系统内存紧张时,它会被回收
所以软引用通常用在内存敏感的地方,如高速缓存。
class MyOjbect {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize 被触发==========");
}
}
public class ReferenceTest {
public static void main(String[] args) {
// StrongReference strongReference = new StrongReference();
// System.out.println("before gc strongReference="+strongReference);
// strongReference = null;
// System.gc();
// System.out.println("after gc strongReference="+strongReference);
SoftReference softReference = new SoftReference(new MyOjbect());
System.gc();
try {
Thread.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("内存充足,after gc softReference="+softReference);
try{
byte[] bytes = new byte[10 * 1024 * 1024 ]; //将内存调成10m(vm option -Xms10m -Xmx10m)创建一个10m的数组,此时内存紧张了
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("内存紧张,after gc softReference="+softReference.get());
}
}
}
3、弱引用
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存周期更短
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存
class MyOjbect {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize 被触发==========");
}
}
public class ReferenceTest {
public static void main(String[] args) {
WeakReference<MyOjbect> weakReference = new WeakReference<>(new MyOjbect());
System.out.println("before gc weakReference="+weakReference.get());
System.gc();
try {
Thread.sleep(1);
}catch (Exception e){
}
System.out.println("after gc weakReference="+weakReference.get());
}
}
4、虚引用
- 虚引用必须和引用队列(ReferenceQueue)联合使用
虚引用需要用java.lang.ref.PhantomReference来实现,顾名思义,就是
形同虚设
,与其他几种引用不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收机制回收,它不能单独使用,也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue一起使用)
- PhantomReference的get方法总是返回null
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后,做某些事情的通知机制。
PhantomReference的get方法总是返回null
,因此无法访问对应的引用对象
- 处理监控通知使用
设置虚引用的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现finalize机制更灵活的回收操作
class MyOjbect {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize 被触发==========");
}
}
public class ReferenceTest {
public static void main(String[] args) {
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference<>(new MyOjbect(), referenceQueue);
ArrayList<byte[]> list = new ArrayList<>();
new Thread(()->{
while (true){
byte[] bytes = new byte[ 1* 1024 * 1024];
list.add(bytes);
System.out.println("添加数组成功,phantomReference="+phantomReference.get());
try {
TimeUnit.MICROSECONDS.sleep(500);
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
Reference poll = referenceQueue.poll();
if(Objects.nonNull(poll)){
System.out.println("虚引用对象被回收,poll="+poll.get());
break;
}
}
}).start();
}
}
5、ThreadLocal为什么要使用弱引用?
因为在Entry中key是用的ThreadLocal,value用的当前线程的变量副本。
- 如果key使用的是强引用,引用ThreadLocal的对象被回收了,但是在map中一直对ThreadLocal存在强引用,那么一直不会被回收,造成内存泄漏。
- 如果key使用的是弱引用,引用ThreadLocal的对象被回收了,ThreadLocal会在垃圾回收时被回收掉,而线程变量副本则会在下次set()、get()、remove()方法中被清除。
- 注:set、get、remove方法会删除掉Entry中key为null的value
6、最佳实践
- 在一开始使用ThreadLocal.withInitial(()->初始化值)来初始化对象
- 建议把ThreadLocal修饰为static(这样保证ThreadLocal在使用过程中被类强引用,不会被清除)
- 用完记得手动remove(防止内存泄漏)
7、小总结
- ThreadLocal并不解决线程间共享数据的问题
- ThreadLocal适用于变量在线程间隔离且在方法间共享的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全问题以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收
- 对于Entry中key为null的数据,会在调用set()、get()和remove()方法时清除掉
九、Java对象内存布局和对象头
1、Object object = new Object()谈谈你对这句话的理解?一般而言JDK8按照默认情况下,new一个对象占多少内存空间
- 位置所在:JVM堆中->新生区->伊甸园区
- 构成布局:在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
2、对象在堆内存中的布局
1. 对象头
对象头分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(klass)的首地址。
在64位系统中,对象标记占8个字节,类型指针占8个字节(不考虑压缩指针的情况下,如果开启压缩指针,那么只占4个字节)
-
对象标记Mark Work
- 哈希码
- GC标记
- GC次数
- 同步锁标记
- 偏向锁持有者
- 存储结构:见下图,前25位不使用,第26位到第56位存储hashCode, 依次类推由上图可得,为什么
GC次数最高是15
?因为分配的存储分代年龄的字节只有4个,最大值就是15! - 对象标记MarkWord默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化
-
类元信息(又叫类型指针)
- 对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
-
对象头多大
2. 实例数据
存放类的属性数据信息,包括父类的属性信息
3. 对齐填充(保证8个字节的倍数)
虚拟机要求对象的起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按8字节补充对齐。
如一个对象的对象头占16个字节,其中有两个属性,一个int类型,一个boolean类型,int类型32位,占4个字节,boolean类型8位,占1个字节,那么这个时候对象头+实例数据的长度就是21个字节,对齐填充会补充到24个字节(8的整数倍),此时对其填充的长度就是3个字节
3、聊聊Object obj = new Object()
1. GC年龄采用4位bit存储,最大为15,例如MaxTenuringTreshold参数默认值就是15
2. 尾巴参数说明
压缩指针相关命令
1、在默认配置中,启动了压缩指针,-XX:+UseCompressedClassPointers。
此时对象头就不占16个字节了,而是MarkWork(8)+类型指针(4)=12个字节,当没有实例数据时,对齐填充再填充4个字节,此时对象的长度是16个字节。
可手动关闭压缩,命令:-XX:-UseCompressedClassPointers。
十、Synchronized与锁升级
对于锁升级,可以查看这篇文章
https://blog.csdn.net/qq_40722827/article/details/105598682
1、阿里及其他大厂面试题
1. 谈谈你对Synchronized的理解
2. Synchronized的锁升级
2、Synchronized的性能变化
1. java5以前,只有Synchronized,这个是操作系统级别的重量操作
-
重量级锁,如果在锁竞争激烈的情况下,性能会下降
-
Java5之前,用户态和内核态之间的切换
- java的线程是映射到操作系统的原生线程上的,如果要阻塞或者唤醒一个线程,需要操作系统介入,需要在用户态和内核态之间切换,这种切换会消耗大量的系统资源,因为用户态和内核态都有各自专用的内存空间,专用的寄存器等,用户态切换内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
- 在早期java版本中,Synchronized属于重量级锁。
2. 为什么每个对象都可以成为一个锁??
每个对象在创建的时候,都会带一个monitor,Monitor的本质是依赖底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本很高。
3、Synchronized锁种类和升级步骤
1. 升级流程
- Synchronized用的锁是存在java对象头里MarkWord中,锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
- 锁指向:
- 偏向锁:MarkWord存储的是偏向锁的线程ID;
- 轻量锁:MarkWord存储的是指向线程栈中LockRecord的指针
- 重量锁:MarkWord存储的是指向堆中的monitor对象的指针
2. 偏锁
1、是什么
偏向锁:单线程
当线程A第一次竞争到锁,通过操作修改MarkWord中的偏向线程ID、偏向模式。
如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步
2、主要作用
由于一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获取锁,不需要进行用户态、内核态进行频繁切换
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争的情况下消除了同步语句,直接提高系统性能。
3、锁的持有
锁总是被
第一个占用他的线程持有,这个线程就是锁的偏向线程
锁在第一次被拥有的时候,记录下偏向线程ID。这样偏向线程后续进入和退出同步锁的代码块时,不需要重新加锁和释放锁
,而是去对比MarkWord中的线程ID是否相同
如果相同
,那么说明是偏向锁进入,不需要再次尝试获取锁
如果不同
,则发生了竞争,这个时候会尝试CAS来替换MarkWord里面的线程ID为新线程ID.
新线程竞争成功
,表示之前的线程不存在,MarkWord里面的线程ID为新线程ID,锁不会升级,仍为偏向锁
竞争失败
,这时候可能需要升级成为轻量级锁,才保证线程间公平竞争锁
4、补充
偏向锁在Java15以后,逐步废弃
3. 轻量级锁(自旋锁)
1、主要作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短
本质就是自旋锁CAS
2、轻量级锁的获取
加入线程A已经拿到锁,这是线程B又来抢对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁。
而B在争抢时,发现对象头中MarkWord的线程ID不是自己的,这时线程B就会进行CAS操作希望能够获取锁
1、如果CAS成功,那么就会将MarkWord中的线程ID替换成自己的ID,重新偏向于B线程
2、如果争抢失败,那么偏向锁就会升级成自旋锁,而B线程会进入自旋等待获取该轻量级锁
3、锁的自旋
自旋锁的大致原理是:
线程如果自旋成功了,那么下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率成功
如果很少自旋成功,那么下次会减少自旋的次数,甚至不自旋,避免CPU空转
4. 小总结
1. 锁升级后HashCode去哪里了?
- 在无锁状态,hashCode存储在对象头的MarkWord中,当对象的hashCode()方法第一次被调用,会生成identity hash code存储在对象头中。
- 在一个索引对象调用过hashCode()方法之后,无法升级为偏向锁,因为偏向锁会用线程id覆盖掉hashCode,这种情况下,再次调用hashCode()方法,会导致一个对象前后两次获得的hashCode值不一样
- 升级为轻量级锁之后,jvm会在当前线程的栈帧中创建一个LockRecord变量,用于记录锁对象的MarkWord,其中自然也包含了分代年龄和identity hash code,因此轻量级锁和hashCode可以共存。
- 升级为重量级锁后,保存的是指向重量级锁Monitor的指针,Monitor中保存了MarkWord中的值,当锁释放时,会将这些属性写回对象头。
- 在偏向过程中遇到hash一致性要求,会立即撤销偏向状态,膨胀为重量级锁
4、JIT编译器对锁的优化
1. JIT消除
当一个锁对象没有扩散到被其他线程使用,那么从JIT的角度看,相当于无视他,synchronized(o)就不存在了
2. JIT锁粗化
假如一个方法中首尾相连,前后相邻的都是同一个锁对象,那么JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,提升了性能
十一、AQS
1. AQS入门理论储备
1、是什么?
1. 字面意思
抽象的队列同步器
2. 技术解释
是用来实现锁和其他同步组件公共基础部分的抽象实现,是
重要基础框架以及整个JUC的基石,主要解决所分配给谁的问题
整体就是抽象的FIFO队列,来完成资源获取线程的排队工作,并通过一个int变量来表示持有锁的状态。
2、AQS为什么是JUC的内容中最重要的基石
1. 与AQS理论相关的
- RetrantLock
- CountDownLaunch
- RetrantReadWriteLock
- Semaphore
- …
2. 进一步理解锁和同步器的关系
- 锁,面向锁的使用者,定义了程序员与锁交互的API,隐藏了实现细节,直接调用即可
- 同步器,面向锁的实现者。DougLea提出了统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的
公共基础部分
3、能干嘛?
1. 有阻塞就需要排队,实现排队必然需要队列
既然说到了
排队等候机制
,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要采用的是CLH队列的变体实现的,将暂时获取不到的锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。他将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发带到同步的效果
2. 解释说明
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改
4、小总结
图11.1.4.1
2. AQS源码分析前置知识储备
1、AQS内部体系架构
1. AQS的int变量
AQS同步状态成员变量 private volatile int state
2. 小总结
有阻塞就需要排队,实现排队必然需要队列
state 变量+CLH双端队列
3. Node的内部结构
1、Node的int变量
Node的等待状态waitState成员变量 volatile int waitStatus
2、结构图
3、属性说明
2、源码分析
1. 以ReentrantLock为例
ReentrantLock的默认构造方法是非公平锁,其代码实现为
如上图可见,当使用默认构造方法时,创建的是非公平锁。而是用入参为布尔值的构造方法时,当入参为true,创建公平锁,入参为false,创建非公平锁
2. 公平锁与非公平锁加锁方式的区别
对于公平锁和非公平锁,加锁的方式都是调用的lock()方法,而公平锁和非公平锁的lock()方法,分别为
//非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
//非公平锁加锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//公平锁加锁
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
由上述代码可知,对于公平锁和非公平锁,加锁的区别在于,非公平锁在调用acquire(1)之前,会尝试CAS,如果成功,就直接将锁的占用线程改为当前线程。
3. acquire(1)的详解
acquire源码,acquire是父类AbstractQueuedSynchronizer的方法,其内容为:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire中主要有3个方法:
- tryAcquire(arg)
- addWaiter(Node.EXCLUSIVE)
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
1、protected boolean tryAcqiure(int arg)
这是一个模板方法,交给子类实现,作用是尝试抢锁
tryAcquire源码:
//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
从上述代码可以看出,公平锁和非公平锁的区别在于公平锁比非公平锁多了一个判断
hasQueuedPredecessors() ,这个方法是用于检查前面有没有其他线程节点在排队。
2、addWaiter(Node.EXCLUSIVE)
从图11.1.4.1可知,线程所存储的队列是双向队列,所以addWaiter()方法的主要作用,就是创建一个新的Node节点,把当前线程的值存进去,然后添加到双向队列中
3、acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
acquireQueued(addWaiter(Node.EXCLUSIVE),arg)这个方法主要是用来尝试获取锁,如果获取不到,就使用LockSupport.park()方法进行阻塞,直到发放通行证
4. 整个ReentrantLock加锁过程
- 尝试加锁
- 加锁失败,进入队列
- 线程入队列后,进入阻塞状态
5. AQS流程图
十二、ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
1、本章路线总纲
无锁–>独占锁–>读写锁–>邮戳锁
2、关于锁的大厂面试题
1. 你知道Java里面有哪些锁?
2. 你说你用过读写锁,锁饥饿的原理是什么?
3. 有没有比读写锁更快的锁?
4. StampedLock知道吗?(邮戳锁/票据锁)
5. ReentrantReadWriteLock有锁降级机制,你知道吗?
3、请你简单聊聊ReentrantReadWriteLock
1. 是什么
1、读写锁说明
读写锁定义:一个资源能同时被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程
2、再说说演变
无锁无序–>加锁–>读写锁演变复习
3、【读写锁】意义和特点
它只允许读读共存,而读写和写写依然是互斥的
,大多实际场景是"读/读"线程间不存在互斥关系
,只有"读/写"线程或"写/写"线程间的操作需要互斥。因此引入ReentrantReadWriteLock
一个ReentrantReadWriteLock同时只能存在一个写锁,或者多个读锁,不能同时存在读锁和写锁,也即一个资源可以被多个读操作访问或者被一个写操作访问,但是两者不能共存
只有读多写少的场景下读写锁才有较高的性能体现。
2. 特点
1、可重入
2、读写兼容
3、code演示ReentrantReadWriteLockDemo
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyResource myResource = new MyResource();
for (int i = 1; i <= 10; i++) {
int finnalI = i ;
new Thread(()->{
try {
myResource.reentrantWrite(String.valueOf(finnalI),String.valueOf(finnalI));
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(finnalI)).start();
}
for (int i=1;i<=10;i++){
int finnalI = i;
new Thread(()->{
try {
myResource.reentrantRead(String.valueOf(finnalI));
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
},String.valueOf(finnalI)).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 1; i <= 3; i++) {
int finnalI = i ;
new Thread(()->{
try {
myResource.reentrantWrite(String.valueOf(finnalI),String.valueOf(finnalI));
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"新写锁==>"+String.valueOf(finnalI)).start();
}
}
public void normalLock(){
MyResource myResource = new MyResource();
for (int i = 1; i <= 10; i++) {
int finnalI = i ;
new Thread(()->{
try {
myResource.normalWrite(String.valueOf(finnalI),String.valueOf(finnalI));
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(finnalI)).start();
}
for (int i=1;i<=10;i++){
int finnalI = i;
new Thread(()->{
try {
myResource.normalRead(String.valueOf(finnalI));
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
},String.valueOf(finnalI)).start();
}
}
}
class MyResource{
Map<String,String> map = new HashMap<String,String>();
ReentrantLock reentrantLock = new ReentrantLock();
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void normalWrite(String key,String value){
try {
reentrantLock.lock();
System.out.println("线程"+Thread.currentThread().getName()+"正在写入");
map.put(key,value);
System.out.println("线程"+Thread.currentThread().getName()+"写入完成");
}finally{
reentrantLock.unlock();
}
}
public void normalRead(String key){
try{
reentrantLock.lock();
System.out.println("线程"+Thread.currentThread().getName()+"正在读取");
String s = map.get(key);
System.out.println("线程"+Thread.currentThread().getName()+"完成读取"+s);
}finally {
reentrantLock.unlock();
}
}
public void reentrantRead(String key){
try {
readWriteLock.readLock().lock();
System.out.println("线程"+Thread.currentThread().getName()+"正在读取");
String s = map.get(key);
System.out.println("线程"+Thread.currentThread().getName()+"完成读取"+s);
}finally {
readWriteLock.readLock().unlock();
}
}
public void reentrantWrite(String key,String value){
try {
readWriteLock.writeLock().lock();
System.out.println("线程"+Thread.currentThread().getName()+"正在写入");
map.put(key,value);
System.out.println("线程"+Thread.currentThread().getName()+"写入完成");
}finally{
readWriteLock.writeLock().unlock();
}
}
}
4、结论
一体两面,读写互斥,读读共享,读没有完成时候其他线程写锁无法获得
5、从写锁–>读锁,ReentrantReadWriteLock可以降级
1. 《Java并发编程的艺术》中关于锁降级的说明:
ReentrantReadWriteLock锁降级:将写入所降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),
锁的严苛程度变强叫升级,反之叫降级。
写锁的降级,降级成了读锁
- 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成了读锁。
- 规则惯例,先获取写锁,然后获取读锁,在释放写锁的次序。
- 如果释放了写锁,那么就完全转换为读锁。
2. 读写锁降级演示
锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
code演示LockDownGradingDemo
public static void main(String[] args) {
ReentrantReadWriteLock.ReadLock readLock = new ReentrantReadWriteLock().readLock();
ReentrantReadWriteLock.WriteLock writeLock = new ReentrantReadWriteLock().writeLock();
writeLock.lock();
System.out.println("写锁加锁---------------");
readLock.lock();
System.out.println("读锁加锁----------------");
writeLock.unlock();
System.out.println("写锁解锁----------------");
readLock.unlock();
System.out.println("读锁解锁---------------");
}
如果有线程在读,那么线程是无法获取写锁的,是悲观锁的策略
3. 写锁和读锁是互斥的
写锁和读锁是互斥的(这里的互斥是指的线程间的互斥,当前线程既可以获取写锁,有可以获取读锁,但是获取到了读锁之后就不能继续获取写锁),这是因为读写锁需要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么其他正在运行的线程无法感知到当前写线程的操作
4、面试题:有没有比读写锁更快的锁?邮戳锁!
1. 是什么
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化
邮戳锁也叫票据锁
stamp(戳记,long类型)代表了锁的状态。当stamp返回零时,表示线程获取锁失败并且,当释放锁或者转换所的时候,都要传入最初获取的stamp值
2. 它是由锁饥饿问题引出的
1、锁饥饿问题:
ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多,想要获取写锁就变得计较困难了,加入当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那么1个写线程就杯具了,因为当前有可能一直存在读锁,而无法获取写锁,根本没机会写
2、如何缓解锁饥饿问题?
使用"公平"策略可以一定程度上缓解这个问题 new ReentrantReadWriteLock(true)
但是"公平"策略是以牺牲系统吞吐量为代价的
3. StampedLock的特点
- 所有获取锁的方法,都需要返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
- 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
- StampedLock是不可重入的,危险(如果一个线程已经持有写锁,再去获取写锁就会造成死锁)
- StampedLock的三种访问模式:
1. Reading(悲观读模式):功能和ReentrantReadWriteLock读锁类似
2. Writing(写模式):功能和ReentrantReadWriteLock写锁类似
3. Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读
4. 乐观读模试演示
package com.tangjie.myspringproject.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;
public class StampLockDemo {
static int num = 37;
static StampedLock stampedLock = new StampedLock();
public void write(){
long l = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName()+"写线程准备修改");
try{
num = num + 17;
}finally {
stampedLock.unlockWrite(l);
}
System.out.println(Thread.currentThread().getName()+"写线程结束修改");
}
public void read(){
long l = stampedLock.readLock();
System.out.println(Thread.currentThread().getName()+"\t come in readlock code block,4 seconds continue........");
for(int i = 0;i<4;i++){
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"正在读取中.........");
}
try{
int result = num;
System.out.println(Thread.currentThread().getName()+"获得成员变量值result:"+result);
System.out.println("写线程没有修改成功,读锁时候写锁无法介入");
}finally {
stampedLock.unlockRead(l);
}
}
public void optimismRead(){
long l = stampedLock.tryOptimisticRead();
System.out.println(Thread.currentThread().getName()+"线程中,l 4s之前的值是"+l);
for (int i=0;i<4;i++){
try {
System.out.println(Thread.currentThread().getName()+"正在读取中============");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(!stampedLock.validate(l)){
System.out.println("读过程中,数据被修改,锁升级为悲观读===================");
long l1 = stampedLock.readLock();
try{
System.out.println(Thread.currentThread().getName()+"悲观锁读取中============");
int result = num;
System.out.println(Thread.currentThread().getName()+"获得成员变量值result:"+result);
System.out.println("写线程没有修改成功,读锁时候写锁无法介入");
}finally {
stampedLock.unlockRead(l1);
}
}
}
public static void main(String[] args) {
StampLockDemo stampLockDemo = new StampLockDemo();
new Thread(()->{
stampLockDemo.optimismRead();
},"readThread").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
stampLockDemo.write();
},"writeThread").start();
}
}
5. StampedLock的缺点
- StampedLock不支持重入,没有Re开头
- StampedLock的悲观读锁和写锁都不支持条件变量(Conditon),这个也需要注意
- 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法