引言:
在前几篇博文中我详细介绍了HashMap的底层实现原理,后来我接连写了三天JVM和GC的一些知识,那些知识偏向于理论。今天换点口味,和大家一起研究学习一下ConcurrentHashMap的底层实现,因为jdk1.8在HashMap和concurrentHashMap和以往都发生了变化。知识一直都是连续的,如果我觉得光说ConcurrentHashMap的源码,很多小伙伴会云里雾里,所以为了让所有人,甚至是新手都能通透理解,限于篇幅和时间,我打算把ConcurrentHashMap分为三部分来写,第一篇作为基础,第二篇为认识,第三篇为熟知。今天第一篇我主要介绍一下Java内存模型,volatile关键字和CAS算法,如果理解这三点,对后面的源码理解会有极大的帮助。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290
技术点:
1、悲观锁与乐观锁:
悲观锁是指如果一个线程占用了一个锁,而导致其他所有需要这个锁的线程进入等待,一直到该锁被释放,换句话说就是这个锁被独占,比如说典型的就是synchronized;乐观锁是指操作并不加锁,而是抱着尝试的态度去执行某项操作,如果操作失败或者操作冲突,那么就进入重试,一直到执行成功为止。
2、原子性,指令有序性和线程可见性:
这三个性质在多线程编程中是核心的问题。原子性和事务的原子性一样,对于一个操作或者多个操作,要么都执行,要么都不执行。指令有序性是指,在我们编写的代码中,上下两个互不关联的语句不会被指令重排序。指令重排序是指处理器为了性能优化,在无关联的代码的执行是可能会和代码顺序不一致。比如说int i = 1;int j = 2;那么这两条语句的执行顺序可能会先执行int j = 2;线程可见性是指一个线程修改了某个变量,其他线程能马上知道。
3、无锁算法(nonblocking algorithms):
使用低层原子化的机器指令, 保证并发情况下数据的完整性。典型的如CAS算法。
4、内存屏障:
在《深入理解JVM》中解释是:它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;它会强制将对缓存的修改操作立即写入主存;如果是写操作,它会导致其他CPU中对应的缓存行无效。在使用volatile修饰的变量会产生内存屏障(后面会详细解释)。
Java内存模型
下面是我从百度上引入的一张具有代表性的图:
①解释:我根据这张图来解释java内存模型,从图中可以看出每个线程都需要从主内存中读取操作,这个就是java内存模型的规定之一,所有的变量存储在主内存中,每个线程都需要从主内存中获得变量的值。
然后从图中可以看到每个线程获得数据之后会放入自己的工作内存,这个就是java内存模型的规定之二,保证每个线程操作的都是从主内存拷贝的副本,也就是说线程不能直接写主内存的变量,需要把主内存的变量值读取之后放入自己的工作内存中的变量副本中,然后操作这个副本。
最后线程与线程之间无法直接访问对方工作内存中的变量。最后需要解释一下这个访问规则局限于对象实例字段,静态字段等,局部变量不包括在内,因为局部变量不存在竞争问题。
②基本执行步骤:
a、lock(锁定):在某一个线程在读取主内存的时候需要把变量锁定。
b、unlock(解锁):某一个线程读取玩变量值之后会释放锁定,别的线程就可以进入操作
c、read(读取):从主内存中读取变量的值并放入工作内存中
d、load(加载):从read操作得到的值放入工作内存变量副本中
e、use(使用):把工作内存中的一个变量值传递给执行引擎
f、assign(赋值):它把一个从执行引擎接收到的值赋值给工作内存的变量
g、store(存储):把工作内存中的一个变量的值传送到主内存中
h、write(写入):把store操作从工作内存中一个变量的值传送到主内存的变量中。
这里我再引入一张别的地方被我搜来的图供大家一起理解:
volatile关键字
在基本清除了java内存模型之后,我们开始详细说明一下volatile关键字,在concurrentHashMap之中,有很多的成员变量都是用volatile修饰的。被volatile修饰的变量有如下特性:
①使得变量更新变得具有可见性,只要被volatile修饰的变量的赋值一旦变化就会通知到其他线程,如果其他线程的工作内存中存在这个同一个变量拷贝副本,那么其他线程会放弃这个副本中变量的值,重新去主内存中获取
②产生了内存屏障,防止指令进行了重排序,关于这点的解释,请看下面一段代码:
package com.brickworkers;
public class VolatileTest {
int a = 0; //1
int b = 1; //2
volatile int c = 2; //3
int d = 3; //4
int e = 4; //5
}
在如上的代码中,因为c变量是用volatile进行修饰,那么就会对该段代码产生一个内存屏障,用以保证在执行语句3的时候语句1和语句2是绝对执行完毕的,而且在执行语句3的时候,语句4和语句5肯定没有执行。同时说明一下,在上述代码中虽然保证了语句3的执行顺序不可变换,但是语句1和语句2,语句4和语句5可能发生指令重排序哦。
总结:volatile修饰的变量具有可见性与有序性。
下面,我们用一段代码来进一步解释volatile和原子性的概念:
package com.brickworkers;
public class VolatileTest {
// int a = 0; //1
// int b = 1; //2
public static volatile int c = 0; //3
// int d = 3; //4
// int e = 4; //5
public static void increase(){
c++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
public void run() {
increase();
}
}
).start();
}
Thread.sleep(5000);
System.out.println(c);
}
}
//运行3次结果分别是:997,995,989
这个就是典型的volatile操作与原子性的概念。执行结果是小于等于1000的,为什么会这样呢?不是说volatile修饰的变量是具有原子性的么?是的,volatile修饰的变量的确具有原子性,也就是c是具有原子性的(直接赋值是原子性的),但是c++不具有原子性,c++其实就是c = c +1,已经存在了多步操作。所以c具有原子性,但是c++这个操作不具有原子性。
根据前面介绍的java内存模型,当有一个线程去读取主内存的过程中获取c的值,并拷贝一份放入自己的工作内存中,在对c进行+1操作的时候线程阻塞了(各种阻塞情况),那么这个时候有别的线程进入读取c的值,因为有一个线程阻塞就导致该线程无法体现出可见性,导致别的线程的工作内存不会失效,那么它还是从主内存中读取c的值,也会正常的+1操作。如此便导致了结果是小于等于1000的。
注意,这里笔者也有个没有深刻理解的问题,首先在java内存模型中规定了:在对主内存的unlock操作之前必须要执行write操作,那意思就是c在写回之前别的线程是无法读取c的。然而结果却并非如此。如果哪位朋友能理解其中的原委,请与我联系,大家一起讨论研究。
CAS算法
CAS的全称叫“Compare And Swap”,也就是比较与交换,他的主要操作思想是:
首先它具有三个操作数,a、内存位置V,预期值A和新值B。如果在执行过程中,发现内存中的值V与预期值A相匹配,那么他会将V更新为新值A。如果预期值A和内存中的值V不相匹配,那么处理器就不会执行任何操作。CAS算法就是我再技术点中说的“无锁定算法”,因为线程不必再等待锁定,只要执行CAS操作就可以,会在预期中完成。
在ConcurrentHashMap中,很多的操作都会依靠CAS算法完成,具体的在后面两篇博文再深入体会。对于ConcurrentHashMap的基础就先写到这里,中间还有一些笔者也没有参悟,希望大家来一起研究。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012403290/article/details/67636469