首页 > 其他分享 >多线程的原子性,可见性,有序性

多线程的原子性,可见性,有序性

时间:2022-12-08 14:56:19浏览次数:45  
标签:static Thread 原子 flag 线程 有序性 volatile 多线程 public

java内存模型定义了主存,工作内存等这些抽象概念,底层对应着cpu寄存器,缓存,cpu指令优化等。

由此引出了 原子性,可见性,有序性

一、原子性

保证指令不会受到上下文切换的影响而产生指令交错,锁就是用来解决这个问题的

二、可见性

为了保证指令不会受cpu缓存的影响

2.1 现象描述和解释

先看一个例子

private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class);

    static boolean flag=true;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag){
                }
            }
        });

        t1.start();
        Thread.sleep(1000);
        LOGGER.info("改变标记");
        flag=false;

    }

上边的代码t1线程当flag=true时会一直循环,主线程1s后改变flag,按预想的t1应该会结束,实际上t1线程不会结束,

这就是可见性问题。

java内存模型中有主内存,每个线程都有自己的工作内存,当一个变量被频繁读取时,jit编译器会将flag的值缓存到工作内存中的高速缓存中,后边从缓存中读取。这样当主线程修改了flag并更新到主内存后t1线程还是从高速缓存获取flag,所以一直不能停止

总结下来就是一个线程对主内存的数据进行了修改,但对另外一个线程不可见

2.2 解决办法

(1) 给共享的变量加一个关键字volatile,表示容易变化的,这样对这个线程的读取就不会走缓存,一直从工作内存获取。

static volatile boolean flag=true;

(2) synchronized也可以解决可见性问题

获取共享变量值的时候先加锁,这样也能保证可见性

public class Test8 {

    private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class);

    final static Object lock = new Object();

    static boolean flag=true;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (lock){
                        if(!flag){
                            break;
                        }
                    }
                }
            }
        });

        t1.start();

        Thread.sleep(1000);
        LOGGER.info("改变标记");
        synchronized (lock){
            flag=false;
        }
    }
}

这种解决方式下需要注意对 flag变量的所有操作都要放在synchronized块中

2.3 简单应用

两阶段终止模式可以使用线程的interrupt方法和打断标记来实现,这种方式需要特殊处理InterruptedException

异常,在异常处理中重新设置打断标记,否则就不能正常停止。

也可以使用volatile关键字来实现,这种方式就不需要特殊处理InterruptedException异常了

public class Test2 {

    public static void main(String[] args) throws InterruptedException {
        Monitor monitor = new Monitor();
        monitor.start();
        // 2秒后主线程中执行停止
        Thread.sleep(3000);
        monitor.stop();
    }

}
//建设器类,有一个线程一直在监控
class Monitor{
    static Logger logger = LoggerFactory.getLogger(Monitor.class);
    Thread t;
    //控制是否停止的标记
    private volatile boolean isStop=false;
    //开始监控的方法
    public void start(){
        t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    Thread current =Thread.currentThread();
					// 因为isStop被volatile修饰了,所以其他线程的修改可以感知到,
                      // 这样就可以用来控制线程是否结束
                    if(isStop){
                        //被打断了
                        logger.debug("要结束了,执行结束前的操作");
                        break;
                    }
                    try {
                        //每隔一秒执行一次监控逻辑
                        Thread.sleep(1000);
                        logger.debug("执行监控逻辑");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //这种方式不需要在异常中进行特殊处理
                    }
                }
            }
        },"t1");
        t.start();
    }

    //停止监视器的方法
    public void stop(){
        isStop=true;
    }
}

2.4 volatile解决可见性问题的原理

volatile的原理是基于内存屏障,

在读取被volatile修饰的变量时会在读取指令之前加入读屏障,

在写入被volatile修饰的变量时会在写指令之后加入写屏障,

读屏障会保证屏障之后对volatile变量的读取都从主内存中读,写屏障会保证屏障之前对volatile变量的修改都会刷新到主内存中。

三、有序性

保证指令不会受cpu指令并行优化(指令重排)的影响

2.1 问题描述

jit编译器会在不影响最终结果的前提下调整指令的执行顺序,在多线程环境下可能就会出现一些问题。

例如创建对象时,在java层面看到的是一句代码

User user = new User()

在字节码指令层面会对应着几个步骤

(1)创建实例,(2)执行构造方法(3)暴露引用

而 2和3的顺序是有可能被调整的,这样在多线程环境下如果这个user是个共享变量,当前线程有可能先执行了3那么其他线程就有可能拿到一个不完整的对象。

2.2 解决办法

变量用volatile关键字修饰

2.3 volatile解析有序性问题原理

在读取被volatile修饰的变量时会在读取指令之前加入读屏障,

在写入被volatile修饰的变量时会在写指令之后加入写屏障,写屏障会保证之前的指令不进行指令重排。

2.4 有序性应用 多线程单例模式

public class App {
    private App(){}
    //volatile关键字禁止指令重排
    private volatile static App app;

    public static App getInstance(){
        if(app == null) {
            //只有第一次创建对象时才会进入同步块并加锁
            synchronized (App.class){
                //防止多个线程同时进入了第一个if
                if(app == null){
                    app = new App();
                }
            }
        }
        return app;
    }
}

标签:static,Thread,原子,flag,线程,有序性,volatile,多线程,public
From: https://www.cnblogs.com/chengxuxiaoyuan/p/16966067.html

相关文章

  • Python3 多线程并发处理的返回值收集
    库函数threading背景去查询python3多线程,可以找到一大堆关于threading库的博客教程,但是多数是几个线程执行同一个函数(他们博客里各个线程传入的函数参数不同),且没有......
  • 8 网络&多线程
    HeadFirstJava和AcWingJava课程做的总结8。所有网络运作的低层细节都已经由java.net函数库处理掉了。传送与接收网络上的数据只不过是链接上使用不同的链接串流的输......
  • java面试(多线程)
    1. Callable、Future、FutureTash详解Callable与Future是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable接口的类与实现Runnable的类都是可以被线程......
  • JUC5 多线程锁(下)
    1.​​synchronize​​锁升级:无锁,偏向锁,轻量锁,重量锁(看病:社区医院->三甲医院)1.1 概述按照获得锁和释放锁的性能消耗,锁的分类:1.无锁状态2.偏向锁:不进行​​CAS​​,测......
  • JUC4 多线程锁(上)
    1.乐观锁和悲观锁①.悲观锁什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改适合写操作多的场......
  • 多线程--面试题整理
    简述线程,程序、进程的基本概念线程:与进程相似,但线程是比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空......
  • 原子操作
    1定义​​一个操作是原子的(atomic),如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构。​​2简介在​​多进程​​​(​​​线程​​​)访问资源时,能......
  • 盘点JAVA中基于CAS实现的原子类, 你知道哪些?
    前言JDK中提供了一系列的基于CAS实现的原子类,CAS的全称是Compare-And-Swap,底层是lockcmpxchg指令,可以在单核和多核CPU下都能够保证比较交换的原子性。所以说,这些原子......
  • JAVA 实现多线程发售火车票
    publicclassdemo05{publicstaticvoidmain(String[]args){TicketWindowtw=newTicketWindow();newThread(tw,"窗口1").start();ne......
  • 深刻理解JAVA并发中的有序性问题和解决之道
    问题Java并发情况下总是会遇到各种意向不到的问题,比如下面的代码:intnum=0;booleanready=false;//线程1执行此方法publicvoidactor1(I_Resultr){if(rea......