JUC1.8-ConcurrentHashMap源码学习-准备

概述

分享下本人在盘ConcurrentHashMap底层源码之前做的技术点预先准备,以及要点方法的学习方式。由于本人之前技术能力也是有限,所以不论之前是有了解过还是未了解的同学都是可以阅读。 当然小菜鸟在阅读学习过程中,有些知识点理解有误,也请各位大佬多多指出。

阅读前了解知识点:

* 二进制的计算

由于在ConcurrentHashMap底层源码过程中,采用大量的位运算代替乘,取余等计算[位运算效率高],那么位运算是基于计算机可识别的二进制的基础上计算,此版块必须得理解;

二进制基础【整数 32位。十进制转二进制 不足32位的,最高位补符号位,其余补零】:

  1. 二进制的最高位是符号位:0代表正数,1代表负数
  2. 在计算机中二进制中有这么三种类型:原码,反码,补码;
  3. 正数的原码,反码,补码都是相同的
  4. 负数的反码: 在原码的基础上,符号位不变,其他位取反。 例如(1010->0101)
  5. 负数的补码:在反码的基础上 + 1
  6. 0的反码和补码都是0
  7. 在计算机运算时,均是与补码的形式进行计算;

正数15转二进制的例子:

15256[2^8]128[2^7]64[2^6]32[2^5]16[2^4]8[2^3]4[2^2]2[2^1]1[2^0]
原码0000001111
反码0000001111
补码0000001111

15 = 2^0 + 2^1 + 2^2 + 2^3。 不满32补0,最高位0是代表正数,因此完整的原-反-补码:
00000000 00000000 00000000 00001111

负数-15转二进制例子:
原码:10000000 00000000 00000000 00001111(转二进制,最高位为符号位)
反码:11111111 11111111 11111111 11110000(符号位不变,其余取反)
补码:11111111 11111111 11111111 11110001(反码+1)

* Java-位运算

在了解二进制的转换后,那么在Java环境中,均是使用补码进行运算;

以下例子用于说明【使用8位方便标识】:
int a = 3;
原码:0000 0011
反码:0000 0011
补码:0000 0011

int b = 5;
原码:0000 0101
反码:0000 0101
补码:0000 0101

1. &与运算[同时为1,则为1,反则0 ]:
3&5 = 1
3补码:0000 0011
5补码:0000 0101
结 果:0000 0001

2. |或运算[只要有1位为1,则为1,反则0]:
3|5 = 7
3补码:0000 0011
5补码:0000 0101
结 果:0000 0111

3. ~非运算[将操作数的每个位(包括符号位)全部取反]
~3 = -248
3补码:0000 0011
结 果:1111 1000

4.^异或运算[俩位相同时,则为0,反则1]
3^5 = 6
3补码:0000 0011
5补码:0000 0101
结 果:0000 0110

5.<<左移运算[将反码整体左移n位,右边空出补0]
3<<1 = 6
3补码:0000 0011   ->左移2位 :000 00110  ->结果:0000 0110

6.>>右移运算[将反码整体右移n位,左边空出,则以符号位为准(正数补0,负数补1)]
3>>1 = 1
3补码 :0000 0011  ->右移1位:00000 001   ->   结果:0000 0001

7.>>>无符号右移运算[将反码整体右移n位,左边空出全部补0]
-3>>>1 = 246
-3补码:1111 1101    ->右移1位:01111 110  ->  结果:0111 1110

* 悲观锁与乐观锁的理解

悲观锁: 一个线程占有了一个资源,而导致其他线程进行等待或者执行其他没有加锁代码块,一直到该锁释放,在由下一个线程占有,性能低。 俗称独占锁,synchronized就是典型悲观锁

乐观锁:每个线程都可以访问,没有加锁,抱着尝试的态度,去执行某个操作,一旦操作冲突或者操作失败,则重试,直到成功为止;

*Java内存模型

每一个线程中,都会有属于自己的工作内存保存了该线程使用的变量在主内存副本copy。 并且各个线程不能访问对方的工作内存,所有线程均是对变量的操作必须在工作内存进行,so线程之间的变量传递只能通过主内存。

[外链图片转存失败(img-dBZtGvz3-1567159246662)(media/15671335030725/15671553423928.jpg)]

主内存与工作内存之间的又是怎么交互:即一个变量如何从主内存拷贝到工作内存? 如何从工作内存同步到主内存中的实现细节?

Java内存模型定义了8种原子操作

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  • use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。

Java内存模型还规定了执行上述8种原子操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

来一张通俗易懂的图:
在这里插入图片描述

* 原子性,有序性,可见性

在了解了上述的Java内存模型,在来看看并发开发中主要围绕三个性质:
原子性:与事务一样样儿的含义。 N个操作要么一起执行,要么一起不执行或者失败;

有序性:在Java的执行过程中,代码执行的保证一定得顺序执行。 那反则无关联的代码,为了性能,可能会和代码块的顺序有出入,例如int i=1, int j=2 ,int m=3, 可能会先int m=3-》int i=1-》 int j=2 这种被称为指令重排

可见性:指当一个线程修改了这个变量的值,新值(修改后的值)对于其他线程来说是立即可以得知的; 在变量被修改过,直接忽略改线程工作内存,直接同步到主内存中; 其他线程在读取时,也是直接去主线程中获取; 普通变量需要经过线程的工作内存

被volatile关键字修饰的变量,是具备可见性以及放置指令重排

* CAS无锁算法

它是如何做到线程安全呢?
首先思考一个问题:为什么会产生线程不安全?看下方代码

void sum(int a){
    i = i+a;
}

在并发情况下,i属于普通变量,现在有A和B俩个线程并发操作这个函数,那么A先从主内存获取i变量—>刷新到A线程工作内存中---->进行i+a操作,在此操作过程中,B线程也要执行此函数,那此时A线程还是处理中,主内存的值还是原先i的,此时B线程获取依旧是原来的i【注意此时A线程已经对i做了操作,只是还没完成而已】---->刷新到B工作内存中---->此时A线程已经完成了函数并将i存入A工作内存----->JMM将A工作内存i副本刷新到主内存【在注意此刻B可是已经把原来老的i取走在用呀】---->这时B线程也完成了函数----->刷新B工作内存并同步主内存了【看出问题了吧】。

本身i+a被A和B线程个执行了一次,正常情况来说B晚于A执行,那么B的i+a中的i应该是A线程计算出来结果,所以出现线程不安全。 那么怎么去解决这个问题? Java给我们提供两种大类型的方式:1、A在执行的时候,B线程等到A执行完成后,B在执行【其实走的悲观锁的思路,现在不做过多介绍】。
2、AB俩个线程你俩可以并行执行,但是变量不能都经过线程工作内存呀,而且得本地保留个副本,不论是哪个线程改了后,得立马让其他线程知道, 那么每个线程在做最后一不commit时,就得用本地的副本与主内存进行比较,相同的话,那么在调整主内存的值,否者不给调整;

http://www.cnblogs.com/stateis0/
为了方便大家理解,说了这么多大白话,下面开始介绍CAS:
CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。

当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。

与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且,线程间的相互影响也非常小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,他要比基于锁的方式拥有更优越的性能。

简单的说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,哪说明它已经被别人修改过了。你就需要重新读取,再次尝试修改就好了。

CAS 的缺点:
CAS 看起来非常的牛皮,但是他仍然有缺点,最著名的就是 ABA 问题,假设一个变量 A ,修改为 B之后又修改为 A,CAS 的机制是无法察觉的,但实际上已经被修改过了。如果在基本类型上是没有问题的,但是如果是引用类型呢?这个对象中有多个变量,我怎么知道有没有被改过?聪明的你一定想到了,加个版本号啊。每次修改就检查版本号,如果版本号变了,说明改过,就算你还是 A,也不行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值