【记录】并发编程 - 学习日志(五)

本文详细探讨了Java内存模型中的可见性问题,重点介绍了volatile关键字和synchronized在保证线程安全中的作用,以及两阶段终止模式、犹豫模式和内存屏障的概念,解释了指令重排对多线程一致性的影响。
摘要由CSDN通过智能技术生成

5. 共享模型之内存

5.1 Java 内存模型

java 内存模型,简称 JMM,包括主存(线程共享)和工作内存(线程私有)

5.2 可见性

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test1")
public class Test1 {
    static Boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (run) {

            }
        }, "t").start();

        Thread.sleep(1000);
        run = false;
        log.debug("run = {}", run);
    }
}
// 某次运行结果

16:12:03 [main] c.Test1 - run = false

为何在主线程中将 run 的值修改为 false 后,t 线程并未运行结束?

1. t 线程读取主存中 run 的值到其工作内存中;

2. 因 t 线程频繁向主存中读取 run 的值,故即时编译器(JIT 编译器)会将主存中 run 的值缓存在其工作内存的高速缓存中;

3. t 线程不在向主存中读取 run 的值,转为向即时编译器工作内存的高速缓存中读取 run 的值;

4. 1 秒后,主线程将其工作线程中 run 的值改为 false 并同步到主存中,但因【3】,故 t 线程未运行结束。


那么该如何解决上述问题呢?

这里提供两种解决方法。

volatile

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test1")
public class Test1 {
    static volatile Boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (run) {

            }
        }, "t").start();

        Thread.sleep(1000);
        run = false;
        log.debug("run = {}", run);
    }
}
// 运行结果

16:43:19 [main] c.Test1 - run = false

进程已结束,退出代码 0

使用 volatile 关键字修饰成员变量和类变量(静态变量)可强制线程仅能从主存中读取变量的值

volatile 关键字可以用来修饰局部变量吗?

因局部变量存储在工作内存中,故而无需从主存中读取,也就无需被 volatile 关键字修饰了。

局部变量存储在 JVM - 运行时数据区 - Java 虚拟机栈 - 栈帧 - 局部变量表中,属线程私有。

synchronized 

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test2")
public class Test2 {
    static Boolean run = true;
    static Object o = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (o) {
                while (true) {
                    if (!run) {
                        break;
                    }
                }
            }
        }, "t").start();

        synchronized (o) {
            Thread.sleep(1000);
            run = false;
            log.debug("run = {}", run);
        }

    }
}
// 运行结果

16:54:24 [main] c.Test2 - run = false

进程已结束,退出代码 0

volatile 和 synchronized 两种解决方法的区别

volatilesynchronized
可见性
原子性×
较轻量级重量级

5.3 两阶段终止模式

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(5000);
        tpt.end();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    private Thread t1;
    private volatile Boolean stop = false;

    public void start() {
        t1 = new Thread("t1") {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        log.debug("料理后事");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                        log.debug("无异常,执行监控记录");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();
    }

    public void end() {
        stop = true;
        t1.interrupt();
    }
}
// 某次运行结果

18:35:20 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:35:21 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:35:22 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:35:24 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.rui.jmm.TwoPhaseTermination$1.run(Test4.java:29)
18:35:24 [t1] c.TwoPhaseTermination - 料理后事

进程已结束,退出代码 0

 5.4 犹豫模式(Balking)

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination5 tpt = new TwoPhaseTermination5();
        tpt.start();
        tpt.start();
        tpt.start();
        Thread.sleep(5000);
        tpt.end();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination5 {
    private Thread t1;
    private volatile Boolean stop = false;

    public void start() {
        t1 = new Thread("t1") {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        log.debug("料理后事");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                        log.debug("无异常,执行监控记录");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();
    }

    public void end() {
        stop = true;
        t1.interrupt();
    }
}
// 某次运行结果

18:52:51 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:51 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:51 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:52 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:52 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:52 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:53 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:53 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:53 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:54 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:54 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:54 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.rui.jmm.TwoPhaseTermination5$1.run(Test5.java:31)
18:52:55 [t1] c.TwoPhaseTermination - 料理后事
18:52:55 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:55 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:55 [t1] c.TwoPhaseTermination - 料理后事
18:52:55 [t1] c.TwoPhaseTermination - 料理后事

如何避免同一时间同一线程多次启动呢?

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination5 tpt = new TwoPhaseTermination5();
        tpt.start();
        tpt.start();
        tpt.start();
        Thread.sleep(5000);
        tpt.end();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination5 {
    private Thread t1;
    private volatile Boolean stop = false;
    private Boolean starting = false;

    public void start() {
        t1 = new Thread("t1") {
            @Override
            public void run() {
                synchronized (this) {
                    if (starting) {
                        return;
                    }
                    starting = true;
                }
                while (true) {
                    if (stop) {
                        log.debug("料理后事");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                        log.debug("无异常,执行监控记录");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();
    }

    public void end() {
        stop = true;
        t1.interrupt();
    }
}
// 某次运行结果

19:04:21 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:22 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:23 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:24 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:25 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:25 [t1] c.TwoPhaseTermination - 料理后事

进程已结束,退出代码 0

5.5 有序性

指令重排:JVM 会在不影响(单线程)正确性的前提下调整语句的执行顺序

指令重排可能会影响多线程正确性

    // 符合指令重排条件
    int a = 1;
    int b = 2;

    // 不符合指令重排条件
    int c = 3;
    int d = c + 1;

5.6 内存屏障

  • 可见性

        在读屏障之后对共享变量的读取均来自主存

        在写屏障之前对共享变量的修改同步到主存

  • 有序性

        语句的执行顺序:

        读/写屏障之前的语句 -> 读/写屏障之后的语句

5.7 volatile 原理

volatile 的底层原理是 内存屏障

使用 volatile 修饰共享变量后,读取该变量的语句后会设置读屏障,修改该变量的语句前会设置写屏障。

    // 禁止指令重排
    int a = 1;
    volatile int b = 2;

为何仅 b 变量被 volatile 关键字修饰即可禁止指令重排

    // 禁止指令重排
    int a = 1;

    // --- 写屏障 ---
    volatile int b = 2;
    

double - checked locking(双检索)

以单例模式为例

package com.rui.jmm;

public final class Singleton {

    private Singleton() {
    }

    private static Singleton INSTANCE = null;

    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (INSTANCE == null) {
                INSTANCE = new Singleton();
            }
        }
        
        return INSTANCE;
    }
}

假设多个线程同时执行 getInstance 方法,那么这些线程皆需获取锁,该如何优化呢?

package com.rui.jmm;

public final class Singleton {
    
    private Singleton() {
    }

    private static Singleton INSTANCE = null;

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

可能先执行 return INSTANCE; 语句(指令重排),如何解决这个问题呢?

package com.rui.jmm;

public final class Singleton {

    private Singleton() {
    }

    private static volatile Singleton INSTANCE = null;

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    //写屏障
                    INSTANCE = new Singleton();
                }
                //读屏障
            }
        }
        // 读屏障
        return INSTANCE;
    }
}

5.8 happens - before

synchronized:线程拥有对象锁时对共享变量的修改 该线程释放该对象锁后,其他线程拥有对象锁时对共享变量的读取 可见

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test6")
public class Test6 {
    static int count = 1;
    static Object o = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (o) {
                count = 10;
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(count);
            }
        }, "t2").start();
    }
}
// 运行结果

10

进程已结束,退出代码 0

volatile:使用 volatile 关键字修饰共享变量后,线程对共享变量的修改  之后其他线程对共享变量的读取 可见

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test7")
public class Test7 {
    static volatile int count = 1;

    public static void main(String[] args) {
        new Thread(() -> {
            count = 10;
        }, "t1").start();
        new Thread(() -> {
            System.out.println(count);
        }, "t2").start();
    }
}
// 运行结果

10

进程已结束,退出代码 0

如果对共享变量的修改发生在线程的初始状态,那么该修改 对 就绪状态及其以后的该线程 可见

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test8")
public class Test8 {
    static int count = 1;

    public static void main(String[] args) {

        count = 5;

        new Thread(() -> {
            System.out.println(count);
        }, "t").start();
    }
}
// 运行结果

5

进程已结束,退出代码 0

终止状态前的线程对共享变量的修改 该线程运行结束后,其他线程对共享变量的读取 可见

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test9")
public class Test9 {
    static int count = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            count = 10;
        }, "t");

        t.start();

        t.join();
        System.out.println(count);
    }
}
// 运行结果

10

进程已结束,退出代码 0

t1 线程打断 t2 线程前对共享变量的修改 该修改后,其他线程对共享变量的读取 可见

package com.rui.jmm;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test10")
public class Test10 {
    static int count = 1;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            count = 5;
            t1.interrupt();
        }, "t2");


        t1.start();
        t2.start();

        while (true) {
            if (t1.isInterrupted()) {
                System.out.println(count);
                break;
            }
        }
    }
}
// 运行结果

5

进程已结束,退出代码 0

 对默认初始化的共享变量的修改 修改后,线程对共享变量的读取 可见


内存屏障

package com.rui.jmm;

public class Test12 {
    static volatile int x = 1;
    static int y = 5;

    public static void main(String[] args) {
        new Thread(() -> {
            y = 1;
            x = 5;
        }).start();

        new Thread(() -> {
            System.out.println("x = " + x);
            System.out.println("y = " + y);
        }).start();
    }
}
// 运行结果

x = 5
y = 1

进程已结束,退出代码 0

说些废话

本篇文章为博主日常学习记录,故而会小概率存在各种错误,若您在浏览过程中发现一些,请在评论区指正,望我们共同进步,谢谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值