java中的并发
并发
并发遇到的问题
读写冲突
和写写冲突
计算机的发展从单核想多核发展,实现了真正的并行.当多个cpu中执行的线程试图同时去修改内存中的同一块地址的内容的时候,这个时候就出现了冲突读写冲突
或者写写冲突
.这个时候就需要一种同步机制来协调这些冲突.- 内存可见性
cpu的执行速度比内存读取速度差很多,cpu引入了多级缓存来帮助缓和内存和cpu的差异的矛盾.这个时候每个内核中都有缓存.当两个内核中的线程对于某一段内存空间做的修改如何让另外一个内核感知到,在现代cpu技术中适用了硬件的协议帮助实现这种变化感知.
jvm是跨平台的系统.不同平台有不同内存模型活系统调用.jvm需要抽象出一套通用的内存模型来想上层提供统一的内存模型,同时也兼容底层的不同平台的差异. - 指令重排序
编译器和解释器实现代码向机器码之间的转变.这个转变过程中会对代码顺序进行调整优化,从而在不影响代码结果的情况下,更适合cpu运行.在并发情况下,可能结果可能会出现错误.这个时候需要一种机制让编译器感知到禁止重排序.
内存模型
工作内存和主内存
jvm的内存模型规定了各种变量的访问规则,即将变量从内存中取出来以及将变量写入到内存中的实现.这里的变量是不包括局部变量.
内存模型分为 线程 --> 工作内存 --> 主内存.
内存之间的交互
jvm抽象出了一些列的原语来在各种内存之间移动数据
lock : 将一个变量标记为线程独占状态
unlock : 将线程独占状态释放
read : 主内存 -->工作内存
load : 工作内存 --> 工作内存副本
use: 工作内存 --> 执行器
assign: 执行器 --> 工作内存
store: 工作内存 --> 主内存
write: 主内存 --> 变量
各种原语的注意:
read,load 以及 write,unload 这些原语不能单独出现,但是可以不连续出现.
assign过的数据必须同步到主内存中,如果没有assign过的数据,不能同步到主内存
在适用use 之前 必须执行 read+load.
一个变量只能被统一个线程lock,lock多少次,就必须unlock多少次
对于lock一个变量,需要先进行load和read
对于unlock一个变量,需要先进unload,store
volatile变量
volatile修饰变量能够1.禁止重排序
,2.内存可见性
.
- 禁止重排序
重排序保证 volatie修饰的变量的赋值前后之间不会发生从排序.
只要volatie变量赋值之前完成的一些列指令.不会被重排序到赋值之后被执行. - 内存可见性
表示当前系统中的变量 a修改后,b线程立刻可见.实现的方式是让:读入的时候:use + load + write
依次执行.写出的时候:assign+store+read
依次执行 - voaltile读
当读取volatile的时候,会清空工作内存,然后重新从内存中读.这种情况让ReentrantLock能解决可见性的原因.
原子性,可见性,顺序性
-
原子性
如果我们希望在一个更大范围内保证原子操作,jmm中提供了lock
+unlock
原语帮助实现.上层适用了Synchronized
关键字. -
可见性
在一个线程中完成修改,在其他线程都立刻可见.
volatile可见性是指对volatile修改的变量,在其他线程线程都可见.原因是use + load + read
这三条原语需要连续执行.
volatile变量的第二种情况是所有对volatile变量的修改编译器都会在其中添加一个lock add$1的指令,将当前线程中所有的变量都写入到内存中.这也是ReentrantLock能够成为锁而实现可见性的原因.
lock同步:这里是因为做了一个约定当unlock的时候,会将所有的工作内存的数据写到主内存中.
final可见性:当创建完成的时候,如果没有发送this逃逸,也是对其他线程可见的. -
有序性
从单线程来看,内部的指令是有序的.从多线程来看,多个指令是无序的.其中这种无序主要是指令重排序
和工作内存和主内存同步延迟可见
.
java中提供了 Synchronized 和volatile来帮助实现同步.
happen-before原则
在多线程中,通过happend判定是否存在竞争条件.是否是线程安全的.
1.程序执行顺序,有依赖的偏序关系
2.同一个锁的 unlock发生于 lock之后
3.线程的start()一定发生在代码执行指令之前
4.线程的结束 一定发生在后续方法之前
5.线程interupt()方法一定发生在检查到中断之前.
6.volatile修改的变量 一定在后一个读操作之前.
7. 类的所有生命周期的操作都在finalize()方法之前执行.
8. 传递性 如果 A hp B, B hp C. 那么 A hp C
java中的线程
线程实现
java中的线程是Thread对象调用start()方法以后,还没有运行结束时候的状态,就是一个线程.java中的线程借助了操作系统的能力,将jvm线程绑定到操作系统的线程上来实现线程的调度,销毁.
线程的模型:
1:1 -> 用户线程和内核线程1:1.这种优势是即使一个线程被阻塞了,其他的线程也会被被正常调用.缺点是:对于线程的操作都需要系统调用的特权切换.然后是一个线程消耗一个内核线程,导致系统线程数量有限.
1:n -> 一个内核线程对应 n个用户线程.优点:创建,销毁线程不需要绑定内核线程,也不需要特权切换.缺点是:没有内核的支持,对于调度和cpu的映射无法实现.功能不够抢单.
n:m -> n个内核线程对应m个用户线程.这种需要操作系统的支持.
大多数jvm都使用了 1:1
的方式实现线程.将用户线程和操作系统内核线程通过native方法绑定.
线程调度
1.线程调度是由操作系统的调度策略来实现的.一般是强制调度.即
线程通过yield,sleep放弃当前获取到的cpu.但是无法主动获取cpu,只能等待操作系统调用.
2.调度优先级
可以通过线程优先级确定,调度的时间多少.但是不同平台的优先级设计会有一些差异.
同时也无法通过优先级判定一个ready状态的线程多久能够得到调度.
状态转变
线程状态
new --> 刚创建,还没有start()完成
Runnable --> Ready + Running
Waiting -> Object.wait() + LockSupport.park() + Thread.join()
TimeWaiting --> Object.wait(time) +Thread.join(time) + LockSupport.parkUnitl() + LockSupport.parkNacos() + Thread.sleep();
Block --> synchronized()
Terminal -> 当前线程已经结束
线程安全
线程安全的定义:
一个数据结构,我们不需要额外的同步手段,也不依赖于特殊的调度策略.在多线程环境下,都能够获取到期望的结构.这就是线程安全的.
如何保证线程安全
互斥同步
synchronized关键字
ReentrantLock
两者区别:
ReentrantLock有可中断特性 + 实现公平锁 + 绑定多个condition.
非阻塞同步
CAS:
Test-and-set等元语
无同步方案
如果一个对象中是无同步数据,那么就是线程安全的.不需要任何同步方案.
1.可冲入代码
2.线程本地变量
锁优化
自旋锁 和自适应
当试图获取锁的时候,锁被其他线程占有.这个时候可以自旋一定次数.如果在自旋一定次数任然没有获取锁,这个时候就挂起.
自旋的次数可以进行配置.同时系统也会自适应,如果之前有自旋成功过,那么就自旋,如果没有成功过,就直接不自旋.
自旋锁适合占用时间比较短的任务.这样通过自旋避免挂起,如果大多数都无法自旋成功,那么就会浪费很多时间.
锁消除
有些代码块就是无共享状态的逻辑.这个时候编译阶段可以将这些锁给去除.
锁粗化
正常情况 将同步的代码块越短越好.但是如果通过将锁的粒度变大而减少加锁次数的时候,这个时候可以进行锁粗化.例如:循环中加锁.
轻量级锁
在对象头部会有markword,记录了hashcode,分代年龄,偏向模式,锁类型.
加锁过程 --> 当获取锁的时候,发现当前不存在竞争,这个时候,首先创建一个LockRecord,添加到栈帧中,然后适用这个LockRecord的指针cas到markword中,如果成功,就表示获取到了锁,然后将后两位状态为设置为00.表示获取锁成功.如果cas失败,会判断一下是否是当前的线程已经获取到了锁.
释放锁 --> 适用cas进行替换,如果能替换成功就正常释放锁.如果cas失败,表示有其他线程已经获取到了锁,这个时候只去唤醒等待线程.
加锁失败 --> 升级为重量级锁,将状态给为10,重量级锁,然后创建一个ObjectMonitor,然后使用cas将markword指向重量级锁的引用.
偏向锁
当多线程场景下不存竞争,或者线程是依次加锁的时候.这个时候为了减少加锁的资源消耗,而提供了偏向锁.
锁偏向:将当前ThreadId适用cas设置到markword中的hashCode位置上.然后去执行.
重偏向过程:当一个线程进入的时候,查看当前的ThreadId已经有了线程Id,查看对应的Thread的状态是还在执行过程中,如果已经执行结束,适用cas实现重偏向.
锁升级:如果一个线程来的时候,判断当前线程占有偏向锁的线程还在运行,这个时候将锁升级为轻量级锁.
计算hashcode:当获取HashCode的时候,这个时候,锁会被审计为重量级锁.同时当前对象不可以再进行偏向.
ReentrantLock的实现.
//todo
常见多线程问题应该如何实现
三个线程打印0,奇数,偶数,依次执行,最终打印出2n个数出来.
class ZeroEvenOdd {
/**
* 给定一个数n,一次打印 0,1,0,2,0,3,0,4,0,5
*/
private int n;
private Object lock = new Object();
// count 0 ,1,2,3 == > 0,2 打印2
private volatile int count = 0;
public ZeroEvenOdd(int n) {
this.n = n;
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void zero(IntConsumer printNumber) throws InterruptedException {
synchronized (lock) {
for (int i = 0; i < n; i++) {
while (count != 0 && count != 2) {
lock.wait();
}
printNumber.accept(0);
count = count + 1;
lock.notifyAll();
}
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
synchronized (lock) {
for (int i = 2; i <= n; i = i + 2) {
while (count != 3) {
lock.wait();
}
printNumber.accept(i);
count = 0;
lock.notifyAll();
}
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
synchronized (lock) {
for (int i = 1; i <= n; i += 2) {
while (count != 1) {
lock.wait();
}
printNumber.accept(i);
count = 2;
lock.notifyAll();
}
}
}
}
这个题需要考虑的是应为有三个程序同时协作打印一个序列出来,需要思考的内容
1.一个时间点只有一个线程能够打印数值,所以需要一把锁.
2.然后是有三种情况,分别是打印0,奇数,偶数.需要一个数值来分别状态,所以添加一个count.当count为哪个值的时候,就表示当前的线程可以执行了.0,2 --> 0. 1 --> 奇数 . 3 --> 偶数
3.因为总共打印的元素个数是2n,所以每个线程的数值是固定的.适用for循环内部统计打印的值是多少,然后适用count + lock来协调.
- 这里适用了synchronized关键字.在每个线程执行完毕的时候,notify的时候,会唤醒所有的线程.可以适用Lock来帮助试下,创建三个Condition.
class ZeroEvenOdd {
private ReentrantLock lock = new ReentrantLock();
private Condition zeroCondition = lock.newCondition();
private Condition oddCondition = lock.newCondition();
private Condition evenCondition = lock.newCondition();
private volatile int count = 0;
private int n;
public ZeroEvenOdd(int n) {
this.n = n;
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void zero(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
for (int i = 0; i < n; i++) {
while (count != 0 && count != 2) {
zeroCondition.await();
}
printNumber.accept(0);
count++;
if (count == 1) {
oddCondition.signalAll();
} else {
evenCondition.signalAll();
}
}
} finally {
lock.unlock();
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
for (int i = 2; i <= n; i += 2) {
while (count != 3) {
evenCondition.await();
}
printNumber.accept(i);
count = 0;
zeroCondition.signalAll();
}
} finally {
lock.unlock();
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
for (int i = 1; i <= n; i += 2) {
while (count != 1) {
oddCondition.await();
}
printNumber.accept(i);
count = 2;
zeroCondition.signalAll();
}
} finally {
lock.unlock();
}
}
}
四个线程,打印n个数,其中只能被三整除的打印fizz,只能被5整除的打印buzz,同时被3,5整除的打印 fizzbuzz,其他打印数值
class FizzBuzz {
private int n;
public FizzBuzz(int n) {
this.n = n;
}
private volatile int count = 1;
private Object obj = new Object();
// printFizz.run() outputs "fizz".
public void fizz(Runnable printFizz) throws InterruptedException {
synchronized (obj) {
while (count <= n) {
while ((count % 3 != 0 || count % 5 == 0) && count <= n) {
obj.wait();
}
if (count <= n) {
printFizz.run();
count++;
obj.notifyAll();
}
}
}
}
// printBuzz.run() outputs "buzz".
public void buzz(Runnable printBuzz) throws InterruptedException {
synchronized (obj) {
while (count <= n) {
while ((count % 3 == 0 || count % 5 != 0) && count <= n) {
obj.wait();
}
if (count <= n) {
printBuzz.run();
count++;
obj.notifyAll();
}
}
}
}
// printFizzBuzz.run() outputs "fizzbuzz".
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
synchronized (obj) {
while (count <= n) {
while ((count % 15 != 0) && count <= n) {
obj.wait();
}
if (count <= n) {
printFizzBuzz.run();
count++;
obj.notifyAll();
}
}
}
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void number(IntConsumer printNumber) throws InterruptedException {
synchronized (obj) {
while (count <= n) {
//这里有一个条件是全局的
while ((count % 3 == 0 || count % 5 == 0) && count <= n) {
obj.wait();
}
if (count <= n) {
printNumber.accept(count);
count++;
obj.notifyAll();
}
}
}
}
}
由于不太方便计算每个线程打印的数值的数量.所以需要一个全局的数表示第当前打印的进度.
1.多个进程打印一个序列,所以需要一把锁来做同步,保证只有一个线程打印.
2. 每个线程满足的条件是 count<=n .当前线程的结束进度.
3. 在每个线程等待满足条件的时候,需要添加一个额外的全局的条件count <= n
如果不添加的话,可能会导致当前进程唤醒的时候,count已经不满足全局条件了.
4.后面获取锁以后,也需要判断全局条件.
哲学家吃饭问题
防止死锁的方式是打破循环等待.奇数先左后右
,偶数先右后左
,这样能有效避免循环等待,打破死锁.
class DiningPhilosophers {
private Object[] objs = new Object[5];
/**
* 0,1,2,3,4,
* 五个哲学家.其中有五把餐叉,每个哲学家随机的拿起左右边的.保证其中不会死锁.
* 其中
* 偶数 --> 先 取右侧.后取左侧
* 奇数 --> 先 取左侧.后取右侧
*/
public DiningPhilosophers() {
for (int i = 0; i < objs.length; i++) {
objs[i] = new Object();
}
}
/**
* 编号是
*
* @param philosopher
* @param pickLeftFork
* @param pickRightFork
* @param eat
* @param putLeftFork
* @param putRightFork
* @throws InterruptedException
*/
// call the run() method of any runnable to execute its code
public void wantsToEat(int philosopher,
Runnable pickLeftFork,
Runnable pickRightFork,
Runnable eat,
Runnable putLeftFork,
Runnable putRightFork) throws InterruptedException {
//这里的数组的左右计算有一些差别.就是 rank(左) == rank rank(右) == (rank +1) % 5
int rightRank = (philosopher + 1) % 5;
int leftRank = philosopher;
if (philosopher % 2 == 0) {
//偶数 右 左
synchronized (objs[rightRank]) {
synchronized (objs[leftRank]) {
pickLeftFork.run();
pickRightFork.run();
eat.run();
putLeftFork.run();
putRightFork.run();
}
}
} else {
//奇数 右 左
synchronized (objs[leftRank]) {
synchronized (objs[rightRank]) {
pickLeftFork.run();
pickRightFork.run();
eat.run();
putLeftFork.run();
putRightFork.run();
}
}
}
}
}
总结
在实现多线程的题目的时候思考:
1.其中多线程的锁的个数.
2.确定每个线程执行的条件,然后适用 while(condition){obj.wait}等待.
3.确定当前 线程执行的单调性,这些单调性的维护是否是全局共享的,如果是全局共享的,需要在第二步中的等待条件中添加上判断.如果是局部控制,不需要.
4.其他的和大于普通的代码相同.
其中关键点:
1.并发的问题
2.单调性问题,线程能够执行结束.