目录
1.synchronized关键字---监视器锁monitor lock
1.1synchronized的特性
互斥
刷新内存
可重入
1.3synchronized使用注意事项
2.volatile关键字
2.1volatile保证内存可见性问题
MESI缓存一致性协议
内存屏障
2.2volatile解决有序性问题
3.总结synchronized和volatile解决的问题
在上一篇文章中我们谈到了线程安全问题的原因主要是五个方面,想要了解详情的小伙伴可以参考如下链接。http://t.csdn.cn/uGnflhttp://t.csdn.cn/uGnfl
总结起来就是下面五句话:
1.多个线程修改了同一个共享变量
2.线程是抢占式执行的,CPU调度是随机的
3.指令执行时没有保证原子性
4.多线程环境中内存可见性问题
5.指令的有序性问题
对于第一个问题,在写程序时,大多数都是要修改同一个变量的,不能避免;对于第二个问题,CPU时硬件层面上的东西,我们也是没有办法处理的。而对于剩下三个问题,都有可能通过Java层面去处理,这里我们可以通过synchronized和volatile关键字来解决线程安全问题。
1.synchronized关键字---监视器锁monitor lock
1.1synchronized的特性
互斥
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其它线程如果也执行到同一个对象的synchronized就会阻塞等待。
进入synchronized修饰的代码块,相当于加锁
退出synchronized修饰的代码块,相当于解锁
例如下图:
某一个线程要执行这个方法时,就先获取锁,获取到之后再去执行代码,另外的线程执行这个方法时,要要获取锁,但是当线程持有锁时她就要等待,等到上一个线程释放这把锁时才可以。
大家有没有发现,加入锁之后的方法,就变成了单线程。
所以,在使用synchronized时,一定要分场景使用:
1.在获取数据时,可以用多线程来提高效率
2.修改数据时,用synchronized修饰来保证安全。
来看看两个图解,了解synchronized关键字需要注意的问题:
这里有t1,t2两个线程,t1先使用了这个方法,获取锁,释放锁之后,t2竞争到锁资源。
具体哪个线程会竞争到锁资源是不一定的,因为线程是抢占式执行的,CPU调度是随机的,并不是先阻塞等待的线程就一定要先拿到锁。
在t1执行方法时,被CPU调度走之后,依然不会释放锁,其它线程依然要阻塞等待。
所以,通过对代码加锁,解决了原子性问题。
刷新内存
synchronized的工作过程:
1.获得互斥锁
2.从主内存拷贝变量的最新副本到工作的内存
3.执行代码
4.将更改后的共享变量的值刷新到主内存
5.释放互斥锁
通过对代码加锁,保证一个线程执行完所有操作之后并释放锁,第二个线程才可以获取到锁,读到的肯定是第一个线程修改的最新结果,从而保证了可见性问题。
可重入
理解“把自己锁死”。
一个线程没有释放锁,然后又尝试再次加锁:
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
而Java中的synchronized是可重入锁,即一个线程可以对同一个锁对象加多次锁,因此没有上面的问题。
1.2synchronized的使用示例
synchronized本质上要修改指定对象的“对象头”。从使用角度看,synchronized也势必要搭配一个具体的对象来使用。
1.直接修饰普通方法:锁的synchronizedDemo对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
2.修饰静态方法:锁的synchronizedDemo对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
3.修饰代码块:明确指定锁哪个对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
4.锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
1.3synchronized使用注意事项
锁对象,只要可以记录当前获取到的线程就可以了,锁对象,可以是Java中的任何对象。
在Java虚拟机中,对象在内存中的结构可以划分为4部分区域:
- markword
- 类型指针
- 示例数据(类中的属性)
- 对其填充
markword主要描述了当前是哪个线程获取到锁资源,记录的是线程对象信息,当线程释放锁资源的时候就会把线程对象信息清除掉,其它线程就可以继续获取锁资源。
2.volatile关键字
synchronized可以解决原子性,内存可见性问题,但不能解决有序性问题。这时候就需要用到另一个关键字---volatile。
2.1volatile保证内存可见性问题
volatile修饰的变量,能够保证“内存可见性”。
代码在写入volatile修饰的变量的时候
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取volatile修饰的变量的时候
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
示例代码:
- 创建两个线程 t1 和 t2
- t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
- t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
- 预期当用户输入非 0 的值的时候, t1 线程结束.
package com.bitejiuyeke.lesson04;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
/**
* 重现内存可见性问题
* 创建两个线程 ,一个线程不行的循环判断标识决定是否退出
* 第二个线程来来修改标识位
*
* @Author 比特就业课
* @Date 2023-05-05
*/
public class Demo29_Volatile {
private static int flag = 0;
public static void main(String[] args) throws InterruptedException {
// 定义第一个线程
Thread t1 = new Thread(() -> {
System.out.println("t1线程已启动.");
// 循环判断标识位
while (flag == 0) {
// TODO :
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1线程已退出.");
});
// 启动线程
t1.start();
// 定义第二个线程,来修改flag的值
Thread t2 = new Thread(() -> {
System.out.println("t2线程已启动");
System.out.println("请输入一个整数:");
Scanner scanner = new Scanner(System.in);
// 接收用户输入并修改flag的值
flag = scanner.nextInt();
System.out.println("t2线程已退出");
});
// 确保让t1先启动
TimeUnit.SECONDS.sleep(1);
// 启动t2线程
t2.start();
}
}
运行结果:
原因:
1.线程1在执行过程中并没有对flag进行修改
2.在执行时,线程1先从主内存把flag加载到自己的工作内存中,也就是寄存器和缓存中
3.CPU对执行的过程有一定的优化:既然当前线程没有修改变量的值,而工作内存的读取速度是主内存的一万倍以上,那么每判断这个flag时从工作内存读取即可
4.目前线程2修改flag的值之后,并没有一种机制来通知线程1获取最新的值
为变量加入volatile之后:
// 注意观察用volatile修饰后的现象
private static volatile int flag = 0;
程序可以正常退出:
MESI缓存一致性协议
缓存一致性协议:当某个线程修改了一个共享变量之后,通知其它CPU对该变量的缓存中置为失效状态。当其它CPU中执行的指令再需要获取缓存中变量的值时,发现这个值被置为失效状态,那么就需要从主内存中重新加载最新的值。
内存屏障
对加了volatile的变量,加入了以下的内存屏障,Load表示读,Store表示写。
当发送写操作之后就会通过缓存一致性协议来通知其它的CPU中的缓存值失效。
所以,volatile可以解决内存可见性问题。
2.2volatile解决有序性问题
有序性指的是再保证程序执行结果正确的前提下,编译器,CPU对指令的优化过程。
volatile修饰的变量,就是要告诉编译器,不需要对这个变量所涉及操作进行优化从而实现有序性。
所以,volatile可以解决有序性问题。
3.总结synchronized和volatile解决的问题
再代码中,对于共享变量最好加上volatile关键字。