并发编程
总线锁机制
某个cpu要读取一个数据的时候,就会对总线加锁,只有当这个cpu释放锁之后其他CPU才可以获取数据
MESI协议
MESI,当我们修改本地内存的数据时,就会强制刷新到主内存,然后发布一个消息,其他CPU通过总线嗅探到修改的消息,就会将本地的数据标记为过期,然后下一次就会重新从内存中获取数据。在底层具体是通过lock前缀指令
Java内存模型
线程有工作内存和主内存,
在内存模型下多线程并发运行的问题
线程是基于自己的内存读取数据,就会一直读取到旧值,导致数据错误
可见性、原子性、有序性
可见性
一个线程在修改数据,另一个线程读不到新的值
原子性
对于i++,只要是多个线程并发运行,都是不保证原子性的,第一个线程i++,执行完就等于1,第二个线程i++,就等于2
有序性
对于代码,编译器和指令器为了提高代码执行的效率,就会将指令进行重排序
Volatile的作用
volatile可以保证可见性和有序性,不能保证原子性
JMM
volatile保证在assign之后,会将结果assing+write带主内存中,然后还会让其他线程的工作内存中的flag变量会过期,然后其他线程就会冲主内存中重新读取数据
为什么不能保证原子性?
两个线程同时都加载了变量,同时执行i++,然后写入结果,及时你过期线程2的i的值,计算已经完成了,即使过期工作内存的值,也不会重新读取值进行计算,就不会保证原子性
happens before保证有序性
- 程序次序规则:一个线程的每个操作,happens-befor与该线程的后续操作
- 监视器锁规则:释放锁happens-before与加锁
- volatile变量规则:对volatile的写happens-befgor与volatile读,是通过内存屏障来实现
底层原理
可见性
对Volatile变量执行写操作的时候,JVM就会给CPU发送lock前缀指令,Lock前缀指令会引起处理器缓存写入到内存,一个处理器的缓存写回到主内存的时候就会导致其他处理器的缓存失效
有序性
Volatile会通过内存屏障实现
-
LoadLoad屏障确保Load1的数组装载先与Load2
-
StoreStore屏障,确保Store1和Store2的代码是不能进行重排序的,Store1的数据一定先与Store2以及后续指令
-
StoreLoad屏障,确保Store1和Load2的代码是不能进行重排序的,确保Store1指令的数据一定先与Load2大
-
LoadStore
在每个Volatile写之前,加StoreStore屏障,禁止在他之前的普通写和Volatile写和他重排序,在每个Volatile写的后面会加一个StoreLoad的屏障
在每一个Volatile读之前,加入LoadLoad屏障,禁止在他之前的普通读和Volatile读重排序,在每一个Volatile后会加一个LoadStore屏障
Double Check
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton doubleCheckSingleton;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getInstance() {
if (doubleCheckSingleton == null) {
synchronized (DoubleCheckSingleton.class) {
if (doubleCheckSingleton == null) {
doubleCheckSingleton = new DoubleCheckSingleton();
}
}
}
return doubleCheckSingleton;
}
}
为什么要加volatile,禁止指令重排序
synchronized
使用synchronized关键字,在jvm编译之后,会有moniotrenter和monitorexit指令,每个对象都关联一个monitor,在monitor中有一个计数器,加锁的时候,先判断monitor的值是不是0,如果是0就进行加锁,如果不是0判断加锁的线程是不是当前线程,如果是就进行重入加锁,把计数器的值+1,释放锁的时候就会把计数器的值设置为0,然后其他线程就可以来争抢锁
eureka自我保护
eureka server自己的网络问题,导致大量的服务实例没有办法发送心跳,他就会认为是因为自己的网络问题,然后就进入自我保护机制,就不会摘除服务实例。在eureka设置了一个比例是0.85,如果有超过25%的心跳,没有及时更新,就会进入自我保护机制,避免摘除过多的服务实例,然后之后如果发现有超过85%的服务实例恢复发送心跳了,就会退出自我保护机制,就会继续检查服务实例的心跳。
eureka自我保护机制的实现
统计每分钟发送心跳的数量,和预期的心跳数量进行比较
wait和notify的底层原理
每一个monitor中都含有一个wait set,调用wait方法的时候就会将当前线程释放掉锁然后加入到wait set中,然后别的线程就可以来获取锁。notify就会唤醒wait set中的线程,然后来争抢锁
wait和sleep
wait会释放锁,sleep是不会释放锁,wait和notify必须在sychronized代码块中使用,wait如果没有设置等待时间就会一直等待,sleep等待时间到了就会继续执行
synchronized对可见性的保证
synchronized对内存的语义,会强制从主内存中读取,也会强制把工作内存刷入主存
Atomic原子类
CAS原理
AtomicLong
、AtomicBoolean
、AtomicReference
、LongAddr
无锁化和乐观锁的思想
首先回去回去值是多少,然后判断是否和当前线程的值是否一样,如果一样就执行操作,不一样就说明有其他线程已经修改过值了,就会再次查询值,然后继续判断,如果是就说明没有人改过。CAS,Compare And Set
Atomic
原子类的核心原理就是CAS,无锁,乐观锁
Unsafe类
Unsafe
是JDK底层的类,是不允许实例化和使用的,封装了底层的一些操作
源码
value
使用volatile
来保证可见性
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
CAS
cpu会通过底层的CPU的指令实现的,CPU会通过轻量级的小块内存锁机制来实现
问题
- ABA问题
- 无线循环问题
- 多变量原子问题:AtomicReference解决多变量问题
LongAdder
通过分段锁机制,来解决自旋问题。分段CAS来优化性能,分段迁移,某一个线程如果对一个Cell更新的时候,发现出现了很难更新他的值,出现了多次CAS的问题,会自动迁移段,尝试去更新别的Cell的值,不会盲目等待,在获取值的时候,会将base和cell的值加起来
ReentrantLock
AQS
AbstractQueuedSynchronizer
抽象队列同步器。AQS有一个state=0
,表示当前加锁线程的数量,0就表示没有线程加锁,有一个当前加锁的线程currentThread
,然后加加锁,然后将当前加锁的线程设置为自己,然后如果其他线程过来加锁,发现state的值不是0,说明已经加锁了,然后就会判断加锁的线程是不是自己,如果是就将state值+1,进行重入加锁,不是的话就加入等待队列,然后线程1释放锁之后,就会唤醒队列中的线程去争抢锁
源码
public ReentrantLock() {
sync = new NonfairSync();
}
首先构造函数,创建了一个Sync
默认是采用非公平锁来实现的NonfairSync
,ReentrantLock
在进行加锁的时候是基于Sync
来实现的lock操作
abstract static class Sync extends AbstractQueuedSynchronizer
Sync是一个抽象静态内部类是AQS的子类,抽象队列同步器,是Java并发包,锁、同步器底层的基础组件
AQS中有两个核心组件Node
和state
NonfairSync是Sync
的子类,覆盖重写了几个方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
底层是基于NonfiarSync
加锁,compareAndSetState(0, 1)
,在AQS中有一个核心的state
变量,代表了锁的状态,判断是否是0,判断是否有人加锁,如果为0表示没人加锁,就把他设置为1,底层是基于unsafe
的CAS来实现
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
CAS可以保证无锁化对数据的修改,在原子操作中,如果值是我们期望值,就是符合要求,没有人修改过,就可以设置为其他值,如果加锁成功就设置自己为当前加锁的线程,设置是当前的线程
setExclusiveOwnerThread(Thread.currentThread());
通过state变量进行可重入加锁
此时state不是0就会进入acquire(1)
方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(1)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
就会调用nonfairTryAcquire(1)
,先获取到当前线程,
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 这一段代码是用来判断,是否在执行之前,有线程释放了锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果加锁的线程和 之前加锁的线程是一样的,就进行冲入加锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}