一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
JMM
内存模型可以理解为在特定的操作协议下, 对特定的内存或者高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型,Java虚拟机(jvm)是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)
线程/工作内存/主内存 三者的关系
- 所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问
- 每个线程都有一个独立的工作内存,用于存储线程私有的数据
- 线程对变量的操作必须在工作内存中进行(线程安全问题的根本原因)
a. 首先要将变量从主内存拷贝到线程的工作内存中, 不允许直接操作主内存中的变量
b. 每个线程操作自己工作内存中的变量副本,操作完成后再将变量写回主内存
c. 多个线程对一个共享变量进行修改时,都是对自己工作内存中的副本进行操作,相互不可见, 所以主内存最后得到的结果是不可预知的
d. 不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
JMM的三个特征
可见性
在Java中,不同线程拥有各自的私有工作内存,当线程需要修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到该线程的工作内存中(变量副本),当该线程修改其变量副本后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值,
原因: 工作内存和主内存存在同步延迟
解决方案:
- volatile: 可以保证内存可见性
① 规定线程每次修改变量副本后立刻同步到主内存中
② 规定线程每次使用变量前,先从主内存中拷贝最新的值到工作内存中,用于保证能看见其它线程对变量修改的最新值
-
synchronize: 同步锁
-
Lock锁
注意:
volilate可以保证可见性,但是假设多线程同时在做a++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2
public class Test {
private static boolean flag = true;
public static void main(String[] args) {
//创建一个线程并启动
new Thread(new Runnable() {
@Override
public void run() {
while(flag){
//System.out.println("=============");
}
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}
}
有序性
在单线程程序中代码逐行执行,但是在多线程并发时,程序的执行就有可能出现乱序,
多线程执行程序时, 为了提高性能,编译器和处理器常常会自动对指令做重排序,目的是进行相关的优化, 指令重排序使得代码在多线程执行时可能会出现一些问题
解决方案:
- volatile: 可以保证有序性
在指令序列中插入内存屏障(一组处理器指令) , 防止指令重排序
-
synchronize: 同步锁
-
Lock锁
原子性
原子性指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个线程的操作一旦开始,就不会被其他线程干扰, 只能当前线程执行完, 其他线程才可以执行
解决方案:
-
synchronize: 同步锁
-
CAS
-
Lock锁
AtomicInteger类,原子性Integer类,底层就使用了volatile+CAS来实现,保证了自增操作的原子性。我们直接使用AtomicInteger类来替代n++即可。
class TestCAS2 {
static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
for (int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <10000 ; j++) {
atomicInteger.incrementAndGet();
}
}
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicInteger);
}
}
总结
-
volatile可以解决可见性, 有序性, 但是不能解决原子性
-
synchronize/Lock可以解决可见性, 有序性, 原子性
Lock锁
基于synchronized锁的一些缺点,JDK1.5中推出了新一代的线程同步方式:Lock锁。更强大、更灵活、效率也更高。其核心API如图所示
实现线程同步
/**
* @Classname Test
* @Description
* @Date 2022/4/4 10:24
* @Author Lee
*/
public class Test {
public static void main(String[] args) {
new Thread(new MyTicket(),"窗口A").start();
new Thread(new MyTicket(),"窗口B").start();
}
}
class MyTicket implements Runnable{
static int num =1;
static Lock lock = new ReentrantLock(); //设置静态 线程共享
@Override
public void run() {
while (num<=100){
lock.lock(); //加锁
try {
if(num<=100) {
System.out.println(Thread.currentThread().getName() + "卖出了" + num + "张票");
num++;
}else {
System.out.println("卖完了");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
}
Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();//试获取锁,成功返回true 失败false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//拿不到锁时会等待一定的时间
void unlock();
Condition newCondition();
}
Lock避免死锁
/**
* @Classname TestB
* @Description 解决死锁
* @Date 2022/4/4 13:04
* @Author Lee
*/
public class TestB {
public static void main(String[] args) {
ReentrantLock ykq = new ReentrantLock();
ReentrantLock dc = new ReentrantLock();
new Thread(new XiaobaiRunn(ykq,dc)).start();
new Thread(new XiaomingRunn(ykq,dc)).start();
}
}
class XiaomingRunn implements Runnable{
private ReentrantLock ykq;
private ReentrantLock dc;
public XiaomingRunn(ReentrantLock ykq,ReentrantLock dc){
this.ykq = ykq;
this.dc = dc;
}
@Override
public void run() {
ykq.lock();
System.out.println("小明抢到了遥控器,正在准备抢电池");
if(ykq.isLocked()){
ykq.unlock();
}
dc.lock();
System.out.println("小明抢到了电池,打开空调爽歪歪");
dc.unlock();
if(ykq.isLocked()){
ykq.unlock();
}
}
}
class XiaobaiRunn extends Thread{
private ReentrantLock ykq;
private ReentrantLock dc;
public XiaobaiRunn(ReentrantLock ykq,ReentrantLock dc){
this.ykq = ykq;
this.dc = dc;
}
@Override
public void run() {
dc.lock();
System.out.println("小白抢到了电池,正在准备抢遥控器");
ykq.lock();
System.out.println("小白抢到了遥控器,打开空调爽歪歪");
ykq.unlock();
dc.unlock();
}
}
ReadWriteLock 实现读写操作
/**
* @Classname TestC
* @Description 读共享 读写互斥 写互斥
* @Date 2022/4/4 14:39
* @Author Lee
*/
public class TestC {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
op op = new op(readWriteLock);
new Thread(()->op.read(), "A").start();
new Thread(()->op.read(), "B").start();
new Thread(()->op.write(), "C").start();
new Thread(op::write, "D").start();
}
}
class op{
private ReadWriteLock readWriteLock;
public op(ReadWriteLock l){
readWriteLock = l;
}
public void read(){
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"开始读");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束读");
readWriteLock.readLock().unlock();
}
public void write(){
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"开始写入");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束写入");
readWriteLock.writeLock().unlock();
}
}
Lock锁和同步锁(synchronized)的选择
- 类型不同
synchronized是关键字。修饰方法, 修饰代码块
Lock是接口
- 加锁和解锁机制同步
synchronized是自动加锁和解锁,程序员不需要控制。
Lock必须由程序员控制加锁和解锁过程, 解锁时, 需要注意出现异常不会自动解锁
- 异常机制
synchronized碰到没有处理的异常,会自动解锁,不会出现死锁。
Lock碰到异常不会自动解锁,可能出现死锁。所以写Lock锁时都是把解锁放入到finally{}中。
- Lock功能更强大
Lock里面提供了tryLock()/isLocked()方法,进行判断是否上锁成功。synchronized因为是关键字,所以无法判断。
- Lock性能更优
如果多线程竞争锁特别激烈时,Lock的性能更优。如果竞争不激烈,性能相差不大。
- 锁住内容不同
synchronized可以锁方法、可以锁代码块
Lock只可以锁代码块