Java并发编程笔记2-线程可见性&线程封闭&指令重排序

简介: 一.指令重排序 例子如下: public class Visibility1 { public static boolean ready; public static int number; } public class ReaderThread extend...

一.指令重排序

例子如下:

public class Visibility1 {
    public static boolean ready;
    public static int number;
}
public class ReaderThread extends Thread {

    @Override
    public void run() {
        while (!Visibility1.ready){
            Thread.yield();
            System.out.println(Visibility1.number);
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        new ReaderThread().start();
        Visibility1.number = 42;
        Visibility1.ready = true;
    }
}

多次运行结果分别如下:

可以看到多次运行所得到三种结果,分别为0,42,没有输出结果。

程序一开始执行,默认将ready赋值为false,ready默认赋值为0,一开始执行时,在ReaderThread中符合循环条件,进入循环,遇到

Thread.yield();
这语句是就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。如果自己先走出现结果为0的情况,因为,默认初始int的类型数据为0,如果让别的线程先走让这样就又回到了Main方法中的继续执行
Visibility1.number = 42;
Visibility1.ready = true;
这两条语句,问题来了,这两条语句会按照程序的顺序执行吗?
答案是,并不一定的。这就涉及到指令重排序问题,也就是说

CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。

指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。

重排序的目的是为了性能。

也就是说,有时候CPU认为

Visibility1.ready = true; 语句执行要快,所以让Visibility1.ready = true;先执行。这就导致了会出现了无输出结果。这就是涉及了指令重排序问题,虽然这里如果出现了指令重排序问题,
进入生产开发会造成严重的后果的。
但是要注意一点,如下:

 

二.可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。

所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就这这个操作同样存在线程安全问题。

先看如下程序:

public class Visibility {
    private static boolean bChanged;

    public static void main(String[] args) throws InterruptedException {
        new Thread(){
            @Override
            public void run() {
                for (; ; ){
                    if (bChanged == true){
                        System.out.println("!=");
                        System.exit(0);
                    }
                }
            }
        }.start();
        Thread.sleep(10);

        new Thread(){
            @Override
            public void run() {
                for (; ; ){
                    bChanged = true;
                }
            }
        }.start();
    }

}

运行结果为:


可以看到运行为死循环。按道理来说应该会输出!=结束的。但是为什么会出现死循环呢?
原因是因为程序中bChanged如果没有手动赋值前,默认是false的,所以一开始没输出,但是第二线程命名修改了bChanged的值了,为什么还不退出呢,因为线程是CPU启动的,而CPU一开始从主存中取数据并没有立即将数据送到CPU,而是先送到了缓存中,
而,另外一个线程修改bChanged的值,是就该主存中的值,而那个输出结果的线程并没有从主存中取bChanged的值,而是去缓存中取了bChanged的值,而缓存中bChanged的值是false,这就是为什么会死循环的原因。如下图:


解决办法:
  1.在成员变量中加入关键字volatile,如下:
  
private volatile static boolean bChanged;


这个关键字告诉线程一定要走内存,不要再从缓存中获取数据了。只要已修改,其他线程立马知道值已经被修改了,因为它们都是从主存中取的数据。
修改后的结果如下图:

可以看到程序立马结束。并且有输出。因为这个关键字让这个成员变量都可见了。这就是线程的可见性。

其实volatile关键字还有一个功能就是:阻止指令排序。但是这只是相对的阻止。如下:

以前官方推荐是volatile,但是现在synchronized已经优化的很好了。不要刻意去用volatile。如果一个类加上volatile关键字,那么它的成员变量也会默认加上volatile关键字。

volatile只能解决可见性。一定程度上解决指令排序,但是是相对的。

但是volatile解决了可见性,就一定可以解决问题了吗?并不是的。例子如下:

如果你只希望单一变量可见性,volatile是可以的,但是如果关注的不是单一变量volatile就不管用了。

2.用synchronize加锁的办法。它能解决可见性,原子性。

 

三.线程封闭

实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。什么是线程封闭呢?

当访问共享变量时,往往需要加锁来保证数据同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程中访问数据,就不需要同步了。这种技术称为线程封闭。在Java语言中,提供了一些类库和机制来维护线程的封闭性,例如局部变量和ThreadLocal类,

线程封闭其实就是不共享数据就行了。

线程封闭方法有:

  1.不要共享变量,在变量中加final关键字

  2.栈封闭:栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的
局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。如果了解JVM应该懂得。

  3.ThreadLocal线程绑定。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。

想要操作什么数据,可以先把数据放在ThreadLocal中绑定,用的时候才取出来。

例子如下:

public class LocalTest {
    private int num;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}
public class ThreadLocalDemo {
    private static ThreadLocal<LocalTest> threadLocal = new ThreadLocal<LocalTest>();

    public static void main(String[] args) throws InterruptedException{
         final LocalTest local = new LocalTest();

        new Thread(){
            @Override
            public void run() {
                for (; ; ){
                    threadLocal.set(local);
                    LocalTest l = threadLocal.get();
                    l.setNum(20);
                    System.out.println(Thread.currentThread().getName() + "---" +threadLocal.get().getNum());
                    Thread.yield();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (; ; ){
                    threadLocal.set(local);
                    LocalTest l = threadLocal.get();
                    l.setNum(30);
                    System.out.println(Thread.currentThread().getName() + "---" +threadLocal.get().getNum());
                    Thread.yield();
                }
            }
        }.start();

    }

}

结果如下:

 

可以看到各个线程对应的值并没有乱。
目录
打赏
0
0
0
0
54
分享
相关文章
|
1月前
|
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
49 17
|
1月前
|
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
60 26
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
3月前
|
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
287 2
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程
|
16天前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
142 60
【Java并发】【线程池】带你从0-1入门线程池
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
55 23
|
12天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
81 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
106 14

热门文章

最新文章