一, 前言
主要讲解一下多线程中的一些概念, 本文之后就开始针对 JUC 包的设计开始解读;
二, 概念
线程安全
1. 存在共享数据(临界资源);2. 多个线程同时操作共享数据; 只有同时出现这两种情况的时候才会造成线程安全问题;
解决线程安全
同一时刻有且只有一个线程在操作共享数据, 其他线程必须等到该线程处理完数据以后在对共享数据进行操作;
多线程特性
原子性
现在的操作系统主要是通过时间分片的形式来管理线程或者进程, Java 编程语言一句语言需要多条 CPU 指令来完成, Java 在多线程切换的时候由于不满足原子性的特征, 导致共享变量产生意料之外的结果; 典型的 count+=1, 如下图, 共享变量的 count 的指最终结果是 1 而不是 2;
可见性
在多处理器 (CPU) 系统中, 每个处理器都有自己的高速缓存, 而它们又共享同一主内存, 整体结构如下图:
在单核 CPU 的情况下, CPU 缓存与内存数据一致性问题容易处理, 因为所有线程的操作都是针对同一个 CPU 的缓存, 一个线程对缓存的写对于另外的线程一定是可见的, 整体的执行情况如下图:
在多 CPU 的情况下, 每个 CPU 都有自己的缓存, 这个时候每个共享变量在 CPU 中的缓存都是不可见的, 这个时候就产生了 CPU 缓存与内存数据一致性的问题, 整体执行的情况如下图, 由于 count 变量分别在不同的 CPU 上执行, 相互看不到对方的操作, 这个时候变量 count 就会不一致, 产生意料之外的结果, 针对这种我也写了一个 demo;
- /**
- * @author wtz
- *
- * 线程可见性 demo
- */
- public class ThreadVisiable {
- private int count = 0;
- private void add() {
- int retry = 0;
- while (retry <10000) {
- count += 1;
- retry++;
- }
- }
- public int sumCount() throws InterruptedException {
- Thread thread1 = new Thread(() -> {
- add();
- });
- Thread thread2 = new Thread(() -> {
- add();
- });
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- return count;
- }
- public static void main(String[] args) throws InterruptedException {
- ThreadVisiable threadVisiable = new ThreadVisiable();
- int count = threadVisiable.sumCount();
- System.out.println(count);
- }
- }
- View Code
有序性
编译器为了优化性能, 有的时候会改变程序中语句的执行顺序, 在 Java 经典的双重检查创建单例模式, 就是其中的一个体现, 代码如下图:
- /**
- * @author wtz
- * <p>
- * 双重锁定单例模式 指令重排
- */
- public class Singleton {
- private static Singleton singleton;
- private Singleton() {
- }
- public static Singleton getInstance() {
- if (singleton == null) {
- synchronized (Singleton.class) {
- if (singleton == null) {
- singleton = new Singleton();
- }
- }
- }
- return singleton;
- }
- }
- View Code
代码整体上看起来无任何瑕疵, 但是实际这个方法并不完美, 问题出在 new 的操作上, 正常情况下 new Singleton()执行的操作如下步骤:
1. 在内存中分配一块空间;
2. 在内存上初始化 Singleton 对象;
3. 将内存地址赋值给 singleton 变量;
经过编译器优化以后可能是这个样子:
1. 在内存中分配一块空间;
2. 将内存地址赋值给 singleton 变量;
3. 在内存上初始化 Singleton 对象;
优化以后当 CPU 时间片切换时间刚好是线程 B 判断为空的时候, 这个时候 singleton 此时不为空, 不需要进入锁中, 这个时候就返回为初始化的 singleton, 整体性执行过程如下图:
竞态条件 & 临界区
当两个线程竞争同一资源时, 如果对资源的访问顺序敏感, 就称存在竞态条件. 导致竞态条件发生的代码区称作临界区;
互斥锁
互斥锁解决了并发程序中的原子性问题, 保证同一时刻只有一个线程执行, 保证了一个或多个操作在 CPU 执行的过程中不被中断, Java 原生语言主要是通过 synchronized 实现互斥锁;
Java 内存模型
Java 内存模型主要是为了解决内存可见性和指令重排 (编译优化) 的问题, 使用内存模型约束了 CPU 缓存和编译优化; Java 内存模型 (JMM) 从不同的角度来说都可以说很多的东西, 比如从线程角度来说, JMM 规范不同线程之间线程通信的问题, 从操作系统的角度来说, JMM 规范了工作线程与内存之间访问的问题; 我们主要从程序员角度来看这个问题的话我认为可以从三方面说起:
1.volatile,synchronized 和 final 语义;
2.JUC 并发包;
3.happens-before;
我们主要说第 3 点, 第 1,2 点以后在补充, 我们先要明白一些概念, 才能更好的理解后面的一些内容; happens-before 主要有 8 个原则, 我们通俗的话来讲讲:
1. 程序的顺序性, 单线程的每个前面的操作优先于后面的操作;
2.volatile, 对于 volatile 修饰的变量, 写的操作一定优先于读的操作, 也就是说对变量写操作对于后续的读操作都是可见的;
3. 锁, 解锁的操作优先于加锁的操作, 在 Java 锁指的就是 synchronized, 变量在解锁之前的操作, 在重新加锁之后一定可以看到;
4. 传递性, A 优先于 B,B 优先于 C, 则 A 优先于 C;
5. 线程开始原则, 主线程 A 启动子线程 B, 则子线程 B 能够看到主线程 A 在启动 B 子线程之前的操作;
6. 线程终止原则, 主线程 A 等待子线程 B 完成, 当子线程 B 完成以后, 主线程 A 能够看到子线程的 B 的操作;
7. 线程中断原则, 对线程 interrupt()方法优先于发生被中断线程检测到中断事件的发生;
8. 对象终结规则, 构造函数的执行一定优先于它的 finalize 方法;
等待 - 通知机制
等待 - 通知机制主要是为了处理循环等待造成的 CPU 消耗问题, 主要有以下两个步骤:
1. 线程首先获取互斥锁, 当线程要求的条件不满足时, 释放互斥锁, 进入等待状态;
2. 当要求的条件满足时, 通知等待的线程, 重新获取互斥锁;
Java 原生语言主要是通过 synchronized + wait + notify/notifyAll 实现;
活跃性
死锁
死锁的定义一组相互竞争资源的线程因相互等待, 导致永久阻塞的现象, 发生死锁必备的四个条件:
1. 互斥, 共享资源同时只有占用一个线程;
2. 占有且等待, 线程 A 获取共享资源 X, 在等待共享资源 Y 的时候, 不是释放共享资源 Y;
3. 不可抢占, 其他线程不能抢占线程 A 获取的共享资源;
4. 循环等待, 线程 A 等待线程 B 获取的共享资源, 线程 B 等待线程 A 获取的共享资源;
只要破坏其中任意一个条件就可以跑坏死锁;
活锁
线程之间互相谦让, 导致线程无法执行下去, 解决方案通过给线程随机等待一个时间;
饥饿
线程不能正常的访问共享资源, 并且无法执行下去, 解决线程饥饿的办法:
1. 保证资源的公平性, 也就线程的优先级一样;
2. 保证资源充足;
3. 避免线程长时间占用锁执行;
三, 结束
欢迎大家加群 438836709! 欢迎大家关注我!
来源: https://www.cnblogs.com/wtzbk/p/11489059.html