java sleep println方法对线程间变量的可见性的影响

最近在看java volatile关键字验证可见性时遇到的问题

volatile保证可见性

线程修改变量时先将变量从主内存中拷贝到工作内存中,修改完成后在刷新到主内存。如果有多个线程并发修改共享变量就会出现1、2同时拷贝变量到工作内存,1线程修改后,2线程是感受不到的。

示例:

public class VolatileDemo {
    int  num = 0;

    public void addNum(){
        this.num +=1;
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(()->{
            try{
                Thread.sleep(3000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            volatileDemo.addNum();
        },"1").start();

        while(volatileDemo.num == 0){
        }
        System.out.println("具有可见性");
    }
}

由于num不具有可见性,main线程会一直在while处循环。

我在上边例子的基础上增加了sleep和println方法后num变量具有了可见性,代码如下:

public class VolatileDemo {
    int  num = 0;

    public void addNum(){
        this.num +=1;
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(()->{
            try{
                Thread.sleep(3000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            volatileDemo.addNum();
        }).start();

        while(volatileDemo.num == 0){
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("num==0");
        }
        System.out.println("具有可见性");

    }
}

原因:导致num具有可见性的原因有两处,①是sleep方法,②是println方法。

①:sleep前不一定会将工作内存的变量更新到主内存中,sleep后也不一定会从主内存中重新加载。

java文档https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html中关于sleep和yield的解释

②:println方法中有synchronized同步锁,锁的获取、释放都会把当前线程的共享变量刷新到主内存。

    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

### Java 线程可见性问题及解决方案 #### 1. 可见性问题概述 在多线程环境中,当一个线程修改了一个共享变的值时,其他线程可能无法及时看到该变的最新值。这种现象被称为 **可见性问题**[^1]。这是因为每个线程都有自己的工作内存(Work Memory),而这些工作内存中的数据可能是主内存(Main Memory)的一个副本。如果某个线程更新了其工作内存中的变值,但未立即将其同步回主内存,则其他线程的工作内存可能会继续使用旧值。 以下是可能导致可见性问题的原因: - 编译器优化:为了提高性能,编译器可能会重新排列指令顺序。 - CPU缓存机制:现代CPU通常会有多个级别的缓存,这使得某些操作的结果仅存储在局部缓存中而不是全局内存中。 ```java public class Test { private static boolean stop = false; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { int i = 0; while (!stop){ i++; } System.out.println("Result:" + i); }); thread.start(); Thread.sleep(1000); stop = true; // 修改后的值可能不会立刻被子线程感知到 } } ``` 在这个例子中,`stop` 被设置为 `true` 后,主线程期望子线程能够退出循环并终止运行。然而,由于缺乏必要的同步措施,子线程可能始终看不到最新的 `stop` 值,从而导致无限循环[^1]。 --- #### 2. 解决方案 ##### (1)使用 `volatile` 关键字 通过声明变为 `volatile`,可以确保对该变的所有读取都直接从主内存中获取,而非线程本地缓存;同时所有的写入也会立即刷新至主内存[^2]。这样就解决了因缓存而导致的数据不一致性问题。 ```java public class VolatileTest { private volatile static boolean stop = false; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { int i = 0; while (!stop){ i++; } System.out.println("Result with volatile:" + i); }); thread.start(); Thread.sleep(1000); stop = true; // 使用 volatile 后,子线程能正确检测到变化 } } ``` 注意:虽然 `volatile` 提供了可见性的保障,但它并不具备原子性支持。对于复合赋值运算(如自增操作 `count++`),仍需额外考虑同步手段来维护线程安全性。 ##### (2)利用 `synchronized` 锁定资源 `synchronized` 不仅提供了互斥访问控制功能,还隐含着建立发生前关系的作用——即每次进入或离开监视器之前都会触发相应的内存屏障动作,强制将当前线程持有的所有脏数据刷回到主存,并使后续线程重载目标对象的状态[^2]^。 下面展示了一种基于锁的方式实现相同效果: ```java public class SyncTest { private static boolean stop = false; public static synchronized void setStop(boolean value){ stop = value; } public static synchronized boolean getStop(){ return stop; } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { int i = 0; while (!getStop()){ i++; } System.out.println("Synchronized Result:" + i); }); thread.start(); Thread.sleep(1000); setStop(true); // 设置标志位并通过同步方法传播给其它线程 } } ``` 此处定义的一对 getter/setter 方法均加上了同步修饰符,保证任何时刻只有一个线程可对其进行更改或者查询操作,进而消除潜在竞争条件带来的隐患。 ##### (3)采用高级并发工具类 除了基本的关键字外,在实际开发过程中更推荐借助 JDK 自带的一些专门设计用于处理复杂场景下的并发问题的支持库,像 AtomicXXX 类族成员便是一大亮点之一。它们内部实现了无锁算法,能够在大多数情况下提供优于传统锁定策略的表现特性的同时兼顾高效与稳定两方面需求[^4]. 例如替换掉原始布尔型字段改用 AtomicInteger 来表示停止信号: ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicTest { private static final AtomicInteger STOP_FLAG = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { long count = 0L; while (STOP_FLAG.get() == 0){ count++; } System.out.println("Atomic Count:" + count); }); thread.start(); Thread.sleep(1000); STOP_FLAG.set(1); // 利用 CAS 实现精确数值比较交换过程完成状态切换 } } ``` 以上三种方式各有优劣之处,具体选用哪一种取决于应用场景的具体要求以及团队技术积累程度等因素综合考决定。 --- ### 总结 Java 中的可见性问题是多线程环境下常见的挑战之一。通过对共享变应用合适的同步机制,可以有效缓解此类难题的发生概率及其影响范围。常用的解决办法包括但不限于标记易变属性为 `volatile`, 运用内置锁结构 (`synchronized`) 或者调用更高层次抽象封装好的 API 接口等途径达成目的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宝藏程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值