- 1,小例子开场
- 2,JMM数字原子操作
- 3,JMM缓存不一致问题
- 4,深入理解java内存模型与volatile关键字
- 5,同步器核心原理剖析(自旋+LockSupport+CAS)
- 6,HashMap&ConcurrentHashMap底层结构
- 7,手写一个红黑树/双向链表的数据结构
- 8,手写一个hashmap
1,小例子开场
两个线程,一个等待数据,一个准备数据,如果不运行程序的话,结果猜想打印结果估计是
等待数据
准备数据开始
准备数据完成
得到了数据
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("等待数据。。。");
while (!isEnd){}
System.out.println("得到了数据");
}
});
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("准备数据开始");
isEnd = true;
System.out.println("准备数据完成");
}
});
}
然后真实结果是
等待数据
准备数据开始
准备数据完成
这就是Volatile关键字的用途了!
2,JMM数字原子操作
- read读取:从主内存读取数据
- load载入:将主内存读取到的数据写入工作内存
- use使用:从工作内存读取数据来计算
- assign赋值:将计算好的值重新赋值到工作内存中
- store存储:将工作内存数据写入主内存
- write写入:将store过去的变量值赋值给主内存中的变量
- lock锁定:将主内存变量加锁,标识为线程独占状态
- unlock解锁:将主内存变量解锁,解锁后其他线程可以锁定该变量
Java线程内存模型
3,JMM缓存不一致问题
(1)总线加锁(性能太低)
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他cpu没法去读或写这个数据,知道这个cpu使用完数据释放锁之后其他cpu才能读取该数据。
(2)MESI缓存一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
Volatile关键字的底层就是实现了缓存一致性,提供了一个总线(MESI)
4,深入理解java内存模型与volatile关键字
并发编程三大特性:可见性,原子性,有序性。
volatile保证可见性和有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制!
现在有这样一段代码,十个线程,每个线程给num加1000,理想结果应该是num=10000,但是下面代码的运行结果往往小于等于1w,为什么?
public class Test {
private static volatile Integer num = 0;
private static void inscrease(){
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for(int i=0;i<threads.length;i++){
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int j=0;j<1000;j++){
inscrease();
}
}
});
threads[i].start();
}
for(Thread t: threads){
t.join();
}
System.out.println(num);
}
}
这里列出模型图,volatile提供了一个总线机制。
已知:线程1对num加1之后,对主内存加锁,然后把num加1的结果写入主内存,然后同时运行的其他线程通过总线机制实时的监听到了新数据。
造成数据小于1w的原因
线程2通过总线感知到了主内存的新数据之后,会立即废掉自己工作内存中的数据(失效)但是此时线程1的锁还没有释放,线程2无法从主内存读取新数据并更新到自己的工作内存中,而且存在一定的时间差,此时很多线程都在疯狂的加一加一的运行,但是却又一直监听新数据并失效老数据,因此,这些线程做的都是无用功,并没有真正的加上去。
5,同步器核心原理剖析(自旋+LockSupport+CAS)
(1)手写高synchronize同步器锁
现在有一个场景,就是很经典的减库存高并发的场景,以下是伪代码
public void reduceShop(){
//查询库存
//如果库存数量>0,库存减1
//如果库存数量<0,减库存失败
}
假如数据库的库存数量是5,如果是高并发的情况下(30个线程同时减库存),没有加synchronize同步锁,结果将如何?
结果是,下单成功的数量远远大于5,为什么?
看上图右侧,多个线程就会有多个线程栈,每个线程栈都有自己的局部变量,分析一下整个过程,首先线程1从数据库取出来库存数量5,然后存到自己的局部变量表里,相同时刻,另一个线程也从数据库里取出库存数量5,也存到了自己的局部变量里,然后各自减各自的局部变量,然后结果都是4,最后两个线程都把4更新到了数据库里了,结果就是,两个线程都下单成功了,而实际上库存数量只减了一个。
这里手写的一个公平锁
package com.dayouzc.e2eapp.ebusiness.controller.rest.ff;
import sun.misc.Unsafe;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.locks.LockSupport;
public class MyLock {
//当前加锁状态,记录加锁的次数
private volatile int state = 0;
//当前持有锁的线程
private Thread lockHolder;
//提供一个队列来保存线程的引用,并发线程进入队列,当上一个线程释放锁后,唤醒下一个线程去获取锁,就是从队列中获取下一个(有序)
private ConcurrentLinkedDeque<Thread> queue = new ConcurrentLinkedDeque<>();
//=================================== 原子操作,Unsafe类是java直接操作硬件的方法实现原子操作,native本地方法(底层)============================
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static long stateOffset = 0;
static {
try {
stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/**
* 原子操作
* @param oldValue 线程工作内存当中的值
* @param newValue 要替换的新值
* @return
*/
public final boolean compareAndSwapState(int oldValue,int newValue){
return unsafe.compareAndSwapInt(this,stateOffset,oldValue,newValue);
}
//======================================================================================================================================
public int getState() {return state;}
public void setState(int state) {this.state = state;}
public Thread getLockHolder() {return lockHolder;}
public void setLockHolder(Thread lockHolder) {this.lockHolder = lockHolder;}
/**
* 尝试获取锁
*/
public boolean tryAquire(){
Thread t = Thread.currentThread();
int state = getState();
if(state == 0){
//当队列中没有等待获取锁的线程时,以及当前线程是等待锁队列的头部线程时,可以直接获取锁,否则就去排队等待获取锁(公平锁)
if((queue.size()==0 || t == queue.peek()) && compareAndSwapState(0,1)){
setLockHolder(t);
return true;
}else{
return false;
}
}else{
return false;
}
}
/**
* 加锁
*/
public void lock(){
//1,获取锁 CAS-compareAndSwap
if(tryAquire()){
return;
}
//没有获取到锁的当前线程
Thread current = Thread.currentThread();
//把没有获取到锁的添加到队列中
queue.add(current);
//2,停留在当前方法
for(;;){ //通过自旋进行停留
//尝试获取锁
if(queue.peek() == current && tryAquire()){
System.out.println("获取到锁的线程:"+current.getName());
//将此线程从队列中的对头移除,下一个补上
queue.poll();
return;
}
//如果线程一直空转的话会浪费性能,所以这里让线程阻塞,Java提供了LockSupport类用于阻塞线程
//阻塞线程(等待持有锁的线程unpark()进行唤醒,才可以自旋获取锁)
LockSupport.park(current);
}
}
/**
* 释放锁/解锁
*/
public void unLock(){
Thread current = Thread.currentThread();
if(current != lockHolder){
throw new RuntimeException("当前线程不是持有锁线程,无法释放锁");
}
//状态值
int state = getState();//1
if(compareAndSwapState(state,0)){
System.out.println("线程" + current.getName() + "释放锁成功");
//把当前持有锁的线程置空
setLockHolder(null);
//取出队列中排第一个的等待锁的线程
Thread headThread = queue.peek();
if(headThread != null){
//唤醒该线程
LockSupport.unpark(headThread);
}
}
}
}
使用方式
MyLock lock = new MyLock();
lock.lock();
//减库存操作
lock.unLock();
6,HashMap&ConcurrentHashMap底层结构
HashMap = 数组 + 连表 + 红黑树
至于红黑树的原理和算法,见文章:https://www.cnblogs.com/skywang12345/p/3245399.html
jdk7的HashMap,高并发情况下,在扩容时会发生死锁,什么原因导致的?
jdk8的HashMap就不会发生死锁,又是做了什么改进规避了这个问题?
concurrentHashMap不会发生死锁,原理怎么实现的?