Java并发(一)-相关概念
Java 并发相关概念
并发
并发是在单核处理机上所任务运行的一种方式,根本上是串行的运行方式,通过时间多路复用算法(复用 CPU)达到部分并行的效果
并行
多核 cpu 运行方式
计算密集
大量计算
IO 密集
频繁 IO 读写
死锁
造成死锁的四个必要条件:
- 互斥
- 请求与等待
- 不可剥夺条件
- 循环与等待
而破坏死锁的条件即是:破坏以上任意一个条件即可
饥饿
长时间未等待到 cpu 调度
竞争条件
不可抢占资源在某一时刻只能有一个线程占有,因此对于不可抢占资源的使用需要线程竞争
原子性
原子是世界上最小的单位,具有不可分割性
一个操作时原子操作,那么我们称他具有原子性
可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的指,其他线程能够立即看到修改的值
有序性
程序执行的顺序按照代码的先后顺序执行
内存栅栏
内存栅栏(memory barrier)就是从本地或工作内存到主存之间的拷贝动作。
仅当写操作先跨越内存栅栏,读操作后跨越内存栅栏的情况下,读操作才可以对写操作的更新可见。
关键字 synchronized 和 valatile 都强制规定了所有的变更必须全局可见,该特性有助于跨越内存栅栏
在程序运行过程中,所有的更改会在高速缓存或者寄存器中完成,然后才会拷贝到主存一跨越内存栅栏。这种跨越序列或顺序称之为 happen###before
缓存一致性
写操作更改发生在缓存中,缓存与主存数据需要保持一致性
如何解决缓存不一致的问题:
- 通过在总线 LOCK 加锁的方式:
因为 CPU 和其他部件通信都是通过总线进行数据传输的,如果对总线加 LOCK 锁的话,也就是说阻塞了其他 CPU 对内存的访问,从而使得这一个只有一个 CPU 能只用这个变量的内存 - 通过缓存一致性协议
最出名的就是 Intel 的 MESI 协议,MESI 协议保证了每个缓存中使用的共享变量的副本都是一致的。他的核心思想是:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 缓存中也存在该变量的副本,会发出信号通知(广播)其他 CPU,将该变量的缓存行置为无效,因此,当其他 CPU 需要读取这个变量时候,发现自己缓存的变量是无效的,那么它会立即从内存中重新读取
CAS
CAS 算法是由硬件直接支持来保证原子性的,有三个操作数:内存位置 V、旧的预期值 A 和新值 B,当且仅当 V 符合预期值 A 时,CAS 用新值 B 原子化地更新 V 的值,否则,它什么都不做。
CAS 的 ABA 问题
当然 CAS 也并不完美,它存在”ABA”问题,假若一个变量初次读取是 A,在 compare 阶段依然是 A,但其实可能在此过程中,它先被改为 B,再被改回 A,而 CAS 是无法意识到这个问题的。CAS 只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的 ABA 漏洞。
锁相关
可重入锁
如果锁具备可重入性,则称作为可重入锁。像 synchronized 和 ReentrantLock 都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个 synchronized 方法时,比如说 method1,而在 method1 中会调用另外一个 synchronized 方法 method2,此时线程不必重新去申请锁,而是可以直接执行方法 method2。1
2
3
4
5
6
7
8public MyClass{
public synchronized void method1(){
method2();
}
public synchronized void method2(){
}
}实现原理:每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为 0 时,这个锁可以被认为是 unheld 的,当一个线程请求一个 unheld 的锁时,JVM 记录锁的拥有者,并把锁的请求计数加 1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出 syncronized 块时,计数器减 1,当计数器为 0 时,锁被释放。
可中断锁
可中断锁:顾名思义,就是可以响应中断的锁。在 Java 中,synchronized 就不是可中断锁,而 Lock 是可中断锁。
如果某一线程 A 正在执行锁中的代码,另一线程 B 正在等待获取该锁,可能由于等待时间过长,线程 B 不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
ReentrantLock lock = new ReentrantLock(true); \\公平锁
实现原理: 通过链表记录线程请求锁的顺序
读写锁
更加细粒度的锁
读写锁将对一个资源(比如文件)的访问分成了 2 个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口。
可以通过 readLock()获取读锁,通过 writeLock()获取写锁
偏向锁
java 偏向锁(Biased Locking)是 java6 引入的一项多线程优化.它通过消除资源无竞争 q 情况下的同步原语,进一步提高了程序的运行性能.偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM 会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用)
因此 流程是这样的 偏向锁->轻量级锁->重量级锁
偏向锁,简单的讲,就是在锁对象的对象头中有个 ThreaddId 字段,这个字段如果是空的,第一次获取锁的时候,就将自身的 ThreadId 写入到锁的 ThreadId 字段内,将锁头内的是否偏向锁的状态位置 1.这样下次获取锁的时候,直接检查 ThreadId 是否和自身线程 Id 一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。
乐观锁、悲观锁
乐观锁总是认为不会产生并发问题,每次读取数据的时候认为不会有其他线程对数据进行修改,因此不会上锁。
但是在更新的时候会判断其他线程在这之前有没有对数据进行修改,一般会使用版本控制机制或 CAS 实现。
实现原理:
- 版本控制机制
一般是在数据表中加上一个数据版本号字段(version,或者版本更新时间字段),表示数据被修改的次数,当数据被修改时,version 值会加 1.
当线程 A 要更新数据时,读取数据的同时也会读取 version,提交更新时对比 version,若之前读取的 version 与此刻数据库中 version 相等时进行更新,否则重试更新操作,直到更新成功。(类似锁的自旋)
- CAS
即 compare and swap 或者 compare and set,涉及到三个操作数:数据所在的内存值,预期值,新值。
当需要更新时,判断内存值(公共)与旧的预期值(之前取的值)是否相等,若相等,则没有被其他线程修改过,使用新值进行更新,否则进行重试,一般情况下是一个自旋,即不断的重试。
本文作者 : 对六
原文链接 : http://duiliuliu.github.io/2019/05/10/java并发一/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
你我共勉!