【JAVAEE】使用synchronized关键字和volatile关键字解决线程安全问题中的原子性,内存可见性和有序性问题

本文详细介绍了Java中的synchronized关键字,包括其互斥性、内存刷新和可重入特性,并通过示例说明其使用场景和注意事项。同时,讨论了volatile关键字如何保证内存可见性和解决有序性问题,以及MESI缓存一致性协议和内存屏障的作用。最后,总结了synchronized和volatile在解决线程安全问题中的应用。
摘要由CSDN通过智能技术生成

目录

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关键字。

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值