Day298,2024年阿里+腾讯+快手offer都已拿到

image-20210614175334178

它调用putVal()方法

image-20210614171537833

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()

  • 工作流程

image-20210614175426150

image-20210614174835199

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个节点就要转成红黑树呢?

作者其实在源码的注释中已经做了解释

image-20210614180516976

原因是:

在数据量不多的情况下,用链表也无所谓,再慢,也无非执行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++的那个案例

image-20210614182705598

那为什么呢?

image-20210614183236268

因为他只能保证一个get()、一个put()操作是具有线程安全的,但不能保证这一个一系列的组合操作线程安全

还通过sychronized来保证这个组合操作的线程安全,但是不推荐,那都可以直接使用hashmap了

image-20210614183429662

image-20210614183511103

  • 推荐方式: 使用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来判断是否修改成功,不然就一直尝试修改


四、CopyOnWriteArrayList


1、诞生的原因

image-20210614185513077


2、使用场景

读操作很多,且很快;但是写操作慢点没事

image-20210614185751965


3、读写规则

CopyOnWriteArrayList只有写写互斥,读都不加锁

在写的过程中,也可读

image-20210614190028843

  • 代码演示

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”);

}

}

}

}

image-20210614191737340

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”);

}

}

}

}

image-20210614191710282


4、实现原理

  • CopyOnWrite含义

在写的时候,去复制一份,然后在新的内存,通过对新的修改写入,然后再把指向原来内存地址的指针,指向新复制出来的内存地址,再将原来的回收

  • 创建新副本,读写分离

对整个原来数据进行复制一份副本,把修改的内容写入新的副本中,最后再替换回去

  • 不可变原理

对于旧的来说,是不可变的

  • 迭代的时候

在迭代的过程中修改了数据,不是修改原来的数据,而是修改副本数据


  • 那为什么ArrayList会报错呢?

在迭代的next()方法中,会首先会调用checkForComodification()方法,判断是否有被加

image-20210614192805991

image-20210614192856546


而CopyOnWriteArrayList就没有调用checkForComodification()去做判断

image-20210614193020825

总结:

CopyOnWriteArrayList拿到什么数据,取决于他的创建时间不取决于他的迭代时间


5、缺点

image-20210614193650912


6、源码分析

image-20210614193909417

他锁的使用方式跟我们之前的ReentrantLock一毛一样的使用

随便点个用了lock()方法看看:↓↓↓

image-20210614194028292

  • 空参构造器初始化方法,就新建了一个Object[]数组,长度为0:↓↓↓

image-20210614194257863


  • add()方法

image-20210614194530919

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();

}

}

image-20210614194647732


  • get()

发现他的get操作中,根本没有加锁!!!

image-20210614194923223


五、并发队列


1、为什么要使用队列

  • 用队列可以在线程间传递数据

保证线程安全之后,你只需要将东西放到队列中;不用考虑有没有多个人跟你一起放,也不用考虑你放的时候,别人取的情况。

  • 队列将安全问题转移到了队列身上

2、并发队列的简介

  • Queue

  • BlockingQueue


3、高并发队列关系图

不包含所有的实现和类,选取最重要的

image-20210614200017233

4、什么是阻塞队列

image-20210614200829006

image-20210614200904916

image-20210614201213643


六、ArrayBlockingQueue阻塞有界队列


  • 一个对象数组+一把锁+两个条件

  • 入队与出队都用同一把锁

  • 在只有入队高并发或出队高并发的情况下,因为操作数组,且不需要扩容,性能很高

  • 采用了数组,必须指定大小,即容量有限


1、主要方法

  • put&take

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

我的面试宝典:一线互联网大厂Java核心面试题库

以下是我个人的一些做法,希望可以给各位提供一些帮助:

整理了很长一段时间,拿来复习面试刷题非常合适,其中包括了Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等,且还会持续的更新…可star一下!

image

283页的Java进阶核心pdf文档

Java部分:Java基础,集合,并发,多线程,JVM,设计模式

数据结构算法:Java算法,数据结构

开源框架部分:Spring,MyBatis,MVC,netty,tomcat

分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等

微服务部分:SpringBoot,SpringCloud,Dubbo,Docker

image

还有源码相关的阅读学习

image

小编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)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 27
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值