什么是JMM?
JMM:Java Memory Model Java内存模型,不是一个真实的存在的东西,它是一个概念,约定!
关于JMM的一些同步的约定:
线程在实际的运行时,是有工作内存和主内存两个概念的。
实际的运行图示:
内存交互操作有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操作之前,必须把此变量同步回主内存
对于刚才上面的图示,我们不难发现,当我们其中一个线程修改了主存里面Flag的值以后,其他的线程是没有办法及时获取的,以至于出现了信息不对称的情形。
具体的代码实现:
package pers.mobian.jmm;
import java.util.concurrent.TimeUnit;
public class JMMTest {
private static int num = 1;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num == 1 ) {
//循环体
}
}).start();
//确保让开启的其他线程先执行
TimeUnit.SECONDS.sleep(2);
//修改变量的值,观察线程的变化
num = 0;
System.out.println(num);
}
}
总结:我们的结果会打印0以后,就陷入了一个死循环。即可以理解为每一个线程将主存的变量都转换为了自己的私有变量,以至于内部修改,外部依然没有变化。
请你谈谈你对Volatile的理解
将我们的代码修改为:
package pers.mobian.jmm;
import java.util.concurrent.TimeUnit;
public class JMMTest {
//添加volatile关键字,保证了变量的可见性
private static volatile int num = 1;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num == 1 ) {
}
}).start();
TimeUnit.SECONDS.sleep(2);
num = 0;
System.out.println(num);
}
}
按照JMM的概念分析,理论上只要我们的线程信息交换足够快,那么就可以完成结果为1000的运算。可是现实是很难做到。当我们添加了volatile关键字以后,依然不起作用,即无法保证我们运算的原子性。
测试代码:
package pers.mobian.jmm;
import java.util.concurrent.TimeUnit;
public class VoTest01 {
private static volatile int num = 0;
//加synchronized锁肯定是可以的
private static void add() {
num++;
}
public static void main(String[] args) throws InterruptedException {
//开启10个线程,每一个线程中执行100次方法
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 100; i1++) {
add();
}
}).start();
}
//避免我们开启的线程还没有结束就执行主线程
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(num);
}
}
我们反编译该类的.class文件javap -c VoTest01.class
,可以得到其对应的字节码文件,通过字节码文件分析,一个简单的num++,需要经历三步,所以其不是原子性。
如果不添加lock锁和synchronized锁,如何保证原子性呢?
我们可以使用原子类
测试代码:
package pers.mobian.jmm;
import java.util.concurrent.atomic.AtomicInteger;
public class VoTest02 {
//使用原子类
private static AtomicInteger num = new AtomicInteger();
private static void add() {
//使用原子类的增加方法
num.getAndIncrement();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 100; i1++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(num);
}
}
使用源自类的对象进行增加就不再是简单的+1了,这些类的底层直接和OS相关,直接在内存中修改值。其底层中很关键的一个类就是Unsafe类,它是一个很特殊的存在
指令重排:我们写的程序,计算机并不一定是按照我们写的顺序执行的。
例如:
int a = 2; //1
int b = 3; //2
a = a + 1; //3
b = a + a; //4
我们编写代码的顺序是:1234。但是执行的顺序却有可能是1324、2134
数据a b c d的默认值都是0
指令重排前的结果:a=0、b=0
线程A | 线程B |
---|---|
a=c | d=c |
c=2 | c=5 |
指令重排后的结果:a=5、b=2
线程A | 线程B |
---|---|
d=2 | a=5 |
a=c | b=d |
指令重排在逻辑上是存在的
volatile如何保证我们的指令重排?
使用内存屏障(可以理解为CPU指令):
综上所述:volatile可以保证可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象
五种单例模式详解
编写一个CAS的测试代码:
package pers.mobian.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest01 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//期望、更新
//public final boolean compareAndSet(int expect, int update)
//达到期望值就跟新数据,否则不更新, 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());
}
}
点到getAndIncrement方法的源码中:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
public final int getAndIncrement() {
//参数列表:当前对象,当前对象在内存中的值,数字1
return U.getAndAddInt(this, VALUE, 1);
}
}
我们都知道Java无法直接操作内存,但是Java有关键字navite,可以调用C++方法,C++可以可以操作内存。但是Java给自己留了一个后门,可以通过Unsafe类操作内存
Unsafe类的对应源码:这是一个自旋锁
@HotSpotIntrinsicCandidate
//参数列表:当前对象,当前对象在内存中的值,数字1
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//offset:field/element offset
//获取对象 o 的地址偏移值,再赋值给 v
v = getIntVolatile(o, offset);
//如果我的对象 o 加上地址偏移值还是等于 v ,那么我们就直接+1
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
总结:
CAS可以直接比较工作内存中的值和主内存中的值,如果这个值是期望的,那么就执行操作!如果不是就一直循环
缺点:
ABA问题(狸猫换太子)
可以理解为线程B在很快的时间内执行了两次值的交换,且这一切都是在A线程执行之前。虽然数据进行了交换,但是A线程却不知道。
package pers.mobian.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest02 {
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());
}
}
执行结果:
true
2021
true
2020
true
6666
通过结果,我们可以再次发现,我们修改了值,但是cas依然可以比较交换
使用这种原子操作,就可以解决CAS中的ABA问题。其思想就是增加一个乐观锁,此处是添加一个版本号
测试代码:
package pers.mobian.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASTest03 {
//实际的业务开发中,我们引用的可能是一个对象
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//参数:期望值,更新值,当前的版本号,增加对应的版本号
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
//打印我们的最近的一个版本信息
System.out.println("a2=>" + atomicStampedReference.getStamp());
//打印更新值是否成功,返回一个指定的布尔值
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
//打印我们的最近的一个版本信息
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
//打印更新值是否成功,返回一个指定的布尔值
//由于版本信息被悄悄地修改过,所以这里返回的是false
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
执行结果:
b1=>1
a1=>1
a2=>2
true
a3=>3
false
b2=>3
补充:Integer 使用了对象缓存机制,默认范围是 128 ~ 127推荐使用静态工厂方法 valueOf 获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间;
公平锁: 非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认都是非公平)
//默认使用非公平锁
ReentrantLock reentrantLock = new ReentrantLock();
//传入true,使用公平锁
ReentrantLock reentrantLock2 = new ReentrantLock(true);
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
也叫递归锁。
可以理解为你拿着你家大门的钥匙,你只要打开了大门的锁,就可以自动打开房间里面的不同卧室的锁。
synchronized版本
package pers.mobian.relock;
public class ReLockTest01 {
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");
}
}
lock版本
package pers.mobian.relock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReLockTest02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(() -> {
phone.sms();
}, "A").start();
new Thread(() -> {
phone.sms();
}, "B").start();
}
}
class Phone2 {
Lock lock = new ReentrantLock();
public void sms() {
//这是第一把锁,用于锁sms方法
lock.lock();
//这是第二把锁,用于锁call方法
//当然我们也可以直接在call方法里面添加一把锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "=>sms");
call();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
lock.unlock();
}
}
public void call() {
try {
System.out.println(Thread.currentThread().getName() + "=>call");
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
}
注意:添加的锁一定要配对出现,不然会出现死锁现象
前面我们也有提到一个自旋锁(SpinLock)
不断地尝试,直到成功为止
@HotSpotIntrinsicCandidate
//参数列表:当前对象,当前对象在内存中的值,数字1
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//offset:field/element offset
//获取对象 o 的地址偏移值,再赋值给 v
v = getIntVolatile(o, offset);
//如果我的对象 o 加上地址偏移值还是等于 v ,那么我们就直接+1
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
我们自定义一个自旋锁:
package pers.mobian.relock;
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
//传递的参数类型是引用数据类型:默认值为null
//传递的参数类型是基本数据类型int:默认值是0
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);
}
}
测试代码:
package pers.mobian.relock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class SpinLockTest01 {
public static void main(String[] args) throws InterruptedException {
// 我们传统的使用别人的锁的方式
// ReentrantLock reentrantLock = new ReentrantLock();
// reentrantLock.lock();
// reentrantLock.unlock();
//使用我们自定义锁
SpinLockDemo lock = new SpinLockDemo();
new Thread(() -> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(4);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
}, "T1").start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
}, "T2").start();
}
}
执行结果:
T1=>myLock
T2=>myLock
T1=>myUnLock
T2=>myUnLock
总结:我们的T1会先进去拿到锁,然后又修改对应的信息为thread的下一个版本号,此时不需要进入自旋锁。就在此时,T2又拿到了它的锁,但是此时我们的自旋锁里面的thread的版本号已经不再是null,所以会进入自旋锁,不断循环。当T1释放锁时,其对应地解锁会将thread的版本号再次变成null,此时在自旋锁里面的T2就可以离开,最终释放锁。
测试案例:
package pers.mobian.relock;
import java.util.concurrent.TimeUnit;
public class DeadLockTest01 {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
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);
}
}
}
}
解决(排错)死锁的方法:
1、在IDEA的使用jsp -l
查看对应的进程号
2、使用jstack 进程号
找到死锁的问题
继而达到排错的效果
排错的方式:查看日志、查看堆栈信息