它调用putVal()方法
putVal()方法:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//这里和hashmap不一样,hashmap允许一个元素的key为null,但是这里就不允许了
如果这个槽点没有值
if (key == null || value == null) throw new NullPointerException();
//计算出自己的hash值
int hash = spread(key.hashCode());
int binCount = 0;
//在这个for循环中,完成对 值的插入工作
for (Node<K,V>[] tab = table;😉 {
Node<K,V> f; int n, i, fh;
//判断tab是否没有被初始化,或长度等于0,他就进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果他已经被初始化,且这个位置是空的,那就直接放入赋值
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS操作
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//判断当前的Hash值是不是MOVED
//MOVED代表一种特殊的节点,一种转移节点,说明这个槽点正在扩容
else if ((fh = f.hash) == MOVED)
//帮助进行扩容和转移工作
tab = helpTransfer(tab, f);
else {
//如果这个槽点有值
V oldVal = null;
//保证线程安全
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//进行链表操作,根据当前hash值,找到这个hash该放的对应链表位置
for (Node<K,V> e = f;; ++binCount) {
K ek;
//判断当前存在不存在这个hash对应的key
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//把原来的oldVal赋成新值,并过会返回oldVal
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//到了这个就说明,这个是一个新的
Node<K,V> pred = e;
if ((e = e.next) == null) {
//就在链表的最后创建一个新的节点
//并把值初始化赋上
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//判断他是否是一个红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//putTreeVal()把值放到红黑树中
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//走到这,代表已经完成添加操作了
if (binCount != 0) {
//判断是否要将链表转成红黑树
//TREEIFY_THRESHOLD默认值为8,代表链表节点最少为8个才会尝试转成红黑树
if (binCount >= TREEIFY_THRESHOLD)
//treeifyBin()转换红黑树方法
//这里方法会要求数组的长度要大于默认的64;
//且链表节点长度要大于等于8个节点才会转红黑树
treeifyBin(tab, i);
if (oldVal != null)
//最后这里就是上面说的返回oldVal值
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
-
get()
-
工作流程
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//获取到这个key的hash值,并用h来表示
int h = spread(key.hashCode());
//判断当前的这个数组长度不能等于null,且长度大于0,否则就直接返回null
//代表这个map都没被建立初始化完毕
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果这个key对应的hash赋值这个槽点的hash值
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
//就返回val,说明找到了
return e.val;
}
//如果为负数,说明他是一个红黑树节点或者转移节点
else if (eh < 0)
//那就用find()方法去找到这个红黑树对应的位置
return (p = e.find(h, key)) != null ? p.val : null;
//这里就是,这个他又不是数组,又不是红黑树
//那他这里就是一个链表数据结构
//那就用while循环遍历这个链表
while ((e = e.next) != null) {
//找到对应的值
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
//返回
return e.val;
}
}
return null;
}
6、1.7结构和1.8结构的对比
为什么要把1.7的块结构改成1.8这样子类似hashmap的结构???
-
数据结构不同
-
1.7采用Segment块的结构,默认16个块,也就是16个线程数并发
-
1.8采用类似hashmap的数组+链表+红黑树结构,不限制线程数
-
并发度的改变从 16个------>不限
-
Hash碰撞
-
1.7采用拉链法,链表的形式往下
-
1.8采用拉链法,链表形式往下,然后在根据条件转成红黑树
-
保证并发安全
-
1.7采用分段锁,通过Segment块保证线程安全,Segment块继承ReentrantLock
-
1.8采用unsafe工具类的CAS操作 + sychronized修饰符
-
查询复杂度
-
1.7链表查询复杂度为:O n
-
1.8假设编程了红黑树:O logn
那为什么超过链表超过8个节点就要转成红黑树呢?
作者其实在源码的注释中已经做了解释
原因是:
在数据量不多的情况下,用链表也无所谓,再慢,也无非执行7-8个链表;
那为什么后面要转呢?
因为红黑树每个节点所占用的空间是链表的两倍,
空间损耗要比链表大
。
所以一开始采用默认占用空间更少的链表形式存取
但是实际的情况下,8次冲突,然后转成红黑树的情况只有千万之一:也就是上面的图0.00000006
7、在某种情况下ConcurrentHashMap也不是线程安全的
错误的使用会造成他线程不安全
- 代码演示
/******
@author 阿昌
@create 2021-06-14 18:17
-
组合操作并不保证线程安全
*/
public class OptionsNotSafe implements Runnable {
private static ConcurrentHashMap<String,Integer> scores = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException {
scores.put(“阿昌”,0);
OptionsNotSafe r = new OptionsNotSafe();
Thread thread1 = new Thread®;
Thread thread2 = new Thread®;
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(scores);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Integer score = scores.get(“阿昌”);
Integer newScore = score+1;
scores.put(“阿昌”,newScore);
}
}
}
感觉回到了那个熟悉的a++的那个案例
那为什么呢?
因为他只能保证一个get()、一个put()操作是具有线程安全的,但不能保证这一个一系列的组合操作线程安全
还通过sychronized
来保证这个组合操作的线程安全,但是不推荐
,那都可以直接使用hashmap了
- 推荐方式: 使用replace()方法,CAS操作
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
while (true) {
Integer score = scores.get(“阿昌”);
Integer newScore = score + 1;
boolean flag = scores.replace(“阿昌”, score, newScore);
System.out.println(flag);
if (flag){break;}
}
}
}
尽量避免使用组合操作,而是直接使用ConcurrentHashMap提供的方法来实现组合操作,并通过返回值的boolean来判断是否修改成功,不然就一直尝试修改
1、诞生的原因
2、使用场景
读操作很多,且很快;但是写操作慢点没事
3、读写规则
CopyOnWriteArrayList只有写写互斥,读都不加锁;
在写的过程中,也可读
- 代码演示
ArrayList不支持
在迭代过程中修改数据:↓↓↓
public class CopyOnWriteArrayListDemo1 {
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);
list.add(“5”);
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(list);
String next = iterator.next();
System.out.println(next);
if (next.equals(“3”)){
list.remove(“5”);
}
if (next.equals(“4”)){
list.add(“4 found”);
}
}
}
}
CopyOnWriteArrayList支持
在迭代过程中修改数据:↓↓↓
public class CopyOnWriteArrayListDemo1 {
public static void main(String[] args) {
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);
list.add(“5”);
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(list);
String next = iterator.next();
System.out.println(next);
if (next.equals(“3”)){
list.remove(“5”);
}
if (next.equals(“4”)){
list.add(“4 found”);
}
}
}
}
4、实现原理
- CopyOnWrite含义
在写的时候,去复制一份,然后在新的内存,通过对新的修改写入,然后再把指向原来内存地址的指针,指向新复制出来的内存地址,再将原来的回收
- 创建新副本,读写分离
对整个原来数据进行复制一份副本,把修改的内容写入新的副本中,最后再替换回去
- 不可变原理
对于旧的来说,是不可变的
- 迭代的时候
在迭代的过程中修改了数据,不是修改原来的数据,而是修改副本数据
- 那为什么ArrayList会报错呢?
在迭代的next()方法中,会首先会调用checkForComodification()方法
,判断是否有被加
而CopyOnWriteArrayList就没有调用checkForComodification()去做判断
总结:
CopyOnWriteArrayList拿到什么数据,取决于他的创建时间
,不取决于他的迭代时间
5、缺点
6、源码分析
他锁的使用方式跟我们之前的ReentrantLock一毛一样的使用
随便点个用了lock()方法看看:↓↓↓
- 空参构造器初始化方法,就新建了一个Object[]数组,长度为0:↓↓↓
- add()方法
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//上锁
lock.lock();
try {
//拿到原来的数组
Object[] elements = getArray();
//获取原来的数组长度
int len = elements.length;
//复制一份原来的数组内容
Object[] newElements = Arrays.copyOf(elements, len + 1);
//把要添加的元素添加到新的数组中的最后一个位置
newElements[len] = e;
//将新增的array存储的数组引用,改成这个新创建的数组引用
setArray(newElements);
//返回添加成功boolean
return true;
} finally {
//解锁
lock.unlock();
}
}
- get()
发现他的get操作中,根本没有加锁!!!
1、为什么要使用队列
- 用队列
可以在线程间
传递数据
保证线程安全之后,你只需要将东西放到队列中;不用考虑有没有多个人跟你一起放,也不用考虑你放的时候,别人取的情况。
- 队列将安全问题
转移
到了队列身上
2、并发队列的简介
-
Queue
-
BlockingQueue
3、高并发队列关系图
不包含所有的实现和类,选取最重要的
4、什么是阻塞队列
-
一个对象数组+一把锁+两个条件
-
入队与出队都用同一把锁
-
在只有入队高并发或出队高并发的情况下,因为操作数组,且不需要扩容,性能很高
-
采用了数组,必须指定大小,即容量有限
1、主要方法
- put&take
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
我的面试宝典:一线互联网大厂Java核心面试题库
以下是我个人的一些做法,希望可以给各位提供一些帮助:
整理了很长一段时间,拿来复习面试刷题非常合适,其中包括了Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等,且还会持续的更新…可star一下!
283页的Java进阶核心pdf文档
Java部分:Java基础,集合,并发,多线程,JVM,设计模式
数据结构算法:Java算法,数据结构
开源框架部分:Spring,MyBatis,MVC,netty,tomcat
分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等
微服务部分:SpringBoot,SpringCloud,Dubbo,Docker
还有源码相关的阅读学习
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。*
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-DdftmpPW-1710961063934)]
[外链图片转存中…(img-xpjSBhwa-1710961063936)]
[外链图片转存中…(img-Fq2DrlWN-1710961063937)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-E1GbfF65-1710961063937)]
我的面试宝典:一线互联网大厂Java核心面试题库
以下是我个人的一些做法,希望可以给各位提供一些帮助:
整理了很长一段时间,拿来复习面试刷题非常合适,其中包括了Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等,且还会持续的更新…可star一下!
[外链图片转存中…(img-HObH09KJ-1710961063938)]
283页的Java进阶核心pdf文档
Java部分:Java基础,集合,并发,多线程,JVM,设计模式
数据结构算法:Java算法,数据结构
开源框架部分:Spring,MyBatis,MVC,netty,tomcat
分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等
微服务部分:SpringBoot,SpringCloud,Dubbo,Docker
[外链图片转存中…(img-gwW9Us7A-1710961063938)]
还有源码相关的阅读学习
[外链图片转存中…(img-EXnXunnf-1710961063939)]