一.JMM java内存模型
这个概念对于线程安全问题产生原因以及解决都有着很强烈的关系:
1).模型图
JMM 规定:所有的线程对于变量的修改都不能直接在主内存中去修改,而是要将变量值从主内存中复制到线程自己的工作内存中去然后修改,修改完成之后再刷回主内存
很明显,这里java的工作内存之间是不可见的,是内存级别的线程隔离
虽然JMM设计如此,导致了其内存不可见,以及一些原子性问题,以及不能保证原子性,但是他是有相应的措施去保证其三个重要特性
1.原子性 2.内存可见性 3.有序性
二.线程安全问题
1)什么是线程安全问题?
线程安全问题,在多线程的环境下,代码逻辑上没有问题,但是结果不及预期,这就叫做线程安全问题
2)造成线程安全问题的原因
先说结论
1.cpu是随机调度的(也就是线程调度是抢占式)
2.多个线程修改了同一个同一个变量(修改了共享变量)
3.JMM模型不能保证原子性 (结合count++操作思考一下原子性会带来的问题,会有覆盖现象)
4.JMM模型不能保证内存可见性(一个线程在运行感应不到另一个线程对同一个变量的修改)
4.JMM模型不能保证有序性(指令重排序会带来一些,对象半初始化,变量可见性问题,初始化顺序错误)
1.cpu是随机调度的(也就是线程调度是抢占式),这是cpu硬件规定的无法干预
2.多个线程修改了同一个同一个变量(修改了共享变量),这是在多线程环境对同一个变量修改是一个常见需求,也无法干预
3.JMM模型不能保证原子性 4.JMM模型不能保证内存可见性 4.JMM模型不能保证内存可见性
3-5这三点JMM模型有措施实现他三个重要特性通关synchronized,volatile关键字等实现其三个特性
三.synchronized关键字
1)原子性带来的问题
首先看一个场景:
1.创建两个线程,两个线程对同一个变量(初始数值为0),分别自增5w次,我们预期结果为10w
package DEMO;
//场景是创建两个线程对同一个数分别进行5w次的自增,预期结果是让这个数最后的结果返回一个10w
public class demo_0 {
public static void main(String[] args) throws InterruptedException {
Counter0 counter=new Counter0();
Thread thread1 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
Thread thread2 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= "+counter.count);
}
}
//创建一个类用来累加
class Counter0{
public int count=0;
public void increase(){
count++;
}
}
通过结果可以发现结果并不是10w,但是仔细看代码逻辑上并没有问题,结果不及预期,这就是线程安全问题。
分析原因:
首先我们要知道指令的概念,count++,这一句代码有三条指令,中间是工作内存与主内存的关系图,最右边是线程在cpu上调度的时间轴
以上这种就造成了覆盖现象,导致出现了线程安全问题,这里是因为count++,是一个复合操作,这个操作不能保证其原子性,造成了线程安全问题,以上cpu调度的顺序仅仅是随机分析的一种,可以任意定义顺序分析,记住cpu的调度是随机的
2)synchronized来实现操作原子性
1)synchronizd的用法
1.synchronized的可以修饰方法也可以修饰变量,修饰方法的时候锁对象是实例对象本身或者类对象,修饰方法就是看具体()中的对象
2.产生锁竞争的条件:关键是看是不是竞争的是同一把锁,如果是一把锁则产生锁竞争,不是则不回产生锁竞争
3.任何对象都可以是锁对象,可以是实例对象也可以是类对象
2)用synchronized代码解决原子性问题
package DEMO;
//场景是创建两个线程对同一个数分别进行5w次的自增,预期结果是让这个数最后的结果返回一个10w
////用synchronized 锁住整个方法
public class demo_1 {
public static void main(String[] args) throws InterruptedException {
Counter1 counter=new Counter1();
Thread thread1 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
Thread thread2 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= "+counter.count);
}
}
//创建一个类用来累加
class Counter1{
public int count=0;
//用synchronized 锁住整个方法
public synchronized void increase(){
count++;
}
}
package DEMO;
//场景是创建两个线程对同一个数分别进行5w次的自增,预期结果是让这个数最后的结果返回一个10w
////用synchronized 锁住方法
public class demo_2 {
public static void main(String[] args) throws InterruptedException {
Counter2 counter=new Counter2();
Thread thread1 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
Thread thread2 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= "+counter.count);
}
}
//创建一个类用来累加
class Counter2{
public int count=0;
public synchronized void increase(){
//被锁住的代码块前面可能还有很多可以并行执行的代码,他们不会造成线程安全的问题
//用synchronized 锁住代码块
synchronized (this){
count++;
}
//之后的代码也同样可以并行执行,也不会造成线程安全的问题
}
}
修饰方法与修饰代码块的区别在代码备注中写的很清楚了,根据需求来锁定自己想要锁住的代码,以上两种方法均可以得到正确的结果
3)synchronized解决的原理
加了synchronized的代码块在执行相应的代码的时候,会有获取锁LOCK,和释放锁UNLOCK机制,这个锁就是锁对象。
1.锁对象
每个对象在内存中有四个部分,分别是:对象头(markword),类型指针,实例数据,对齐填充
而获取锁的线程信息就是放在对象头中并且有一个计数器。
对于锁对象,任何对象都可以是锁对象,可以是实例对象也可以是类对象
实例对象可以是new出来的每个对象,也可以是类中的成员变量new出来的对象,也可以是用static修饰的成员变量对象,属于类对象全局唯一
类对象,就是类名.class来获取类对象引用全局唯一
2.synchronized修饰的代码运行时候的情况
以上为其被synchronized修饰后的count++语句,注意:当t1执行完任务释放锁之后,并不意味着,下一个是t2获取到锁,而是想要获取锁信息的所有线程都参与锁竞争谁获取到锁,谁就可以,执行自己的逻辑,当然也包括t1自己
还有一点需要注意就是,加锁并不是一定要将加锁的指令执行完才会被调走,而是执行完逻辑才会释放锁,不要把cpu调度和锁定概念搞混
由于加锁的代码块需要获取先获取到锁,而锁只能有一个线程获取,这就会让加锁的代码串行执行,这是其保证原子性的关键
4)如何正确使用synchronized(判断是否构成锁竞争)
其实就只有一句话,判断是不是锁对象使用的是不是同一个即可,使用同一个对象那么就会有锁竞争,如果不是一个对象那就没有锁竞争
以下提供一些代码示例来判断是否构成锁竞争
package DEMO;
//两个线程执行的自增方法一个加了锁,一个没有加锁
public class demo_3 {
public static void main(String[] args) throws InterruptedException {
Counter3 counter=new Counter3();
Thread thread1=new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
Thread thread2 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase1();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
System.out.println("count= "+counter.count);
}
}
class Counter3{
public int count=0;
//一个方法加锁了,一个方法没有加锁
public void increase(){
count++;
}
public synchronized void increase1(){
count++;
}
}
不构成
package DEMO;
//使用不同的锁对象,对同一个静态变量进行自增
public class demo_4 {
public static void main(String[] args) throws InterruptedException {
Counter4 counter=new Counter4();
Counter4 counter2=new Counter4();
Thread thread1=new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
Thread thread2 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter2.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
System.out.println("count= "+counter2.count);
}
}
class Counter4{
public static int count=0;
//一个方法加锁了,一个方法没有加锁
public synchronized void increase(){
count++;
}
}
不构成
package DEMO;
//在类中单独定义一-个对象lo_ker作为锁对象
public class demo_5 {
public static void main(String[] args) throws InterruptedException {
Counter5 counter = new Counter5();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= " + counter.count);
}
}
//创建一个类用来累加
class Counter5{
public int count=0;
Object lo_ker=new Object();
//用synchronized 锁住整个方法
public void increase(){
synchronized(lo_ker){
count++;
}
}
}
构成锁竞争
package DEMO;
//在类中单独定义一-个对象lo_ker作为锁对象
public class demo_6 {
public static void main(String[] args) throws InterruptedException {
Counter6 counter = new Counter6();
Counter6 counter1=new Counter6();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter1.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= " + counter.count);
}
}
//创建一个类用来累加
class Counter6{
public static int count=0;
Object lo_ker=new Object();
//用synchronized 锁住整个方法
public void increase(){
synchronized(lo_ker){
count++;
}
}
}
不构成
package DEMO;
//两个线程执行的自增方法一个加了锁,一个没有加锁
//使用同一个对象,对象中有两个自增方法,两个自增方法中定义一个锁对象
public class demo_7 {
public static void main(String[] args) throws InterruptedException {
Counter7 counter=new Counter7();
Thread thread1=new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
Thread thread2 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase1();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
System.out.println("count= "+counter.count);
//结果正确说明一个问题,当多个线程执行不同的代码块的时候,只要两个代码块使用的是同一个锁对象,仍然会产生锁竞争
}
}
class Counter7{
public int count=0;
//一个方法加锁了,一个方法没有加锁
Object lo_ker=new Object();
public void increase(){
synchronized(lo_ker){
count++;
}
}
public void increase1(){
synchronized(lo_ker){
count++;
}
}
}
构成锁竞争
package DEMO;
//在类中单独定义一-个对象lo_ker作为锁对象
public class demo_8 {
public static void main(String[] args) throws InterruptedException {
Counter8 counter = new Counter8();
Counter8 counter1=new Counter8();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter1.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= " + counter.count);
}
}
//创建一个类用来累加
class Counter8{
public static int count=0;
static Object lo_ker=new Object();
//用synchronized 锁住整个方法
public void increase(){
synchronized(lo_ker){
count++;
}
}
}
构成锁竞争
package DEMO;
//在类中单独定义一-个对象lo_ker作为锁对象
public class demo_9 {
public static void main(String[] args) throws InterruptedException {
Counter9 counter = new Counter9();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= " + counter.count);
}
}
//创建一个类用来累加
class Counter9{
public int count=0;
Object lo_ker=new Object();
//用synchronized 锁住整个方法
public void increase(){
synchronized(demo_9.class){
count++;
}
}
}
构成锁竞争
package DEMO;
//在类中单独定义一-个对象lo_ker作为锁对象
public class demo_10 {
public static void main(String[] args) throws InterruptedException {
Counter10 counter = new Counter10();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= " + counter.count);
}
}
//创建一个类用来累加
class Counter10{
public int count=0;
Object lo_ker=new Object();
//用synchronized 锁住整个方法
public void increase(){
synchronized(String.class){
count++;
}
}
}
构成锁竞争
package DEMO;
//在类中单独定义一-个对象lo_ker作为锁对象
public class demo_10 {
public static void main(String[] args) throws InterruptedException {
Counter10 counter = new Counter10();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程执行完成
thread1.join();
thread2.join();
//打印结果
System.out.println("count= " + counter.count);
}
}
//创建一个类用来累加
class Counter10{
public int count=0;
Object lo_ker=new Object();
//用synchronized 锁住整个方法
public void increase(){
synchronized(String.class){
count++;
}
}
}
构成锁竞争
由以上的例子已经说明了,构成锁竞争的关键了,就是去判断多个线程竞争的是不是同一把锁,如果是同一把锁,构成锁竞争
进入被锁住的代码就需要先获取锁,被锁住的代码没有说一定要是调用相同的代码,只要是需要的是同一把锁,就会构成锁竞争
3)synchronized实现的内存可见性,以及不保证有序性
synchronized实现的内存可见性并不是真正实现,内存可见性,而是通过原子性,让被锁住的代码串行执行,使得当前线程永远会拿到上一个线程所修改后刷回主内存的值,间接实现了内存可见性,当然synchronized不保证有序性
对于内存可见性,还有一点就是:synchronized实现的内存可见性有两点
加锁时清空缓存:当其他线程获取同一个锁时,会先清空本地内存,从主内存中读取共享变量的最新值。
解锁时刷新缓存:当线程执行完同步块并释放锁时,会把本地内存中修改过的共享变量的值刷新到主内存。
不过如果想要单纯实现内存可见性,使用synchronized的关键字,会占用更多资源,我们一般使用volatile关键字来实现对变量的可见性
4)synchronized的特性 互斥性,与可重入
1)互斥性
互斥性我们刚刚已经说的很清楚了,当一个锁对象被一个线程获取了,当其他线程会被阻塞等待,这就是互斥性
2)可重入
用一个代码来看看可重入
package demo1;
//演示synchronized关键字的可重入的特性
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
//使用当前线程类作为锁对象,不对对同一把锁重复加锁,来验证可重入的性质(对于一个线程,已经获取了锁,锁对象中记录了当前的
// 线程信息,在锁住的代码中想再次获取这把锁,仍然可以获取到,对于这种现象,就称为可重入)
synchronized (Thread.currentThread()) {
System.out.println("锁对象为" + Thread.currentThread());
synchronized (Thread.currentThread()) {
System.out.println("锁对象为" + Thread.currentThread());
synchronized (Thread.currentThread()) {
System.out.println("锁对象为" + Thread.currentThread());
synchronized (Thread.currentThread()) {
System.out.println("锁对象为" + Thread.currentThread());
}
}
}
}
//重复对一把锁加了四次,启动线程看一下是否可以重复获取锁
});
//启动线程
t1.start();
//打印结果即可说明synchronized的可重入特性
}
}
我们要知道对于可重入,是对于在一个线程中对于同一把锁可以不断加锁,当在被锁住的代码块中,继续尝试获取那一把锁的时候,他会查看锁对象的锁信息,他会发现锁信息是自己,此时就可以获取到锁,并且对于锁信息中的计数器+1,出了被锁住的代码块就会将锁信息的计数器-1,当计数器为0,时候就会释放锁资源synchronized(){},在进入{括号之前计数器+1,从}出来的时候减1
对于在一个线程中对于同一把锁可以不断加锁,这种现象称为可重入
这里对于在被锁住的代码中加锁提一个问题,后续会有详细介绍
注意看这个代码当假设,t1获取到了lock1的时候,t2获取到了lock2,而在两个线程被锁住的代码块中,t1还需要lock2,t2还需要lock1,而此时两个线程一起阻塞等待,lock1,lock2,两把锁永远不会被释放,这就会造成死锁,后续会详细介绍
5)总结关键
1.首先synchronized可以修饰代码块,也可以修饰方法,可以让被锁住的代码块串行执行
2.锁对象,任何对象都可以是锁对象,可以是实例对象也可以是类对象
3.构成锁竞争的关键是看竞争的是不是同一把锁
4.synchronized,实现了原子性,内存可见性,但不禁止指令重排序,也就是不保证有序性
5.锁定与cpu调度概念不要搞混了
6.synchronized有同步机制,也会实现内存可见性,但是我们一般不使用synchronized让 变量可见,我们一般使用volatile关键字
7.synchronized的特性 1.互斥性 只有一个线程对象可以获取到锁
2.可重入 对于一个线程同一把锁,可以不断重复加锁(锁信息中有
线程信息和计数器)
四.volatile关键字
先给一个场景演示,内存可见性的会造成的问题,注意看备注
package demo2;
//场景:演示内存不可见
import java.util.Scanner;
/**
* 第一个线程:不断的循环执行任务,判断条件为flag==0
* 第二个线程:在第一个线程不断运行的时候,把flag修改为别的值
*
* t2,将flag修改为了1后刷回了主内存,并且退出了,t1,已经将flag的值读到了缓存中
* 由于编译器会自动优化,他认为这个flag值不会变,他不会再从主内存中读取flag的值,而是一直在缓存中读取flag的值,
* 导致他一直认为flag的值一直是1
*/
public class DEMO_00 {
static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1线程启动");
while (flag == 0) {
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程启动");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零整数");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
//启动线程
t1.start();
t2.start();
}
}
场景:第一个线程:不断的循环执行任务,判断条件为flag==0
第二个线程:在第一个线程不断运行的时候,把flag修改为别的值
为运行结果可以看出来t2将flag值修改后,刷回主内存,且正常退出线程,但是t1一直没有退出,此时就出现了因为内存不可见导致的线程安全问题
解释一下以上发生的原因:
t2将flag的值修改后刷回了主内存,并且退出后,t1的循环while(flag),在该线程中并未对其修改,编译器会将其给优化,对于flag的读取会直接从缓存中读取,而不是主内存中去读取,导致了t1读到的flag的值永远都是0,所以t1线程不会正常退出
1)尝试解决内存不可见带来的问题
在使用volatile解决问题之前我们先尝试一下,用一些其他方式来解决问题,并且解释原因
1.使用我们上面说的synchronized来解决
package demo2;
import java.util.Scanner;
//尝试解决
/**
* 加上了synchronized,他会触发synchronized的同步机制,然后会让缓存失效,从主内存中读取,然后就可以知道flag被修改了
* 进入synchronized同步块时,会强制清空线程的工作内存,从主内存重新加载变量,所以可以感知到flag的变化
*/
public class DEMO_02 {
static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1线程启动");
while (flag == 0) {
synchronized (DEMO_02.class){
}
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程启动");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零整数");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
//启动线程
t1.start();
t2.start();
}
}
2.再看看synchronized的影响
package demo2;
import java.util.Scanner;
//尝试解决
/**
* 为什么这样子他可以解决
* 因为println是一个同步方法(内部有synchronized,他会触发内存屏障,让t1,缓存中的值失效,只能从主内存中读取新的flag值,此时就发现了flag
* 已经被更新了),虽然打印的是count,但是仍会让缓存失效,flag的也会失效,从主内存中读取
*/
public class DEMO_01 {
static int flag = 0;
static int count=1;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1线程启动");
while (flag == 0) {
System.out.println(count);
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程启动");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零整数");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
//启动线程
t1.start();
t2.start();
}
}
为什么这样子他可以解决?
因为println是一个同步方法(内部有synchronized,他会触发同步机制,让t1,缓存中的值失效只能从主内存中读取新的flag值,此时就发现了flag已经被更新了),虽然打印的是count,但是仍会让缓存失效,flag的也就失效了,只能从主内存中读取,也就读取到了最新值
3.来看看因为其他原因间接性达成了内存可见性
package demo2;
import java.util.Scanner;
/**
* 这里因为sleep的原因,t1被阻塞了,当他被重新调回cpu时候,他会从主内存中读取flag值,让他感受到了flag变化
* 但是依赖sleep实现内存可见性是不靠谱的且危险的,这只是特定JVM和代码实现下的巧合
*/
public class DEMO_03 {
static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1线程启动");
while (flag == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程启动");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零整数");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
//启动线程
t1.start();
t2.start();
}
}
这里是因为sleep的原因,让t1线程长时间处于阻塞的状态,当t1被重新调回cpu的时候,他会从主内存中读取flag的值,所以t1对于flag的修改可以感知到,但是注意这里是sleep在特定场合(JVM和代码实现下的巧合)下所造成的情况,不要依赖sleep来去实现可见性,这是不可靠且不安全的
2)尝试使用volatile来保证内存可见性
在使用volatile来解决问题之前我们先使用volatile说volatile的用法
1.volatile的用法及其特性
1.用法:要知道volatile只能用于修饰成员变量,不能用于修饰局部变量,他是直接用来修饰 变量的
2.直接可见性:被修饰的变量会存因为内存屏障的原因,让他可以被其他线程可见,也就拥 有了直接可见性
1)读屏障:对于被volatile修饰的变量,每个线程每次对其的读取,都会让缓存失效,从 主内存中直接读取 ,可以让线程读到该变量的主内存中的最新值
2)写屏障:对于被volatile修饰的变量,线程每次对其修改完后,会立即刷回主内存,并 且让 使用该变量的其他线程中的该变量的缓存会立即失效,
但是需要注意volatile的可见性,并不是“立即生效”,是保证下一次的读取能够读到最新 值,是针对“后续读取的可见性”,无法影响已经完成读取的操作(场景两个线程使用同一 个共享volatile变量a初始值为0,两个线程都执行各自操作,一个线程先load了a的值放入 缓存 就被调走,另一 个线程上cpu,全部执行后把a的新值刷回了主内存,并让a在所有 线程的缓存立即失效,但是第一个线程已经读取了该线程的值,他的后续操作不会被影 响,只有到下一次读取他才会去从主内存中读最新值,这就是保证了“后续读取的可见 性”)。
3.有序性:对于被volatile修饰的变量的读写前后会有四个内存屏障:
STORESTORE,LOADLOAD,LOADSTORE,STORELOAD,这四个内存屏障保证 了,不仅保证了被volatile修饰的变量的读写的有序性,还保证了普通写,与 volatile的读写前后的顺序,保证了有序性
volatile读: STORELOAD - >volatile读 - >LOADLOAD/LOADSTORE
volatile写: LOADSTORE - >volatile写 - >STORELOAD
保证了有序性可以解决指令重排序所带来的问题如(1.半初始化对象 2.变量可见性问题
3.初始化顺序错乱)
4.间接可见性:由于四个内存屏障原因,有时候会导致对于普通变量的间接可见性
1.线程A对于普通变量修改后,修改volatile变量前会有一个STORESTORE屏障, 会在 volatile写之前将普通变量写回主内存
2.在volatile读之后,会有LOADLOAD屏障,保证后续普通读操作能够读到最新的 值
当然除了这两种对于volatile变量的读写他还会或多或少的影响相邻变量的缓存行为, 尽管这并非volatile的标准语义
但是我们不要依赖于间接可见性来实现对于某共享变量的可见性
要是想要保证某个变量的可见性,以及保证有序性,避免指令重排序所带来的问题,我们可以直接对该变量用volatile修饰即可,volatile的可见性相比synchronized所带来的资源消耗更少,所以推介对于多线程的共享变量涉及到修改,我们都可以使用volatile来修饰,但是注意
volatile只保证了可见性,有序性,但是并不保证原子性
以上为关于volatile的重要知识接下来来看一下,使用volatile来解决问题
package demo2;
import java.util.Scanner;
//使用volatile解决问题
/**
* 这里volatile修饰了flag,此时会有
* volatile的直接可见性
* 读屏障:对于flag的读取,会让缓存失效,每次都是从主内存中读取,每次都能读到最新的主内存中flag的值
* 写屏障:对于flag的修改,修改后会立即将他写回主内存,所有线程中对于该变量的本地缓存会立即失效
* volatile的可见性并不是“即时立马有效”,他保证的是下一次读取会读到最新的值,是针对
* “后续的读取操作”,无法影响在修改前已经发生的读取
*/
public class DEMO_04 {
static volatile int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1线程启动");
while (flag == 0) {
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程启动");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零整数");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
//启动线程
t1.start();
t2.start();
}
}
package demo2;
//使用volatile来解决
import java.util.Scanner;
/**
* volatile 挥发性的单词翻译
* 第一点:volatile只能用于成员变量,不能用于局部变量
* volatile特性:
* 1.直接可见性
* 读屏障:对于flag的读取,会让缓存失效,每次都是从主内存中读取,每次都能读到最新的主内存中flag的值
* 写屏障:对于flag的修改,修改后会立即将他写回主内存,所有线程中对于该变量的本地缓存会立即失效
* 2.有序性:通过插入内存屏障(storestore,storeload,loadload,loadstore)四种屏障来保证有序性
* 对于 storestore->volatile写 ->storeload
* 对于 storeload ->volatile读 ->loadload/loadstore
* 由于内存屏障的缘故,
* 写传播:volatile 写会将之前的所有普通写操作结果刷新到主内存。
* 读刷新:volatile 读会从主内存中读取最新值,并确保后续普通读操作能看到最新状态。
* 这种对于普通变量的间接性可见性存在,但我们并不要去使用依赖,要想确保某个变量的可见性,并且禁止指令重排序的话,直接对那个变量使用volatile
* 指令重排序的影响(1.变量半初始化 2.变量可见性问题 3.初始化顺序错乱)
*
*/
//volatile写操作会在指令序列中插入内存屏障,这些屏障可能会影响相邻变量(如 flag)的缓存行为,尽管这并非 volatile 的标准语义。,
// 但是并不要依赖于这种间接性的可见性
public class DEMO_05 {
static int flag = 0;
volatile int a=1;
public static void main(String[] args) {
// volatile int a=1; volatile只能用于成员变量,不能用于局部变量
DEMO_05 demo_05=new DEMO_05();
Thread t1 = new Thread(() -> {
//实例化一个DEMO_04为了去调用a
System.out.println("t1线程启动");
while (flag == 0) {
demo_05.a=2;
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程启动");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零整数");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
//启动线程
t1.start();
t2.start();
}
}
以上就是volatile的间接可见性,尽管存在,但是我们不要依赖于间接可见性来保证内存可见性,以上代码只是演示
3)volatile不能保证原子性
我们使用经典案例来说明原子性
package demo2;
/**
* 为什么volatile没有保证原子性?
* 以下的代码出现了线程安全问题的原因就是因为没有保证count++的操作是原子性的
* 因为volatile只能保证对于下次读取的线程能够读到最新的值,并不能对已经完成读取操作的
* 线程产生影响,也就是说当一个线程load,count的值后被调走,另一个线程进入,load后执行完所有的count++操作,刷回了主内存,
* 而t1线程已经load了count的旧值,他仍会执行后续指令将旧值++,再刷回主内存,进行了覆盖操作
* 所以这里加了volatile和没有加并没有产生很大的影响,这里主要是count++操作不是原子性产生出来的线程安全问题
* volatile无法保证复合操作的原子性如count++,就算对于非复合操作也无法保证原子性,只能保证被修饰的变量的可见性,以及其有序性
*/
public class DEMO_06 {
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
Thread t1 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
Thread t2 =new Thread(()->{
for (int i = 0; i <5_0000 ; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t1.join();
System.out.println(counter.count);
}
}
class Counter{
public volatile int count=0;
public void increase(){
count++;
}
}
为什么volatile没有保证原子性?
以下的代码出现了线程安全问题的原因就是因为没有保证count++的操作是原子性的
线程产生影响,也就是说当一个线程load,count的值后被调走,另一个线程进入,load后执行完所有的count++操作,刷回了主内存,
而t1线程已经load了count的旧值,他仍会执行后续指令将旧值++,再刷回主内存,进行了覆盖操作
所以这里加了volatile和没有加并没有产生很大的影响,因为主要是count++操作不是原子性产生出来的线程安全问题,而volatile并不能保证原子性
volatile无法保证复合操作的原子性如count++,就算对于非复合操作也无法保证原子性,只能保证被修饰的变量的可见性,以及其有序性
总结已经,尝试使用volatile来保证内存可见性中的volatile用法及其特性中已经全部总结概述了一遍
五.wait()和notify()/notifyAll()
1)wait,notify的使用
这里描述一个场景:妈妈叫小明去买包子,小明去包子店买包子
妈妈是t1线程,小明是t2线程,包子店是t3线程,妈妈要等小明买完包子回来是(在t1的任务中有t2.join()),小明t2线程的任务就是买包子回去给妈妈,包子店t3的线程任务就是做包子
小明去包子店,发现包子店包子卖完了,他就跟包子店员工说做好了叫我一下,然后就去一旁wait()了,过了一会包子店又出炉了包子,那个员工叫了一下“包子做好咯”notifyAll(),这时候所有在等wait()的人就过来开始排队买包子了,小明买到了包子拿回去给了妈妈,这时候小明的任务结束,妈妈的(t2.join)完成了终于可以继续他的任务了
以上的代码就可以反应出来了wait,notify的用法了,wait是为了等待所需要的资源去等待,而notify则是对于等待的线程唤醒
由于notify和wait是与锁机制深度绑定的所以在使用的时候需要配合synchronized的使用,不搭配使用会报错,且用同一个锁对象去wait,去notify,注意是同一个锁对象,就像小明去等那个员工叫他,那个员工去叫小明,还有一点需要注意锁对象可以直接notify(),不管有没有人wait(),就像包子店出炉包子,他可以哟呵一声,但是如果只有wait没有notify那就会一直死等
现在就来看看代码
package demo2;
import java.util.concurrent.TimeUnit;
/**
* 演示wait()和notify()
* 注意:wait和notify必须搭配synchronized一起使用,因为他们是针对锁对象进行等待的唤醒的,
* 就像一个人卖包子,别人买包子的时候,他没做好,叫别人等他做好,他做好了叫别人,这个卖包子的人就是锁对象
* 注意,notify(),随机唤醒一个
* notifyAll(),唤醒所有的
* notify()可以空喊就像卖包子的老板做好了哟喝一声
* wait()不可以空等没人喊,一直阻塞怎么行
* jion()是等另一个线程执行结束
* wait()是等待资源准备好 注意两者区别,一个是Thread类中方法,一个是Object类中的方法
*/
public class DEMO_07 {
public static void main(String[] args) {
Object loker=new Object();
Thread t1 =new Thread(()->{
while (true){
System.out.println("调用wait方法之前");
//执行自己的代码逻辑
//等待资源准备好
synchronized (loker){
try {
loker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("调用wait方法之后");
System.out.println("===========");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 =new Thread(()->{
while (true){
System.out.println("调用notify方法之前");
synchronized (loker){
loker.notify();
}
System.out.println("调用notify方法之后");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
注意wait与notify需要用锁对象来调用,由打印结果发现在wait之前与之后,中间一定会调用notify,有人发现第二个的wait之前和之后中间只有一个notify之前,那notify之后呢?注意此时一定是已经调用了notify()方法了,只是t2执行完了notify()方法之后被立马调走了,t1被此时被调回了cpu,然后执行打印了wait方法之后,一定要记住线程是随机调度的
这里需要明确注意一个点:当线程t1进入synchronized的代码获取到锁之后,进入被锁住的代码块,此时锁对象中的线程信息是t1,其他线程获取不了该锁,当线程t1调用了wait方法后,此时他会将锁资源给释放掉,然后进入阻塞状态,此时其他线程就可以竞争该锁资源,当t2抢到了该锁资源,进入synchronized代码块后,然后调用notify语句之后会唤醒正在等待的线程,然后继续执行synchronized中的代码,直到执行完后释放锁资源,然后所有需要该锁资源的线程开始锁竞争,当t1,又抢到了该锁资源,会继续执行wait之后的代码逻辑,然后执行完被锁住的代码块逻辑之后,释放锁资源,这个过程与之前所说的synchronized的互斥性不违背,锁对象中只能有一个线程信息,不要认为此时锁对象中有两个线程信息,注意理解wait,notify的执行过程,不要混淆了概念
这里总结一下
1.wait与notify是Object类中的方法
2.wait与notify必须要搭配synchronized一起使用,并且使用同一把锁来调用,针对哪个锁对 象wait,就针对哪个锁对象notify
3.线程需要等待资源准备好需要调用wait来等待资源,让线程进入阻塞状态,并且释放锁资源
4.wait之后会释放锁资源,被唤醒后,抢到锁资源之后继续执行wait之后的逻辑
5.notify()可以空喊,不管有没有线程wait,但是只有wait()没有notify(),就会死等
6.notify()是随机唤醒一个正在wait的线程,notifyAll()是唤醒所有正在wait的线程,注 意,只是唤醒,他们仍然需要去等notify的那个线程释放锁资源后,参与锁竞争,当然不要 忘记是针对的是同一把锁wait与notify
2)join与wait的区别
二者的区别很简单
共同点:两个方法都可以让线程进入阻塞状态,放弃执行
不同点:1.join是Thread类中的方法,wait是Object类中的方法
2.join是让一个线程A调用B.join()是一个线程等待另一个线程执行完毕
wait只是通过锁对象引用.wait()等待资源准备好,由生产资源线程唤醒结束 等 待,必须
3.wait必须搭配synchronized一起使用,join不需要
3)Sleep与wait的区别
二者其实本身就没什么可比性质
共同点:都可以让线程放弃执行一段时间,进入阻塞状态
不同点:1.sleep是Thread类中的静态方法,wait是Object类中的方法
2.sleep是直接通过类名.sleep调用,而wait需要搭配synchronized一起使用,由锁 对象引用.wait来调用
3.sleep(中间必须是有时间限制),而wait(可以有时间限制,也可以没有时间限 制),但是注意一点,就算是有时间限制的wait,他也可以被notify唤醒(时间只 是规定了超了这个时间线程会被自动唤醒),等到调用notify的线程拿到锁资 源,进 入synchronized代码块,通过锁对象调用notify(),唤醒正在wait的线程(注 意同一个锁对象),然后执行完代码块中后续逻辑之后,释放锁资源,所有需要 锁资源的线程开始竞争锁资源,如果被唤醒的线程抢到锁资源后会继续执行wait 之后逻辑
以上便是本文章中的所有内容,谢谢你的阅读~!!