16.JMM
请你谈谈你对Volatile的理解
只要是谈到Volatile,说出下方的这几个特点:
Volatile是 Java 虚拟机提供的轻量级的同步机制。
1.保证可见性
2.不保证原子性
3.禁止指令重排
说到可见性,那一定要谈JMM
什么是JMM?
JMM:Java内存模型,不存在的东西,只是一个概念!约定!
关于JMM的一些同步约定:
1.线程解锁前,必须把共享变量立刻刷回主存。
2.线程加锁前,必须读取主存中的最新值到工作内存中!
3.加锁和解锁是同一把锁!
线程、工作内存、主内存之间的工作关系:8种操作!如下图所示
同样,线程B如果使用Flag数据,也会经历上述8步操作!
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
-
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
-
-
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
-
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
-
不允许一个线程将没有assign的数据从工作内存同步回主内存
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
-
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
-
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
-
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
-
对一个变量进行unlock操作之前,必须把此变量同步回主内存
-
17.Volatile
当没有使用Volatile关键字时,看下述代码:
//下述代码有一个main线程+一个Thread线程,Thread线程会根据num = 0这个值执行while循环,下述的main线程之后会修改num的值为1,但是测试时Thread线程仍然始终一直执行,没有停止,这就是因为main线程修改num值之后并同步到主存中,但是Thread线程的工作内存没有同步num的最新值,即Thread线程的工作内存中的num仍然为0,所以循环仍然一直执行
public class JmmDemo {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{ //该线程对主内存的变化是不可知的,解决该问题的方法就是给num字段用volatile修饰
while (num==0){
}
}).start();
TimeUnit.SECONDS.sleep(2);
num = 1;
System.out.println(num);
}
}
下图就是产生上述原因的关系图:
所以我们需要Thread线程知道主内存中的值所发生的变化!
1.保证可见性
上述代码Volatile可以解决可见性问题!
也就是给num字段加上volatile字段修饰。
private volatile static int num = 0;
2.不保证原子性
原子性:不可分割!
加了volatile关键字也不保证原子性!那怎么样可以保证原子性呢?答:使用synchornized或Lock
package com.codeyu.volatile_;
import java.util.concurrent.TimeUnit;
/**
* volatile关键字不保证原子性!
*/
public class DmDemo01 {
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) throws InterruptedException {
//volatile关键字不保证原子性!所以结果仍然不为20000
//理论上num结果应该为20000,因为20个线程,每个线程执行++操作1000次
for(int i=1; i<=20; i++){
new Thread(()->{
for(int j=0; j<1000; j++){
add();
}
}).start();
}
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"="+num);
}
}
上方add方法为何不安全,因为num++操作不是原子性操作,num++实际上是三个操作,1.将数据从内存拿出;2.+1操作;3.将+1后的值再写回原来的内存位置!
如果不准使用synchornized或Lock,还怎样保证原子性操作呢?
答:使用原子类解决原子性问题!
package com.codeyu.volatile_;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile关键字不保证原子性!
*/
public class DmDemo01 {
//volatile不保证原子性
//所以使用原子类:AtomicInteger
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
//num++;
num.getAndIncrement(); //AtomicInteger的+1方法
}
public static void main(String[] args) throws InterruptedException {
//volatile关键字不保证原子性!所以结果仍然不为20000
//理论上num结果应该为20000,因为20个线程,每个线程执行++操作1000次
for(int i=1; i<=20; i++){
new Thread(()->{
for(int j=0; j<1000; j++){
add();
}
}).start();
}
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"="+num);
}
}
//命令行打印输出:20000
原子类的底层都是直接和操作系统挂钩!在内存中改值,所以可能现在理解不了!
3.禁止指令重排
什么是指令重排?
答:你写的程序,计算机并不是按照你写的那样去执行的!
程序员所写的源代码-(会经历)->编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>最终执行
处理器在进行指令重排的时候,会考虑:数据之间的依赖性!
int x = 1; //1
int y = 2; //2
x = x + 5; //3
y = x * x; //4
我们所期望的程序执行步骤是:1234,但是可能执行的时候就会变成:2143 或 1324 (这写也都是可以的,不对最终结果造成影响)
但不能是:4123。因为4123会造成错误的结果!
指令重排可能会造成的影响:a b x y 初始值都是0;
线程A | 线程B |
---|---|
x=a | y=b |
b=1 | a=2 |
正常的结果:x=0,y=0;但是可能由于指令重排
线程A | 线程B |
---|---|
x=a | y=b |
b=1 | a=2 |
指令重排导致的诡异结果:x=2;y=1;
volatile关键字可以避免指令重排!
volatile是可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
18.彻底玩转单例模式
1.懒汉模式
2.饿汉模式
19.深入理解CAS
什么是CAS?
大厂你必须要深入研究底层!有所突破!
原子类的底层就是用的CAS。
package com.codeyu.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
//CAS:compareAndSet 比较并交换(CAS是cpu的并发原语!)
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//public final boolean compareAndSet(int expectedValue, int newValue)
//如果期望的值(expectedValue)是2020,那么就将值更新为2021
atomicInteger.compareAndSet(2020,2021);
System.out.println(atomicInteger.get());
//因为上方已经修改到了2021,所以下方这句代码会失败,返回false
System.out.println(atomicInteger.compareAndSet(2020, 2021));
}
}
Unsafe类
CAS:比较当前工作内存中的值和主存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,因为底层是自旋锁!
缺点:
1.由于底层是自旋锁,循环会耗时
2.一次性只能保证一个共享变量的原子性
3.存在ABA问题
CAS:ABA问题(狸猫换太子)
21.各种锁的理解
1.公平锁、非公平锁
公平锁:非常公平,不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队!
ReentrantLock reentrantLock = new ReentrantLock();
//ReentrantLock()默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//但也可以改为公平锁
ReentrantLock reentrantLock = new ReentrantLock(true);
//加入true参数
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.可重入锁
可重入锁:拿到外面的锁之后,就可以拿到里面的锁,自动获取!
package com.codeyu.lock;
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone {
public synchronized void sms() {
System.out.println(Thread.currentThread().getName()+"=>sms");
call(); //这里也有锁
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"=>call");
}
}
//输出打印:
A=>sms
A=>call
B=>sms
B=>call
上述代码即使首先发短信sms()方法获得锁,但是因为sms()方法调用了call()方法,所以它同样会获得call()方法的锁!即拿到外面的锁之后,就可以拿到里面的锁,自动获取!
Lock版也可实现上述功能!但是Lock看起来是两把锁,一把外面的锁,一把里面的锁:
package com.codeyu.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone {
Lock lock = new ReentrantLock();
public void sms() {
lock.lock();
//lock锁必须配对,否则就会死在里面。即lock.lock();和lock.unlock();配对
try {
System.out.println(Thread.currentThread().getName()+"=>sms");
call(); //这里也有锁
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"=>call");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
3.自旋锁
spinlock:自旋锁
下方为Unsafe类红的方法:
自定义自旋锁:
package com.codeyu.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
//使用java实现的锁
// ReentrantLock reentrantLock = new ReentrantLock();
// reentrantLock.lock();
// reentrantLock.unlock();
//使用自己写的锁
SpinlockDemo lock = new SpinlockDemo();
new Thread(()->{
lock.myLock();
try {
//业务代码
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"A").start();
TimeUnit.SECONDS.sleep(3);
new Thread(()->{
lock.myLock();
try {
//业务代码
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"B").start();
}
}
A==>myLock
B==>myLock
A==>myLock
B==>myLock
4.死锁
死锁测试:
package com.codeyu.lock;
import java.util.concurrent.TimeUnit;
public class DeadLock {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
/**
* 线程T1和线程T2构造方法的参数反过来
* 所以实际上T1线程最开始会拿到MyThread类中的lockA对象的锁,然后睡眠2s
* 在此时,T2线程最开始会拿到MyThread类中的lockB对象的锁(因为两个线程构造参数返过来了)
* 之后T2线程睡眠2s,之后两线程醒后就会争夺对方的资源,从而死锁!
*/
new Thread(new MyThread(lockA,lockB),"T1").start();
new Thread(new MyThread(lockB,lockA),"T2").start();
}
}
class MyThread implements Runnable {
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get"+lockA);
}
}
}
}
//输出打印:
T1lock:lockA=>getlockB
T2lock:lockB=>getlockA
之后,程序就会暂停!
排查解决死锁问题:
1.使用jps -l
定位进程号(直接在idea的命令行使用即可)
2.使用jstack + 进程号
查看进程信息
面试工作中排查问题:
1.日志
2.堆栈信息