JUC 并发编程
文章目录
理论基础
多线程解决了,CPU、内存、I/O之间的速度差异,出现了新的问题
CPU 怎加了缓存、以均衡与内存速度差异
导致可见性
问题操作系统增加了进程、线程、以分时复用CPU,进而均衡CPU与I/O设备的速度差异;
导致原子性问题
编译程序优化指令执行次序,使得缓存能够得到更加合理利用。
有序性问题
并发问题出现的三要素
可见性 cpu 缓存引起
一个线程对共享变量的修改,另一个线程立刻看到
在 T1 时间,线程1 读取到了 i 的值 并进行操作, 在值刷新到主内存之前, T2线程 来读取 i 的值。
原子性 分时复用引起
转账问题 A转 1000 给B ,A -1000 和 B +1000 必须一起成功,失败。
有序性 重排序引起
int i= 1;
i += 1;
i = 0;
正常顺序执行 i =0 ,但是在jvm 执行class 文件时,可能会发生 指令重排序(instruction Reorder)
- 执行程序时为了提高性能,编译器和处理器常常对指令重排序,有三种类型
- 编译器优化重排序: 编译器在不改变单线程程序语义对前提下,可以重新安排语句的执行顺序
- 指令级并行的重排序: 现代处理器采用了指令级并行技术(lnstruction-Level Parallelism , ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令执行顺序
- 内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和储存操作看上去可能是在乱序执行。
1 属于编译器重排序2,3属于处理重排序。这些重排序都可能会造成内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
对于处理重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers ,intel 称之为 memory fence )指令,通过内存屏障指令禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
java 处理并发问题 JMM(java内存模型)
JMM本质可以理解为,java内存模型规范,jvm主要 通过 volatile 、synchronized 、final 关键字 和 Happens - Before 规则
确保原子性、可见性、有序性
- 原子性
x=10; //常量赋值 是原子性操作
y=x; //非原子性 先去内存读取 x 在赋值
x ++; // 取x x+1 赋值x
x+=1;
x = X + 1;
对于 常量以外的 属性赋值,可以通过 synchronized 和Lock 实现,或者 JUC工具类原子操作类。
- 可见性
java提供了 volatile 关键字来保证可见性。它会保证修改的值会立刻更新到主存中,当有其他线程来取时,会去内存中读取新值(CAS自旋 但是对非原子操作的效果并不理想) 通过synchronized 和 Lock 来上锁执行同步代码块也能确保可见性。
- 有序性
JMM是通过 Happens - Before 规则来保证有序,同时 synchronized 和 Lock 锁定代码块来确保有序性,volatile 能保证一定的有序性
Happens - Before 规则
volatile 和 synchronized 来保证有序性。除此之外,jvm 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
- 单一线程原则 Single Threda rule
- 在一个线程内,在程序前面操作先行发生于后面的操作
- 管程锁定规则 Moitor Lock Rule
- 一个 unlock 操作先行发生于后面对同一个锁的lock 操作
- volatile 变量规则 volatile variable rule
- 对一个volatile 变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则 thread start rule
- thread 对象的 start() 方法调用先行发生于此线程每一个动作
- 线程加入规则 thread join rule
- thread 对象的结束先行与join()方法返回
- 线程中断规则 thread interruption rule
- 对线程 interrupt() 方法的调用先行发生于被中断的代码检测到中断事件的发生,可以通过 interrupted()方法检测到是否有中断发生
- 对象终结规则 Finalizer rule
- 一个对象初始化完成(构造函数执行结束)先行发生于它的 finalize()方法开始
- 传递性 transitivity
- 如果操作A 先行发生于操作B,操作B 先行发生于操作C,那么操作A 先行发生于操作C
线程安全
共享数据按照安全程度强弱顺序分成五类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。
-
不可变
final 修饰的基本数据类型
string、枚举
Number 部分子类: Long、Double 、BigInteger、BigDecimal 等大数据类型
可以使用
Collecions.unmodifiable...()
获取一个不可变集合Map<Object,Object> map = new HashMap<>(); Map<Object,Object> unmodifiableMap = Collections.unmodifiableMap(map);
-
绝对线程安全
不管运行时环境如何,调用者都不需要任何额外同步措施。
-
相对线程安全
相对线程安全需要保证这个对象时操作时线程安全的,在调用的时候不需要额外的保证措施。但时对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
Vector、HashTable、Collections 、SynchronizedCollection() …
-
线程兼容
线程兼容本身时指对象本身并不是线程安全的,但时可以通过调用端正确地使用同步手段来保证对象在并发环境总可以安全地使用,我们平常说一个类不是线程安全的,绝大多熟时候指的就是这一种情况。java API 中大部分类都是属于线程兼容的。
-
线程对立
线程对立时指无论调用端是否采用了同步措施,都无法在多线程环境中并发使用的代码。由于 java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码时很少出现的,而且通常都是有害的。
多线程
状态 | 方法 | 说明 |
---|---|---|
新建 new | 创建后线程未启动 | |
可运行 Runnable | 等待 调度器 分配cpu资源 | |
阻赛 Blocking | 等待获取一个排他锁 | |
无限期等待 waiting | 等待其他线程显式地唤醒,否则不会被分配cpu时间片 | |
Object.wait() / Object.notify()| notifyAll() | 线程睡眠 / 唤醒 | |
Thread.join() | 当前线程调用,等待指定线程结束 | |
LockSupport.park() | ||
限期等待 Timed Waiting | 等待一定的时间后被唤醒 | |
Thread.sleep() | 时间结束 | |
LockSupport.parNanos() | ||
LockSupport.parUntil() | ||
死亡 Terminated | 线程结束任务后自己 关闭,或异常关闭 |
使用方式
- 实现 Runnable 接口 :实现run 方法,用 thread 来调用
- 实现Callable 接口 : 有返回值,封装成 FutureTask 用thread 来调用
- 继承 Thread 类
基础线程机制
Executor
管理多个异步任务执行,不需要显式地管理线程的生命周期。
- CachedThreadPool : 一个任务创建一个线程
- FixedThreadPool: 所有任务只能使用固定大小的线程
- SingleThreadExecutor: 相当于大小为1 的FixedThreadPool
ExecutorService executorService = Executors.newCachedThreadPool();
...;
executorService.shutdown();
Daemon
守护线程是程序运行时后台提供服务的线程,不属于程序不可或缺部分。所有非守护线程结束后,程序终止,杀死守护线程。
sleep()
当前线程进入休眠状态,不会放弃cpu执行权,和锁。
yield()
当前线程已经完成了生命周期中重要的部分,可以切换给其他线程来执行。此方法是对线程调度器的一个建议。
synchronized
jvm 实现 一次只有一个线程能获取 锁
每个对象都有一个this 锁,多线程操作同一个对象才行。 static 方法 和*.calss 是所有对象公用一把锁。
synchronized 修饰方法, 正常结束或者异常结束,都会释放锁。
可使用IDEA插件jclasslib Bytecode Viewer
来查看编译后class文件
synchronized 会在文件中加入monitorenter
和monitorexit
,来操作锁计数器+1 或-1,每一个对象在同一时刻只与一个 monitot锁关联。
- 当 monitor 为0 就代表当前锁没有被获取,(不记得源码了。线程判断 if(monitor == 0) ) 来获取锁。当计数器为1时,其他线程不可获取锁。 可重入锁就时( 不是源码 if( monitor != 0 && 当前线程获取锁 ) ) 来判断当前线程是否可以再次获取锁
锁优化
当其他线程获取不到锁的时候,如果一致尝试获取锁,会占用cpu大量执行时间,进入阻赛放弃cpu执行权的开销又可能过大。
jdk引入了 粗化锁、锁消除、轻量级锁、偏向锁、适应性自旋、等等锁技术
锁 | 说明 |
---|---|
锁粗化 Lock Coarsening | 也就是减少不必要紧链接在一起的 unlock ,lock 操作,将多个连续的锁扩展成一个范围更大的锁。 |
锁消除 Lock Elimination | 通过运行时JIT 编译器 的逃逸分析来消除一些没在当前同步块以外被其他线程共享的数据锁保护,通过逃逸分析可以在线程本stack 上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销) |
轻量级锁 Lightweight Locking | 这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(但线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面重量级互斥锁,取而代之的是monitorenter 和 monitorexit中只需要依靠一条CAS 原子指令就可以完成锁的获取以及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞装,当锁被释放时被唤醒 |
偏向锁 Biased Locking | 为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令 |
适应性自旋Adaptive Spinning | 当线程获取轻量级锁的过程中执行CAS操作失败时,在进入与 monitor 相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,尝试到指定次数任然没有成功则调用(monitor)关联的semaphore 进入阻赛状态 |