1. JMM
JMM是java内存模型,不存在的东西,是一种约定,概念。
1.1 关于JMM的一些同步的约定:
1.线程解锁前,必须把共享变量立刻刷回到主存。
2.线程加锁前,必须读取主存中的最新值到工作内存中。
3.加锁和解锁是同一把锁。
1.2 JMM内存交互的8个操作:
锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:
- 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
- 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。
1.3 并发编程三个重要特性
原子性
一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
在 Java 中,可以借助**synchronized
、各种 Lock
以及各种原子类实现原子性**。
synchronized
和各种 Lock
可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile
或者final
关键字)来保证原子操作。
可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
在 Java 中,可以借助**synchronized
、volatile
以及各种 Lock
实现可见性。**
如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
有序性
由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
在 Java 中,volatile
关键字可以禁止指令进行重排序优化。
2. volatile 关键字
Volatile是java虚拟机提供轻量级的同步机制
1.保证可见性
2.不保证原子性
3.禁止指令重排
2.1 如何保证变量的可见性?
在 Java 中,volatile
关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile
关键字能保证数据的可见性,但不能保证数据的原子性,volatile
变量的写操作和读操作之间是可以被中断的,这意味着在读取或者修改 volatile
变量的过程中,其他线程可能会对这个变量进行修改。synchronized
关键字两者都能保证。
2.2 如何禁止指令重排序?
指令重排:简单来说就是你写的程序,计算机并不是按照你写的那样去执行的。
源代码—>编译器优化的重排—>指令并行也可能会重排—>内存系统也会重排 —>执行
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
在 Java 中,Unsafe
类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
public native void loadFence();
public native void storeFence();
public native void fullFence();
理论上来说,你通过这个三个方法也可以实现和volatile
禁止重排序一样的效果,只是会麻烦一些。
以一个常见的面试题为例讲解一下 volatile
关键字禁止指令重排序的效果。
双重校验锁实现对象单例(线程安全) :
public class Singleton{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){//可以去掉,但是有问题,每个线程都来抢锁,效率低
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
instance
采用 volatile
关键字修饰也是很有必要的, instance = new Singleton();
这段代码其实是分为三步执行:
- 为
instance
分配内存空间 - 初始化
instance
- 将
instance
指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance
() 后发现instance
不为空,因此返回instance
,但此时 instance
还未被初始化。
但是通过反射可以破坏上述的单例模式:
Singleton instance = Singleton.getInstance();
//通过反射获取构造器
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
//无视了私有构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
Singleton instance1 = declaredConstructor.newInstance();
System.out.println(instance1==instance);//false
解决:
public class Singleton{
private volatile static Singleton instance;
private static boolean flag = false;
private Singleton(){
synchronized (Singleton.class){
if (flag == false){
flag = true;
}else {
throw new RuntimeException("不要用反射破坏");
}
}
}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
2.3 如果不加synchronized和lock,怎么保证原子性?
使用原子类,解决原子性问题。它的底层用的是CAS。这些类的底层直接和操作系统挂钩的!在内存中修改值!Unsafe类是一个很特殊的存在。
private volatile static AtomicInteger num = new AtomicInteger();
3. 乐观锁和悲观锁
3.1 什么是悲观锁?使用场景是什么?
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
像 Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
悲观锁通常多用于写比较多的情况下(多写场景),避免频繁失败和重试影响性能。
3.2 什么是乐观锁?使用场景是什么?
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
在 Java 中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。
3.3 如何实现乐观锁?
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。
版本号机制
一般是在数据表中加上一个数据版本号 version
字段,表示数据被修改的次数。当数据被修改时,version
值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version
值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version
值相等时才更新,否则重试更新操作,直到更新成功。
CAS 算法
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
例子:
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//期望 更新
//public final boolean compareAndSet(int expectedValue, int newValue)
//如果期望的值达到了,那么就更新,否则不更新 CAS 是CPU的并发原语!
System.out.println(atomicInteger.compareAndSet(2020,2021));
System.out.println(atomicInteger.get());
atomicInteger.getAndIncrement();
System.out.println(atomicInteger.compareAndSet(2020,2021));
System.out.println(atomicInteger.get());
}
}
自旋锁:
public class SpinlockDemo {
//Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==>myLock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){
}
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==>myUnLock");
atomicReference.compareAndSet(thread,null);
}
}
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
CAS 涉及到三个操作数:
- V :要更新的变量值(Var)
- E :预期值(Expected)
- N :拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
举一个简单的例子 :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
- i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
- i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。
3.4 乐观锁存在哪些问题?
ABA 问题是乐观锁最常见的问题。狸猫换太子
ABA 问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//捣乱的线程:
System.out.println(atomicInteger.compareAndSet(2020,2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021,2020));
System.out.println(atomicInteger.get());
//期望的线程
System.out.println(atomicInteger.compareAndSet(2020,6666));
System.out.println(atomicInteger.get());
}
}
带版本号的原子引用!
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference
类就是用来解决 ABA 问题的,其中的 compareAndSet()
方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public class CASDemo {
static AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1,1);
public static void main(String[] args) {
//AtomicInteger atomicInteger = new AtomicInteger(2020);
new Thread(()->{
int stamp = reference.getStamp();//获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(reference.compareAndSet(1,2,
reference.getStamp(),reference.getStamp() + 1));
System.out.println("a2=>" + reference.getStamp());
System.out.println( reference.compareAndSet(2,1,
reference.getStamp(),reference.getStamp() + 1));
System.out.println("a3=>" + reference.getStamp());
},"a").start();
new Thread(()->{
int stamp = reference.getStamp();
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(reference.compareAndSet(1,6,
reference.getStamp(),reference.getStamp() + 1));
System.out.println("b2=>" + reference.getStamp());
},"b").start();
}
}
循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
这是由于CAS的自旋锁问题。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:
- 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
- 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference
类把多个共享变量合并成一个共享变量来操作。
4. synchronized 关键字
4.1 synchronized 是什么?有什么用?
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
本质就是队列加锁!
同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。
4.2 如何使用 synchronized?
synchronized
关键字的使用方式主要有下面 3 种:
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
修饰实例方法 (锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
synchronized void method() {
//业务代码
}
修饰静态方法 (锁当前类)
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() {
//业务代码
}
静态 synchronized
方法和非静态 synchronized
方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁。
synchronized(this) {
//业务代码
}
总结:
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁;synchronized
关键字加到实例方法上是给对象实例上锁;- 尽量不要使用
synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能。
4.3 构造方法可以用 synchronized 修饰么?
先说结论:构造方法不能使用 synchronized 关键字修饰。
构造方法本身就属于线程安全的,不存在同步的构造方法一说。
4.4 synchronized锁的实现
synchronized 同步代码块
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
当执行 monitorenter
指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
对象锁的的拥有者线程才可以执行 monitorexit
指令来释放锁。在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized 修饰方法
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
总结
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,知道该锁被释放。
不过两者的本质都是对对象监视器 monitor 的获取。
4.5 JDK1.6 之后的 synchronized 底层做了哪些优化?
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
锁消除:在sychronized修饰的代码中,如果不存在操作临界资源的情况下,会触发锁消除,即便写了sychronized,也不会触发。
public synchronized void method(){
//没有操作临界资源
}
锁膨胀:如果在一个循环中,频繁的获取和释放资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。
public void method(){
for (int i = 0; i < 1000000; i++) {
synchronized (test5.class){
}
}
}
//触发锁碰撞
synchronized (test5.class){
for (int i = 0; i < 1000000; i++) {
}
}
锁升级: ReentrantLock是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。
sychronized在1.6之前获取不到锁就会立即挂起当前线程。
在1.6做的升级优化:
- 无锁,匿名偏向:当前对象没有作为锁存在,没有对象持有。
- 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断当前指向的线程是否是当前线程。
- 如果是,直接拿着锁资源走。
- 如果当前线程不是它,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争资源)
- 轻量级锁:会采用自旋的方式去频繁的以CAS的形式获取锁资源。(采用自适应自旋锁)
- 成功,拿着锁资源走
- 如果自旋了一定次数,没拿到锁资源,锁升级。
- 重量级锁:最传统的sychronized方式,拿不到锁资源,就挂起当前线程。
自适应锁可以理解为:当上一个线程经过多次CAS后成功拿到锁,那么当前线程就会多执行几次CAS。如果上一个线程失败了,就少几次CAS。
4.6 synchronized 底层原理
先要对Java对象在堆内存的存储有一个了解。
展开MarkWord:以HotSpot虚拟机为例
MarkWord中标记着四种锁的信息:无锁,偏向锁,轻量级锁,重量级锁。
在无锁状态下,最低位置、会用三个比特位标记着当前锁状态。001
如果是一个偏向锁,则会用54个比特位指向当前线程,同时末尾三个比特位会存储101代表偏向锁。
如果升级为轻量级锁,末尾比特位存放00代表轻量级锁,同时当前线程的信息都压在Lock Record栈中。
如果升级为重量级锁,末尾比特位存放10代表轻量级锁,同时当前线程的信息存在ObjectMonitor中,会把里面的owner设置为持有锁的线程,拿锁失败的都会放在_cxq这个单向链表中,以及EntryList中。当释放以后,EntryList中的线程就会竞争资源了。
ObjectMonitor() {
_header = NULL;
_count = 0; //锁计数器
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
所以sychronized基本上是通过两个队列WaitSet和EntryList实现的,处于wait状态的线程,会被加入到WaitSet,处于等待锁block状态的线程,会被加入到EntryList。用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),owner指向持有ObjectMonitor对象的线程。
当多个线程同时访问一段同步代码时,首先会进入EntryList 集合,当线程获取到对象的monitor 后进入 Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
4.7 synchronized 和 volatile 有什么区别?
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
5. ReentrantLock
5.1 ReentrantLock 是什么?
ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock
里面有一个内部类 Sync
,Sync
继承 AQS(AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的。Sync
有公平锁 FairSync
和非公平锁 NonfairSync
两个子类。
ReentrantLock
默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
使用lock锁三部曲:
1.new ReentrantLock();
2.lock.lock();加锁
3.finally => lock.unlock();解锁
5.2 公平锁和非公平锁有什么区别?
公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
非公平锁 :锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
5.3 lock接口
5.3 synchronized 和 ReentrantLock 有什么区别?
两者都是可重入锁
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
JDK 提供的所有现成的 Lock
实现类,包括 synchronized
关键字锁都是可重入的。
在下面的代码中,method1()
和 method2()
都被 synchronized
关键字修饰,method1()
调用了method2()
。
public class ReentrantLockDemo {
public synchronized void method1() {
System.out.println("方法1");
method2();
}
public synchronized void method2() {
System.out.println("方法2");
}
}
由于 synchronized
锁是可重入的,同一个线程在调用method1()
时可以直接获得当前对象的锁,执行 method2()
的时候可以再次获取这个对象的锁,不会产生死锁问题。假如synchronized
是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 method2()
时获取锁失败,会出现死锁问题。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized
是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized
关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReentrantLock
是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized
,ReentrantLock
增加了一些高级功能。主要来说主要有三点:
- 等待可中断 :
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁 :
ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 - 可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
关于 Condition
接口的补充:
Condition
是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock
对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是
Condition
接口默认提供的。而
synchronized
关键字就相当于整个Lock
对象中只有一个Condition
实例,所有的线程都注册在它一个身上。如果执行notifyAll()
方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而
Condition
实例的signalAll()
方法,只会唤醒注册在该Condition
实例中的所有等待线程。
或者问Synchronized和Lock区别:
1. Synchronized内置的关键字,Lock是一个java类。
2. Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁。
3. Synchronized会自动释放锁,Lock必须手动释放锁!如果不释放,则死锁!
4. Synchronized 线程1(获得锁,阻塞),线程2(等待,傻傻的等)!;lock锁不一定会等待下去!( lock.tryLock() )
5. Synchronized 可重入锁,不可以中断,非公平;Lock 可重入 可以判断锁,非公平(可以自己设置)
6. Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的代码同步问题。
传统的生产者消费者问题:
public class A {
public static void main(String[] args) {
Date date = new Date();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
date.increase();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
date.decrease();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
}
}
class Date{
private int number = 0;
public synchronized void increase() throws InterruptedException {
if (number != 0){
this.wait();
}
number++;
this.notify();
System.out.println(Thread.currentThread().getName() + "=>" + number);
}
public synchronized void decrease() throws InterruptedException {
if (number == 0){
this.wait();
}
number--;
this.notify();
System.out.println(Thread.currentThread().getName()+ "=>" + number);
}
}
如果线程多了就有问题了。
使用JUC来实现生产者消费者问题:
代码实现:
public class B {
public static void main(String[] args) {
Date2 date = new Date2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
date.increase();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
date.decrease();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
}
}
class Date2{
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private int number = 0;
public void increase() throws InterruptedException {
lock.lock();
try {
while (number != 0){
condition.await();//等待,和this.wait()方法效果一样
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll();//通知,唤醒全部
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void decrease() throws InterruptedException {
lock.lock();
try {
while (number == 0){
condition.await();//等待,和this.wait()方法效果一样
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll();//通知,唤醒全部
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
5.4 可中断锁和不可中断锁有什么区别?
可中断锁 :获取锁的过程中可以被中断,不需要一直等到获取锁之后才能进行其他逻辑处理。ReentrantLock
就属于是可中断锁。
不可中断锁 :一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized
就属于是不可中断锁。
5.5 读写锁 ReadWriteLock
/**
* 独占锁(写锁):一次只能被一个线程占有
* 共享锁(读锁):多个线程可以同时占有
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
class MyCacheLock{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private volatile Map<String,Object> map = new HashMap<>();
//存,写
public void put(String key,Object value){
lock.writeLock().lock();//写锁
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入OK");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.writeLock().unlock();
}
}
//取,读
public void get(String key){
lock.readLock().lock();//读锁
try {
System.out.println(Thread.currentThread().getName() + "读入" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读入OK");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.readLock().unlock();
}
}
}
6. 不安全集合
public class ListTest {
public static void main(String[] args) {
//并发下 ArrayList 不安全的
List<String> list = new ArrayList<>();
//ConcurrentModificationException并发修改异常
for (int i = 1; i <= 1000; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
解决:
List<String> list = Collections.synchronizedList(new ArrayList<>());
JUC:
//CopyOnWrite写入时复制
//多个线程调用的时候 list 读取的时候,固定的,写入(覆盖)
//在写入的时候避免覆盖,造成数据问题!用的lock锁!
List<String> list = new CopyOnWriteArrayList<>();
public class ListTest {
public static void main(String[] args) {
//并发下 ArrayList 不安全的
Set<String> set = new HashSet<>();
//ConcurrentModificationException并发修改异常
for (int i = 1; i <= 1000; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
解决:
Set<String> set = new CopyOnWriteArraySet<>();
如果是HashMap的话,
Map<> map = new ConcurrentHashMap<>();
7. ForkJoin
Forkjoin:并行执行任务!提高效率。大数据量!
把大任务分成小任务!
Forkjoin特点:工作窃取。
这个里面维护的都是双端队列。
ForkJoin操作:
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
//临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start,Long end){
this.start = start;
this.end = end;
}
//计算
@Override
protected Long compute() {
if ((end - start)<temp){
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else {//forkJoin
Long middle = (start + end)/2;//中间值
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(start, middle);
forkJoinDemo1.fork();//拆分任务,把任务压入线程队列
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle + 1, end);
forkJoinDemo2.fork();//拆分任务,把任务压入线程队列
return forkJoinDemo1.join() + forkJoinDemo2.join();
}
}
}
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);