并发编程(七)

92 篇文章 0 订阅
10 篇文章 0 订阅

Java JMM(Java Memory Model, 内存模型)

  • JMM定义了主存, 工作内存(抽象概念). 底层对应着 CPU寄存器, 缓存, 硬件内存, CPU指令优化等

JMM体现在以下几个方面
原子性: 保证指令不会受到线程上下文切换的影响
可见性: 保证指令不会受 CPU缓存的影响
有序性: 保证指令不会受 CPU指令并行优化的影响

可见性

  • 当一个子线程频繁的从主线程读取共享变量 a时, JIT编译器会将 a的值, 缓存到自己工作内存中, 来提高效率. 此时即使在主线程, 再修改 a的值, 子线程中依然是旧值, 也就是不会同步的(不可见)
  • 例子:

public class App {
    static boolean a = true; // 共享变量加上 volatile修饰, 则会对所有线程一直是可见的
    static int i;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (a) {
                i++;
            }
            System.out.println("end loop!");
        });
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("stop!" + i);
        a = false;
    }
}

* 在以上例子中, 在变量 a声明处, 加上关键字 volatile, 就可以让子线程一直同步 a的变动(可见性), 也就是不进行 a的缓存

volatile(易变关键字):

  1. 被 volatile修饰的共享变量总是对所有的线程是可见的, 也就是当一个线程修改了一个被 volatile修饰的共享变量的值, 新值总会被其它线程立即得知
  2. 禁止指令重排序优化
    * volatile不能保证原子性, 仅用在一个写线程, 多个读线程的情况

有序性

  • JIT编译器在不影响正确性的前提下, 会在运行时的代码做一些优化(也就是调整语句的执行顺序), 这称之为指令重排, 此优化在多线程下, 会影响结果的正确性
  • 通过并发压测项目复现指令重排引起的异常结果:

# 1. 在项目根目录处, 执行下载, 并加到 Maven Module里
$ mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0 

# 2. 以下代码加到压测项目内, 复现异常结果0, 可以指定多个 如: id={"1", "4"}
@JCStressTest
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "unexpected value!")
@State
public class ConcurrencyTest {
    int num = 0;
    boolean ready = false;
    @Actor
    public void actor1(I_Result r) { // 线程1执行的代码
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) { // 线程2执行的代码
        num = 2;
        ready = true; // 此处, 更改顺序上在 ready变量上加 volatile, 则可以将之前的 num变量一起防止指令重排序
    }
}

# 3. 将压测项目打包
$ mvn clean install 

# 4. 开始压测
$ java -jar ordering\target\jcstress.jar

# 5. 从压测日志中查出结果为0的复现次数

以上压测代码的标准结果为3种:

  • 情况1:线程1先执行, 此时 ready=false, 因此 r1结果为 1
  • 情况2:线程2先执行 num=2, 但没来得及执行 ready=true, 线程1执行了, 此时结果还是1
  • 情况3:线程2执行到 ready=true, 线程1执行了, 此时通过 if条件, 因此结果为4(因为已执行过 num=2)
    * 指令重排导致的, 最后一种情况是:
  • 情况4: 线程2先执行, 此时指令重排, ready=true和 num=2的顺序被切换, 先执行了 ready=true, 然后切换到线程1, if条件通过, 相加值为0的 num, 最终结果为0
    * 解决方法: 将共享变量修饰为 volatile, 这样可以禁用指令重排

volatile原理

volatile的底层实现原理是内存屏障(Memory Barrier/Memory Fence)

  • 在加了 volatile的变量, 写指令后会加入写屏障(sfence), 读指令前会加入读屏障(lfence)
  • volatile保证了共享变量的可见性和有序性, 但无法保证原子性. 而 synchronized是可以保证原子性
  1. 如何保证可见性
  • 写屏障: 在该屏障之前的共享变量的改动, 都同步到主存中
  • 读屏障: 在该屏障之后的共享变量的读取, 都是主存中的最新数据
  1. 如何保证有序性
  • 写屏障会确保指令重排序时, 不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时, 不会将读屏障之后的代码排在读屏障之前

*写屏障仅仅是保证了之后的读能够读到最新的结果, 但不能保证读跑到它前面去. 而有序性的保证也只是保证了当前线程内相关代码的不被重排序. 也就是说无法避免指令交错

原子性

  • 原子性指一个操作是不可被中断的. 即使在多个线程一起执行的时候, 也就是一个操作一旦开始, 就不会被其它线程干扰. 主要是通过加锁方式 如 synchronized, ReentrantLock或 CAS来做
    *注 synchronized语句, 既可以保证代码块的原子性, 也同时保证可见性. 缺点是 synchronized属于重量级操作, 性能低下

终止模式之两阶段终止模式

  • 如何优雅的终止指定线程, 指的优雅意思是, 给被终止后处理后事的机会
  • 两阶段终止模式:
  1. interrupt()& isInterrupted()中断线程方式:

class CCTV {
    private Thread thread;
    public void start() {
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(current.isInterrupted()) {
                    System.out.println(current.getName() + ", 被终止后处理后事!");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(current.getName() + ", 将结果保存!");
                } catch (InterruptedException e) {
                    System.out.println(current.getName() + ", 监控被中断!");
                    current.interrupt();
                }
                // 执行监控操作
            }
        },"监控器");
        thread.start();
    }

    public void stop() {
        System.out.println(Thread.currentThread().getName() + ", 终止监控!");
        thread.interrupt();
    }
}

public class App {
    public static void main(String[] args) throws InterruptedException {
        CCTV t1 = new CCTV();
        t1.start();
        TimeUnit.MILLISECONDS.sleep(4500);
        t1.stop();
    }
}
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
main, 终止监控!
监控器, 监控被中断!
监控器, 被终止后处理后事!

  1. 通过 volatile修饰的共享变量, 作为停止的标记:

class CCTV {
    private Thread thread;
    private volatile boolean stop = false; // 共享变量 volatile修饰, 保证该变量在多个线程之间的可见性
    public void start() {
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(stop) {
                    System.out.println(current.getName() + ", 被终止后处理后事!");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(current.getName() + ", 将结果保存!");
                } catch (InterruptedException e) {
                    System.out.println(current.getName() + ", 监控被中断!");
                }
                // 执行监控操作
            }
        },"监控器");
        thread.start();
    }

    public void stop() {
        System.out.println(Thread.currentThread().getName() + ", 终止监控!");
        stop = true;
        thread.interrupt();
    }
}

public class App {
    public static void main(String[] args) throws InterruptedException {
        CCTV t1 = new CCTV();
        t1.start();
        TimeUnit.MILLISECONDS.sleep(4500);
        t1.stop();
    }
}
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
main, 终止监控!
监控器, 监控被中断!
监控器, 被终止后处理后事!

同步模式之 Balking

  • Balking(犹豫)模式: 当点击启动时, 如果某线程已在运行中, 则不能重复启动

# 例子1
class MonitorService {
    // 判断是否已有线程在监控着
    private volatile boolean running;
    public void start() {
        synchronized (this) {
            if (running) {
                return;
            }
            running = true;
        }
        // 启动监控线程
    }
}

# 例子2
public final class Singleton {
	private Singleton() {}
	private static volatile Singleton INSTANCE = null;
	public static Singleton getInstance() {
		if (INSTANCE == null) { // double-checked locking
			synchronized (Singleton.class) { // 为缩减锁的范围
				if (INSTANCE == null) { // double-checked locking
					INSTANCE = new Singleton();
				}
			}
		}
		return INSTANCE;
	}
}

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值