06_并发
2024-05-23 21:54:04 0 举报
Java并发原理及源码深度剖析,volatile,synchronize,java内存模型,操作系统指令,硬件
作者其他创作
大纲/内容
read
目的是为了其他线程继续往内存缓冲里写日志,刷磁盘不占用锁
a = 4
释放锁
主内存a = 3
write
修改monitor + 1
Thread 1
交换两块缓冲区,为了同步内存数据到磁盘做准备public void setReadyToSync() { LinkedList<EditLog> tmp = currentBuffer; currentBuffer = syncBuffer; syncBuffer = tmp; }
12 write
必须得对同一个对象实例进行加锁、wait、notify,这样的话,他们其实操作的才是通一个对象实例里的monitor相关的计数器、wait set
工作内存
2 执行后面的load指令
wait set
a = 3
N
解决多线程修改共享资源的可见性问题
很幸运的再次拿到当前对象的锁
处理业务
当前doubleBuffer中的日志大小是否大于512k
未加volatile字段
将内存缓冲中的数据刷入磁盘文件中 在这里尝试允许某一个线程一次性将内存缓冲中的数据刷入磁盘文件中 相当于实现一个批量将内存缓冲数据刷磁盘的过程
CPU1读取和写入都是针对自己的多级缓存中
更新刷磁盘标志位isSyncRunning=false
我们期望的是a = 5 ,但实际上a 有可能还是4.只要在thread1的第7步之前,thread2进行了第6步,就会发生原子性问题。因为thread1的assign操作执行后,虽然将thread2的a=3过期,但是thread2已经在cpu中拿a=3 进行 ++操作了,最后将a=4 写入主存。造成原子性问题。
L1缓存
可见性(lock前缀指令 + MESI缓存一致性协议):
将edits log写到内存缓冲里去public void write(EditLog log) { currentBuffer.add(log); }
a = 3 -> 4
有序性(内存屏障:禁止指令重排序):
线程2
y
block
editlogs双缓冲并发机制
3
start
CPU1先load数据a
将edits log写入内存缓冲中,不是直接刷入磁盘文件DoubleBuffer.write
load
修改thread的interrupt状态
这里是卡住不让日志刷磁盘
1 read
synchronized修饰一个普通的方法或this,那么就是对当前这个对象实例在加锁synchronized(myObject) {}synchronized(this) {}
当前是否在将内存缓冲刷入磁盘中isSyncRunning
a = 3修改为4
分段加锁
构建EditLog
CPU1
未加volatile关键字,Thread 1 会在自己的工作内存中读取和修改数据,并不会立刻刷会主存
assign
补充:加了synchronized关键字后,thread修改数据后,会强制刷主存,读取数据的时候会强制读主存。所以加了synchronized的代码块中,不会又可见性和原子性问题。
线程3
8 store
计算任务 a = 3;a++
Y
store + write
并发写日志
volatile a =1
3 use
内存双缓冲区DoubleBuffer
a = a + 1
synchronized锁两种东西:
9 write
5 load
load a
monitorenter
Thread 2
上锁
原因: 现代CPU为了提高运算速度,会将数据load到cpu自己的高速缓存中进行运算和处理,在一段时间内是不会刷回主存的。
synchronized一个静态方法或者对象的class,那么就是对当前这个对象实例在加锁synchronized(MyObject.class) {}public synchronized static void (){}
monitor0 --> 1 --> 0
设置当前正在同步到磁盘的标志位 isSyncRunning = true;
上锁 写日志到内存
while(true)
设置正在同步标志位isAutoSyncScheduled = false;唤醒等待写入log的线程notifyAll();
n
加了volatile关键字,线程1 的行为会做如下改变:1:assign后立刻store+write,写入主内存2:将所有线程工作内存中的 a 过期掉。线程2发现自己工作内存中的a已经过期,会去主内存中重新read+load一个新的a,此时a已经是被线程1修改后的最新指。经过以上两步,就可以让volatile保证多线程对同一个变量修改的时候的可见性。底层是遵循了MESI协议。但是,但是,但是,但是,依然无法保证原子性问题
synchronized关键字,在底层编译后的jvm指令中,会有monitorenter和monitorexit两个指令。每个对象都有一个关联的monitor,比如一个对象实例就有一个monitor,一个类的Class对象也有一个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁
a++ = 4
1
wait()
重入锁例子(monitor = 2)synchronized(myObject) { // 一大堆的代码 A synchronized(myObject) { // 一大堆的代码 B }}
DoubleBuffer 内存双缓冲
将syncBuffer缓冲区中的数据刷入磁盘中 public void flush() { for(EditLog log : syncBuffer) { System.out.println(\"将edit log写入磁盘文件中:\" + log); // 正常来说,就是用文件输出流将数据写入磁盘文件中 } syncBuffer.clear(); }
while卡住判断是否正在同步日志isAutoSyncScheduled==true?
多线程编程三大问题:原子性、可见性、有序性
当前是否有线程在等待刷新下一批edits log到磁盘里去isWaitSync
被唤醒后
sysnchronized
store
11 store
设置同步标志位isAutoSyncScheduled = true
2 load
7 assign
CPU2
myObject
java内存模型(请脑补)
加volatile字段,依然存在原子性问题
一种:某个实例对象来加锁,另一种:对这个类进行加锁。对类加锁,也是在针对一个对象实例进行加锁,其实就是对那个类的Class对象进行加锁
交换双缓冲是很快的过程
interrupt
开始wait()
StoreLoad 内存屏障
将wait在这个对象上的所有对象唤醒,重新竞争锁
4 read
执行业务逻辑
notifyAll()
拿到全局唯一txId放入ThreadLocal
在同步到磁盘中的最大的一个txidsyncMaxTxid
edits log日志的核心组件
monitor ==0?
每个线程自己本地的txid副本ThreadLocal localTxid
a = 3过期掉
是否已经有线程在刷磁盘中?isSyncRunning
synchronize原子性
CPU内存模型
10 assign
线程1
volatile可见性、有序性
承载线程写入edits logLinkedList<EditLog> currentBuffer
获取当前这个幸运线程的最大txIdlocalTxid
这里是卡住不让写日志到内存buffer,直到交换缓冲
加volatile字段
两块缓冲,一块在刷磁盘的时候,另一块可以允许其他线程并发写日志。每次交换后,syncBuffer中的数据就让某个幸运线程去刷磁盘
use
修改monitor - 1
获取锁
wait与sleep的区别:前者释放锁,后者不释放锁wait(),必须是有人notify唤醒他wait(timeout),阻塞一段时间,然后自己唤醒,继续争抢锁wait与notify,必须在synchronized代码块中使用,因为必须是拥有monitor lock的线程才可以执行wait与notify操作因此wait与notify,必须与synchornized一起,对同一个对象进行使用,这样他们对应的monitor才是一样的notify()与notifyall():前者就唤醒block状态的一个线程,后者唤醒block状态的所有线程
thread
更新刷磁盘的最大txId
java 内存模型
将数据同步到磁盘中去的一块缓冲LinkedList<EditLog> syncBuffer
volatile解决 可见性 和 有序性对volatile修饰的变量,执行写操作,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别的CPU修改。如果发现别的CPU修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了LoadLoad屏障:Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2后所有装载指令,他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的StoreStore屏障:Store1;StoreStore;Store2,确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令LoadStore屏障:Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令StoreLoad屏障:Store1;StoreLoad;Load2,确保Store1指令的数据一定刷回主存,对其他cpu可见,先于Load2以及后续指令的数据装载对于volatile修改变量的读写操作,都会加入内存屏障每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排
刷磁盘flush
其实本质上是线程的一个状态标志位,我们通过这个线程状态来改变执行中线程的行为。
线程4
wait与notify
唤醒wait()刷磁盘的线程
主存 a = 2
isInterrupted
是否<=最后刷磁盘的txIdsyncMaxTxid
交换两块缓冲区 editLogBuffer.setReadyToSync();
4
是否已经有线程在排队刷磁盘?isWaitSync
上锁成功
6 use
break;
CPU2接着load数据a,发现a依然=3
0 条评论
下一页