java并发

力扣1293bfs超级经典题 记录一下

补充重点:

补充1 JMM(java内存模型不是jvm内存模型)

  • 一定要区分好,JMM 仅仅是java内存模型,和jvm一点关系没有

**JMM:**屏蔽了各种硬件和操作系统的对内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。用来解决由于多线程操作共享变量时,存在的原子性、可见性(缓存一致性)以及有序性问题。(就像我们想要保证可见性和有序,需要使用内存屏障,而不同平台内存屏障种类不同,例如x86架构只有一个stroeload屏障,因为x86只允许写-读 重排序,而其他处理器可能不知这一个,而jmm就是屏蔽这种处理差异,只提供了一个valilie关键字,就可以实现可见性和有序性,开发者不用关心内部实现。jmm中规定了四种你内存屏障模型。)

java内存模型规定,线程之间的共享变量都存储在主内存中,每个线程都有一个私有的工作内存,线程的工作内存保存了共享变量的副本,线程对变量的操作都是在工作内存中进行,线程不能直接读写主内存的变量。

1jmm中规定的内存屏障四种类型

  • LoadLoad 读后
  • StoreStore 写前
  • LoadStore 读后
  • StroeLord 全能屏障 写后

2.JMM规定的内存间8中交互操作

  • lock
  • unlock
  • read
  • load
  • use
  • assiign
  • store
  • write、

3.使用规则

看书

4.volatile关键字

保证可见性和有序性,是通过lock指令,而lock指令的底层是内存屏障实现的。

内存屏障主要作用是:

  • 防止指令重排序
  • 强制对缓存的修改操作立即写入缓存,她会导致其他cpu对应的缓存行无效。,所以每次读取的都是内存

5.final

记住一点,final修饰的变量必须在定义或者构造器中进行初始化

所以final变量也是可以保证可见性的。

写final域重排序规则:

首先直接定义赋值一定可见,而对于构造器初始化的方式,她会再构造器返回之前插入一个ss屏障,保证可见性,也就是保证final不会重排序到构造函数之外。所以某个变量引用对象的操作一定是在final之后,但是普通变量不一定,那么多线程时候,有一个线程先引用了,然后取普通变量的值可能就是未赋值的值。防止对象引用在对象被完全构造完成前被其他线程拿到并使用,注意不要发生this逃逸(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象)

读final域的重排序规则:大多数都是不需要这个规则的,因为都是遵守的。只有个别才需要

**初次读取对象引用和初次读取该对象的final域,**也就是初次调用对象的final域的时候,会再读fianl之前加个loadload屏障。

也就是这种 a=b(已经初始化好的对象) a.final 这俩不会重排序。只有第一次 第二次 就只有a.final 了 怎么谈重排序

6.volatile与synchronized的区别:

参考链接:https://blog.csdn.net/suifeng3051/article/details/52611233

全面了解推荐链接:https://blog.csdn.net/suifeng3051/article/details/52611310 JMMJava内存模型

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性,但是不能保证有序性,也就是,synch可能会重排序。
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化 (因为synchronize保证可见性是因为线程之间顺序执行)

volatile就是基于内存屏障实现

7.方法区如何判断是否需要回收

主要回收内容为:废弃常量无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

补充2 乐观锁与悲观锁的区别和适用场景

区别:

乐观锁是总假设最好的情况,每次去拿数据的时候都认为别人不会修改,不上锁,但是更新的时候要去判断一下在此期间别人有没有更新这个数据,可以用版本号和cas实现

悲观锁是总假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都去上锁,别人想拿这个数据就会阻塞。

场景:

乐观锁适用于写比较少的情况,也就是冲突比较少的场景,这样可以省去锁的开销,同时,冲突少的时候自旋时间也比较短,系统性能比较高

悲观锁适用于写比较多的情况,也就是冲突比较多的场景,如果这时候采用乐观锁,那么会有大量线程自旋,大量占用cpu,反而降低了系统性能,所以这种场景一般用悲观锁。

补充三:华为算法题 牛客上一个循环依赖,数据读入的问题

4
2
0
1,0
1,1
2,0,1

这种数据必须下面这么读,也就是int也要一行一行读,要不然就出事
    Scanner sc=new Scanner(System.in);
        n=Integer.parseInt(sc.nextLine());
        m=Integer.parseInt(sc.nextLine());//编
        Arrays.fill(h,-1);
        ArrayList<Integer> al=new ArrayList<>();
        for(int i=0;i<n;i++){
            String str=sc.nextLine();
            String [] temp=str.split(",");
4
2
1,3
1,0
1,1
0
例如这组数据,她在nexxtint两个读完后,在用nextline的话,就会读到空串??,这可真实奇葩,也就是如果第三个不是单个的数字,就会读空串。为啥呢???

1.isInterrupted和interrupted方法

isinterrupted 判断是否被打断,不会清除打断标记

interrupted 判断是否被打断,会清除打断标记。

2.interrupt方法

打断 sleep wait join 的线程。会让线程进入阻塞状态,而且打断sleep的线程,会清除打断状态。

3.主线程和守护线程

只有声明为守护线程的才是守护线程,当其他非守护线程结束,守护线程即使没执行完也会结束,gc线程就是守护线程

4.偏向锁的升级

https://www.jianshu.com/p/b422fc9cbba0?utm_source=desktop&utm_medium=timeline

  • https://baijiahao.baidu.com/s?id=1630535202760061296&wfr=spider&for=pc这个链接必须看
  • 首先看对象锁是无锁状态(0 01)还是偏向锁状态(1 01),如果是无锁状态,直接走轻量锁逻辑,如果是偏向锁且是匿名偏向锁,那么就直接把当前的线程id替换到偏向锁,如果不是匿名要看是不是当天线程的id,如果是当前线程的id,那么就不用管,生成一个锁记录就可以了,如果不是,那么就是偏向锁产生竞争了。那么就要进行锁撤销(不是锁消除)
  • 偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID);
  • https://blog.csdn.net/tongdanping/article/details/79647337

注意的几点:

  • 偏向锁重入,也是生成锁记录的
  • 偏向锁没有解锁流程

偏向锁撤销的情况:

  • 调用对象的hashcode方法,因为原本的位置放的是线程id哦。
  • 发生锁竞争的时候
  • 其他线程使用偏向锁对象,应该和第一个一样,初始化是不是就有hashcode了。

批量重偏向:

  • 对于一类对象当撤销偏向锁阈值超过20次以后,就会吧这些对象重新偏向到加锁线程。

批量撤销:

  • 当撤销偏向锁阈值超过40次后,就要把这个类的所有对象变成不可偏向状态,以后新建对象也是不可偏向状态。

偏向锁-》轻量级锁-》重量级锁这个流程弄明白,那么锁膨胀这个地方就解决了。

5.wait和notify

都是object对象的方法,而且,必须要关联monitor对象后(也就是获取锁之后)才能用,还有就是他是把线程放到waitset里面等待了,对应着waiting状态,在entrylist上的线程是blocked状态

6.两阶段终止模式

在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

其实就是在一个线程中不断检测一个信号,根据他去停止(如 break掉循环)。另一个线程发信号表示我要终止你。嗯 这就是两阶段终止

感觉都要和while连用吧。。。或者搞个线程?具体我不懂有多少种实现,反正while是一种。

7.同步模式之保护性暂停

就是一个线程等待另一个线程的结果,如果没有这个结果就要等(wait等),他们两个都是关联到一个对象,然后用的一把锁,所以要用wait。如果有结果了,那么要notifyall一下。

8.Lock和unlock

LockSupport类的方法

每个线程都有个parker对象,有三部分,条件变量(小黑屋),互斥锁,counter。

park就是加锁,只要counter=0,就加锁,如果counter=1,那么不暂停,继续运行,然后设置counter成等于0.

unpark是解锁,直接设置counter=1。同时唤醒条件变量中的线程。多次执行unpark一个线程,和执行一次效果一样。

注意:unpark和park成对使用,unpark在前也可以起作用。

9. 多把锁

  • 作用:增强并发读
  • 坏处:容易发生死锁

10.定位死锁

  • 使用jps定位进程id
    • 可以用jstack去检测对应进程
    • 也可以用jconsole去检测死锁(有这个按钮,方便的一批哈)

11.ReentrantLock和synchornized区别

  • 可中断(lock是可中断锁,而synchronized 不是可中断锁

      		线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
      
       	 	 	 	 	 	 	 	 			如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
      
       	 	 	 	 	 	 	 	 			如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情)
    

    这个中断可不是interrupt的打断。

  • 可以设置超时时间

  • 可以设置为公平锁(synchornized是非公平锁),**公平锁:**就是外面的线程和lock阻塞的线程竞争的时候是一样的,也可以得到锁,如果不公平,那么外面线程是得不到锁的,会加到lock的阻塞队列。

  • 支持多个条件变量(这个就是小黑屋,而且是相当于waitset那种小黑屋)

  • 和synchronized一样都支持重入

12.ReentrantLock杂谈

  • 有可打断模式和不可打断模式(reentrantlock.lockInterruptibly()是可中断,lock()是不可中断,会继续在阻塞队列等待)

  • 有超时功能,用的是trylock(立刻失败),里面放时间参数就可以控制时间,而不是立即失败了。

  • 默认是不公平的

13 AQS

就是一个并发包的基础组件,是阻塞式锁和相关的同步器工具的框架,用来实现各种锁,各种同步组件的。Reentranlock就是在他基础上实现的。

AQS原理

aqs有个state变量表示同步状态,通过内置的FIFO队列完成获取资源线程的派对,aqs使用cas对state进行院子操作,

14wait和sleep的区别

  • sleep是Thread的方法,而wait是Object的方法
  • sleep不需要和synchronized一起使用
  • sleep在睡眠的时候是不释放对象锁的,wait等待会释放 而且会进入对象锁的waitset队列
  • 相同点是 状态都是timed_waiting

15 虚假唤醒

notify只能随机唤醒一个waitset中的线程,如果唤醒的不是正确的线程,就是虚假唤醒

解决方法:改用notifyall,然后根据某些标识,判断呢些线程需要继续等待,呢些不需要而直接执行

16 happens-before 是什么

**前一个结果可以被后续操作获取。**前一个操作对后面的操作可见,后者咸鱼后面的操作执行。

标准:

  • 程序顺序规则:对于操作又先后依赖关系的,不润虚对这些操作进行重排序。也就是之前对之后是可见的,这个就是程序代码中,之前的写对后面的读可见。
  • 锁定规则:就是下面的1
  • valatile规则
  • 线程启动规则:就是start之前的对线程内可见,因为start之前的结果会同步到驻村,新县城创建好了之后直接是主存取值
  • 传递规则 a hb b, b hbc ahbc
  • https://segmentfault.com/a/1190000011458941

17 CAS

  • cas基于乐观锁的思想,不怕别人修改,改了我就重试。
  • synchronize是悲观锁,就是不让你们改,我直接锁上。

1.一个线程对m加锁,并在解锁前对共享变量修改,对接下来对m枷锁的线程可见

2.对valtile变量的写 对其他线程可见

3.线程start前对变量的写,对线程开始后的读可见

4.线程1打断线程2前对变量的写,对于其他得知

17volatile与synchronized的区别:

参考链接:https://blog.csdn.net/suifeng3051/article/details/52611233

全面了解推荐链接:https://blog.csdn.net/suifeng3051/article/details/52611310 JMMJava内存模型

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

volatile就是基于内存屏障实现

18 关闭线程池的方法

  • shutdown 线程池状态变为shutdown
  • shutdownnow 线程池变为stop状态

19为什么用线程池

1.降低资源消耗,提高线程利用率,降低创建和消耗线程的开销

2.提高相应速度,任务来了 有现成的话直接执行,不用等着创建了

3.提高线程的可管理型

  • CAS:乐观锁 (其实底层还有有一把锁, ) 取旧址 操作 cas 三部,他有几个问题:
    • 原子性问题:
      • 因为是compareandset 涉及两部操作 先比较 再设置值,如果两个线程 一个发现相等,要设置值,另一个线程再此期间更改了,这样就会覆盖了。所以必须保证原子性,底层是c++中unsafe类实现的。查看源码有个lock(一把锁,如果多核才有,一般对缓存行枷锁,如果内容大,可能在总线加一把锁,内核级别加了一把锁)和cmp(比较并交换),保证原子性。
    • ABA问题
      • 问题:就是两个线程a和b,a要把值从0改到1,b线程速度很快,先改了0到1,然后又从1改回0,这时候a才要去执行cas操作,但是这时候发现还是0,可以执行。但是事实上这个值已经被改变过了。
      • 解决:加一个版本号,每次修改值的时候,都对版本号+1,而且每次比较也要比较版本号。java有对应的实现。
  • 轻量级锁一定比重量级锁性能高吗
    • 不一定哦,轻量级锁有自旋操作,比较耗cpu,如果线程很多,那么很费cpu
  • 死锁:举例子说明把
  • 定位死锁:1.jconsole用户界面 2.jps查看java进程 jconsole查看一个进程所有线程状态定位死锁线程
  • 饥饿:其实就是非公平锁的时候,可能有的线程一直无法执行。

1 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。

2 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。

3 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。

4 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。

20 unsafe

unsafe类的构造方法是私有的,所以外部无法创建,她由静态代码块,在类加载的时候自动创建一个私有的final的unsafe对象,所以如果我们想用只能通过反射来创建,而且这个名字unsafe就表明这个类最好别自己用,容易产生不安全的事情,因为她可以操作内存

21 LongAdder 和 伪共享

1. 为什么要有longadder,Atomiclong不就可以了吗?

  • 因为Atomiclong在并发量特别高的情况下,线程自旋时间会长很多,整体耗时就会大,而longadder就是为了改善这个情况才产生的。当然longadder不能代替atomiclong因为longadder就几个方法而已。

2. 什么是为共享

为共享是多线程修改两个独立变量的时候,这些变量会处于同一个缓存行。

**白话文:**这是由于cpu三级缓存的结构导致的,cpu缓存是cpu和主存直接一个高效快速的存储设备,容量很小,速度很快,它的结构是缓存行,也就是她读取的单位也是缓存行,对64位处理器缓存行是64字节,例如读取一个long数组,那么每次会读取8个long数据,如果两个cpu读取不同数据,但是因为读取的是缓存行,他们可能读取的是同一个缓存行,如果其中有一个cpu修改了数据,并写会了主存,那么根据缓存一致性原理,另一个cpu的缓存行就会标记为失效状态,这样会造成cpu频繁访问主存,造成时间损耗增大,所以需要填充字节,保证每次只读取一个long数据,可以防止为共享的发生。

解决:在类上加一个注解@sun.misc.Contended

2.1 了解一下cpu三级缓存

cpu缓存定义CPU缓存是位于CPU与内存之间的临时数据交换器,它的容量比内存小的多但是交换速度却比内存要快得多。。为了简化与内存之间的通信,高速缓存控制器是针对数据块,而不是字节进行操作的。每次读取一个缓存行,典型的一行是64字节

2.2 cpu缓存的意义

CPU往往需要重复处理相同的数据、重复执行相同的指令,如果这部分数据、指令CPU能在CPU缓存中找到,CPU就不需要从内存或硬盘中再读取数据、指令,从而减少了整机的响应时间。所以,缓存的意义满足以下两种局部性原理

2.3 cpu三级缓存

随着多核CPU的发展,CPU缓存通常分成了三个级别:L1L2L3。级别越小越接近CPU,所以速度也更快,同时也代表着容量越小。L1 是最接近CPU的, 它容量最小(例如:32K),速度最快,每个核上都有一个 L1 缓存,L1 缓存每个核上其实有两个 L1 缓存, 一个用于存数据的 L1d Cache(Data Cache),一个用于存指令的 L1i Cache(Instruction Cache)。L2 缓存 更大一些(例如:256K),速度要慢一些, 一般情况下每个核上都有一个独立的L2 缓存; L3 缓存是三级缓存中最大的一级(例如3MB),同时也是最慢的一级, 在同一个CPU插槽之间的核共享一个 L3 缓存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7AntoHY2-1626533465997)(D:\2021年秋招历程\新建文件夹\image\image-20210412082938047.png)]

2.4 cpu缓存一致性原理https://www.cnblogs.com/xuanbjut/p/11608991.html

MESIModified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出的)是一种广泛使用的支持写回策略的缓存一致性协议。为了保证多个CPU缓存中共享数据的一致性,定义了缓存行(Cache Line)的四种状态,而CPU对缓存行的四种操作可能会产生不一致的状态,因此缓存控制器监听到本地操作和远程操作的时候,需要对地址一致的缓存行的状态进行一致性修改,从而保证数据在多个缓存之间保持一致性。

MESI中的状态,四种,2bit表示

状态描述监听任务状态转换
M 修改 (Modified)该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。
E 独享、互斥 (Exclusive)该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。当CPU修改该缓存行中内容时,该状态可以变成Modified状态
S 共享 (Shared)该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。当有一个CPU修改该缓存行时,其它CPU中该缓存行可以被作废(变成无效状态 Invalid)。
I 无效 (Invalid)该Cache line无效。

Modified状态跳转
Local Read :从当前Cache取数据,状态不变,还是M。
Local Write :向当前Cache写数据,状态不变,还是M。
Remote Read :先将这条数据写入内存,让其他Core能够拿到最新数据,状态变为S。
Remote Write :先将这条数据写入内存,之后其他Core会修改这条数据,状态变为I。
Exclusive状态跳转
Local Read :从当前Cache取数据,状态不变,还是E。
Local Write :向当前Cache写数据,状态变为M。
Remote Read :其他Core会从内存读取这条数据,变成共享状态,状态变为S。
Remote Write :数据倍修改,当前Cache行数据无效,状态变为I。
Shared状态跳转
Local Read :从当前Cache取数据,状态不变,还是S。
Local Write :向当前Cache写数据,状态变成M,其他Cache中该行数据无效,变成I。
Remote Read :对当前Cache无影响,状态还是S。
Remote Write :数据被修改,当前行数据无效,状态变为I。
Invalid状态跳转
Local Read :如果其他Cache没有缓存这条数据,需要从内存中读取该数据,读取之后状态变成E;
如果其他Cache有这条数据,且状态为M,则先将数据更新到内存,当前Cache再到内存中取数据,之后这两个Cache的相应行数据相同,都变成S;
如果其他Cache有这条数据,且状态为S或E,当前Cache从内存中取这条数据,这些Cache相应行数据相同,都变成S;

Local Write :从内存中读取数据,在Cache中修改,状态变成M;
如果其他Cache有这条数据,且状态为M,则要先将数据更新到内存;
如果其他Cache有这条数据,则这些Cache行变成I。
Remote Read :对当前Cache无影响,状态还是I。
Remote Write :对当前Cache无影响,状态还是I。

3 LongAdder源码剖析

不能替代AtomoticLong,一点是因为她的空间占用更大,二店是因为他实现的方法没有前者多,例如AL可以传入一个函数进行操作(复杂运算),更灵活。

功能:

LongAdder():累加器只有一个无参的构造器,会构造一个sum=0的实例对象。
increment():自增。
decrement():自减。
add(delat):增量计算。
sum():计算sum的和。
reset():重置sum为0。

源码流程:

首先如果cell数组(@sun.misc.Contended 修饰,只有一个long valotile修饰的变量)是空,并且写base成功,那么直接返回

如果cell数组不为空,并且写cell没发生竞争就返回

如果写base竞争,或者是对应下标的cell是空,或者cell竞争失败 那么就要执行LongAccumulate函数

longaccumulae:其实是个自旋函数

主要是三种情况

第一种是cells数组被初始化了,那么当前线程应该将数据写入对应的cell中

第二种是进行cells数组的初始化流程,如果成功直接返回,如果因为并发导致失败,例如没有抢到锁,或者其他抢先一步初始化了,那么就走第三种情况。

第三种尝试去写base,也就是如果第二步因为并发失败了,那么就往base写,如果写base失败就自旋,成功就返回,而且如果失败一定回到第一中情况了。

第一种情况细分:

如果当前线程下标对应的cell是空,那么就创建,成功就返回,如果因为高并发导致失败,直接从头自旋

如果对应的cell不为空,那么判断是否是因为cell竞争进入的longaccumulate,如果是直接先rehash,否则去写cell,如果写cell失败(rehash后写失败哦)并且可以扩容就去扩容,扩容成功就返回,否则继续rehash。

这个cell提供了cas方法,以及volatile的long类型

public void add(long x) {
        //as 表示cells引用
        //b 表示获取的base值
        //v 表示期望值
        //m 表示cells数组的长度
        //a 表示当前线程命中的cell单元格
        Cell[] as; long b, v; int m; Cell a;
        /**
         * 条件1:true-》表示cells已经初始化过了,当前线程应该将数据写入到对应的cell中
         *        false-》表示cells未初始化,当前所有线程应该将数据写入到base中
         *
         * 条件2:true-》表示当前线程cas替换成功
         *        false-》表示发生竞争了,可能需要重试  或者  扩容
         *
         *
         * */
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            /**
             * 什么时候会进来
             * 1.cells已经初始化过了,当前线程应该将数据写入到对应的cell中
             * 2.表示发生竞争了,可能需要重试  或者  扩容
             * */

            //true表示未竞争   false 表示发生竞争
            boolean uncontended = true;

            //条件1:true->说明cells未初始化,也就是多线程写base发生竞争了
            //      false->说明cells已经初始化了,当前线程应该是找自己的cell  写值了

            //条件2: getProbe()获取当前线程的hash值  m表示cells长度-1  cells长度一定是2的次方树
            //        true-> 说明当前线程对应下标的cell为空,需要创建   longaccumulate支持
            //        false->说明当前线程对应的cell不为空,说明下一步将x值添加到cell中

            //条件3:true->表示cas失败,意味着当前线程对应的cell有竞争
            //       false->表示cas成功,返回就好了
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[getProbe() & m]) == null ||
                    !(uncontended = a.cas(v = a.value, v + x)))
                //都有呢些情况会调用
                /**
                 * 1.说明cells未初始化,也就是多线程写base发生竞争了(重试|初始化cells)
                 * 2.说明当前线程对应下标的cell为空,需要创建   longaccumulate支持
                 * 3.表示cas失败,意味着当前线程对应的cell有竞争[重试|扩容]
                 * */
                longAccumulate(x, null, uncontended);
        }
    }
 final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        //表示线程hash值
        int h;
        //条件成立 表示当前线程还没有分配hash值
        if ((h = getProbe()) == 0) {
            //给当前线程分配hash值
            ThreadLocalRandom.current(); // force initialization
            //取出当前线程的hash值,复制给h
            h = getProbe();
            //为什么是true,因为默认情况下,当前线程 一定写入cell0的位置,不把她当做一次真正的竞争
            wasUncontended = true;
        }
        //表示扩容意向,false一定不会扩容,true可能会扩容
        boolean collide = false;                // True if last slot nonempty
        //自旋
        for (;;) {
            //as表示cells引用
            //a表示当前命中的cell
            //n表示cells的数组长度
            //v表示期望值
            Cell[] as; Cell a; int n; long v;
            // case1:表示cells已经初始化了,当前线程应该将数据写入对应的cell中
            if ((as = cells) != null && (n = as.length) > 0) {
                //当前线程对应下标的cell为空,需要创建  这个函数支持
                //cas失败,意味着当前线程对应的cell有竞争[扩容|重试]

                //case1.1 当前线程对应的下标位置的cell是null,需要创建new  cell
                if ((a = as[(n - 1) & h]) == null) {
                    //true表示当前锁未被占用, fasles-表示锁被占用
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        //拿当前的x创建cell
                        Cell r = new Cell(x);   // Optimistically create
                        //条件1:true-》表示当前锁 未被占用 fasle表示当前锁被占用
                        //添加2:true-》表示当前线程获取锁成功,false表示当前获取锁失败。
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                //rs 表示当前cells引用
                                //m 表示cells长度
                                //j 表示当前线程命中的下标
                               Cell[] rs; int m, j;

                               //前俩条件恒成立
                                //rs[j = (m - 1) & h] == null 为了防止其他线程初始化过该位置,然后当前线程
                                //再次初始化位置到时数据丢失
                                if ((rs = cells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                //case1.2
                // 只有一种情况 wasUncontended为false,就是当cells初始化后,并且当前线程竞争修改失败才是false
                // 然后他就直接到h=advance那块重新获取hash值去了
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //case1.3 当前线程rehash过hash值,然后新命中的cell不为空,或者直接命中部位空
                //true->写成功,退出就行了
                //fasle->表示rehash之后命中新的cell 也有竞争,重试一次失败了,或者就是第一次竞争失败
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                        fn.applyAsLong(v, x))))
                    break;
                //case1.4
                //条件1 :n>=NCPU true->扩容意向改为false,表示不扩容了 fasle->说明cells数组还可以扩容
                //添加2:cells!=as ture->表示其他线程已经扩容过了,当前线程rehash之后重试即可
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                //case1.5
                //!collide=true 设置扩容意向为true,但是不一定发送扩容
                else if (!collide)
                    collide = true;
                //case1.6 真正扩容逻辑
                //条件1:true-》表示当前无锁逻辑 可以竞争锁
                //条件2:true-》表示获得所成功,false 表示当前线程有其他线程扩容
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                           Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                //重置当前线程hash值
                h = advanceProbe(h);
            }
            //case2:前置条件  cells还未初始化  as为null
            //条件1:true:表示当前未枷锁
            //条件2:cells==as?因为其他线程可能会再你给as赋值之后修改cells
            //条件3:true 表示获取锁成功 会把cellsBusy=1,false表示其他线程正在持有这把锁
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    //怎么又判断一次? 因为如果两个线程同时执行玩上面if的前两个,然后一个获得锁,执行代码
                    //另一个等锁释放也执行,就覆盖了啊
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //case3:
            //1.当前cellsBusy枷锁状态,其他线程正在初始化cells,所以当前线程将值累加到basse
            //2.cells被其他线程初始化后,当前线程需要将数据累加到base
            else if (casBase(v = base, ((fn == null) ? v + x :
                    fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

22 线程池

如果面试让说线程池,从优势,然后说java中创建线程池的类是Threadpollexecutor类,然后说核心参数,然后说状态,然后说常用的阻塞队列,然后说拒绝策略,然后说Executors可快速创建线程池,把那四个常用的说一下,内部构成参数等等

1. 线程池的优势

  • 降低系统资源消耗,通过复用已经存在的线程,降低线程创建和销毁的消耗
  • 提高系统响应速度,当有任务到达时,通过复用已经存在的线程,无需等待新线程的创建便能立即运行
  • 方便线程并发数的控制,因为线程若是无限制的创建,可能会导致内存占用过多,同时cpu切换也会更加频繁,造成系统资源的浪费
  • 可以提供更强大的功能,例如延时定时线程池

2.线程池为什么使用阻塞队列

线程池是采用生产者-消费者模式设计的
线程池为消费者。

在线程池中活跃线程数达到corePoolSize时,线程池将会将后续的task提交到BlockingQueue中, (每个task都是单独的生产者线程)进入到堵塞对列中的task线程会wait() 从而释放cpu,从而提高cpu利用率。

3.线程池的状态

RUNNING

SHUTDOWN 不会接收新任务,但是会处理阻塞队列剩余任务

STOP 会中断正在执行的任务,并抛弃阻塞队列的任务

TIDYING 任务全部执行完毕,活动线程为0,即将进入中介

TERMINATED 终止了

4.线程池的核心参数(前五个必选,后两个可以选可以不选)

  1. corePoolSize(必需):核心线程数,默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。

  2. maximumPoolSize(必需):线程池维护线程的最大数量。

  3. keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。

  4. unit(必需):指定keepAliveTime参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

  5. workQueue(必需):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现。

  6. threadFactory(可选):线程工厂,用于指定为线程池创建新线程的方式。

  7. handler(可选):线程池对拒绝任务的处理策略。

5.线程池的拒绝策略

  • 抛出RejectedExecutionException异常 ----默认策略
  • 让调用者运行任务
  • 放弃本次任务
  • 放弃队列中最早的任务,本任务取而代之

6.线程池的阻塞队列种类

6.1 LinkedBlockingQueue

1.LinkedBlockingQueue 是一个基于链表的无界阻塞队列(也可以指定队列长度),当生产者往队列中放一个数据时,队列会从生产者手中获取数据并缓存在队列内部,而生产者立即返回,只有当队列缓存区达到所指定的最大容量时才会阻塞生产队列,直到消费者从队列中消费掉一份数据,生产者线程才会被唤醒,消费者这端的处理也基于同样的原理。

2**.LinkedBlockingQueue可以高效的处理并发数据,这是因为生产者和消费者端分别采用了独立的锁来控制数据同步(两把锁)**,所以在高并发的情况下生产者和消费者可以并行地操作队列中的数据,从而提高整个队列的并发性能。

3.如果没有给 LinkedBlockingQueue 指定其容量大小,则默认为 Integer.MAX_VALUE,这样的话,如果生产者的速度大于消费者的速度,则可能还没等到队列阻塞,系统内存就被消耗完了,从而导致内存溢出

6.2 ArrayBlockingQueue

1.基于数组的有界阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,在初始化时必须指定大小。

2.ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。因为ArrayBlockingQueue采用的是数组作为数据存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。

3.ArrayBlockingQueue内部的阻塞队列由重入锁ReenterLock和Condition条件队列实现,并且实现的队列中的锁是没有分离的,所以ArrayBlockingQueue中可以通过构造方法设置公平访问或非公平访问,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

小节

LinkedBlockingQueue队列两把锁,ArrayBlockingQueue一把锁,linkedblockingqueue只能是非公平锁,而array可以设置,默认是非公平锁。

6.3 SynchronousQueuehttps://www.cnblogs.com/frankyou/p/9525015.html(同步队列)

1.SynchronousQueue是一个无缓冲不存储元素的阻塞队列,消费者线程调用take()方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用put()方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。

2.其他存储元素的阻塞队的吞吐量会高一些,但因为队列对元素进行了缓存,所以及时响应性能可能会降低。

3.SynchronousQueue可以声明使用公平锁或非公平锁。

6.4 PriorityBlockingQueue

1.一个支持优先级排序的无界阻塞队列,对元素没有要求。

2.可以实现Comparable接口也可以提供Comparator来对队列中的元素进行比较。

3.跟时间没有任何关系,仅仅是按照优先级取任务。每次出队都返回优先级最高或者最低的元素。

4.内部是使用平衡二叉树实现的,遍历不保证有序。

6.5 DelayQueue

1.DelayQueue类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。(堆)

2.要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。同样要实现compareTo()方法,将队列中的任务排序(一般是按时间排序)

3.应用场景有缓存系统,任务调度系统,或者网吧上机下机系统还有的就是线上考试系统,比如考驾照开始答题时间不同,但是大家同样都有两小时的时间。这些系统虽然依靠遍历也能实现,但是耗时效率低下

7 jkd提供的几种线程池

7.1 newFixedThreadPool(会有饥饿现象,就是线程都去运行了,没人执行任务了,线程不足导致的,和那个非公平锁竞争资源竞争不到区分开,解决是不同任务类型,使用不同的线程池)

定长线程池最大线程数与核心线程数一样多,采用链表结构的无界队列 LinkedBlockingQueue

任务流程:向 newFixedThreadPool 线程池提交任务时,如果线程数少于核心线程,创建核心线程执行任务,如果线程数等于核心线程,把任务添加到 LinkedBlockingQueue 阻塞队列,如果有线程执行完任务,去阻塞队列取任务,继续执行。

适用场景:FixedThreadPool 能够保证当前的线程数能够比较稳定,适用于处理CPU密集型的任务(cpu密集型就是线程大部分时间都花费在cpu运算上),确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,控制线程最大并发数。即适用执行长期的任务。

注意事项:newFixedThreadPool 使用了无界的阻塞队列 LinkedBlockingQueue,如果线程获取一个任务后,如果单个任务的执行时间比较长,会导致队列中累积的任务越积越多,导致机器内存不断飙升, 从而导致OOM。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
7.2 newCachedThreadPool

可缓存线程池:无核心线程,非核心线程数量为 Interger. MAX_VALUE,采用 无缓冲不存储元素的阻塞队列 SynchronousQueue,非核心线程空闲存活时间为60秒。所以如果长时间处于空闲的,该线程池不会占用任何资源。

任务流程:因为没有核心线程,所以任务直接加到SynchronousQueue队列,如果此时有空闲线程,则取出任务执行,如果没有空闲线程,就新建一个线程去执行。执行完任务的线程,可以存活60秒,如果在这期间接到任务,则可以继续活下去,否则将被销毁。

适用场景:适合执行大量、耗时少的任务。

注意事项:当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。这样会导致创建的线程过多,耗尽 CPU 和内存资源,导致OOM。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
7.3 newSingleThreadExecutor

单线程化线程池:核心线程数和最大线程数均为1,采用链表结构的无界队列 LinkedBlockingQueue。如果当前线程意外终止,则会创建一个新的线程继续执行该任务。

任务流程:判断线程池中是否有一条线程在,如果没有,新建线程执行任务,如果有,则将任务加到阻塞队列,执行完线程后,再继续从队列中取。

适用场景:适用于串行按顺序执行任务的场景。不适合可能引起IO阻塞性及影响UI线程响应的并发操作,如数据库操作、文件操作等。

注意事项:使用了无界的阻塞队列 LinkedBlockingQueue,如果线程获取一个任务后,如果单个任务的执行时间比较长,会导致队列中累积的任务越积越多,导致机器内存不断飙升, 从而导致OOM。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
7.4任务调度线程池ScheduledThreadPoolExecutor

在这个之前,先说一下Timer(废弃了,没人用了,这个太垃圾了),她是他之前来实现定时功能的,但是所有任务都是同一个线程调度,因此都是穿行执行的,同一时间只能有一个任务执行,前一个任务延迟或者异常都会影响到之后的任务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fPMDRdJZ-1626533465999)(D:\2021年秋招历程\新建文件夹\image\image-20210412132919259.png)]

现在都用ScheduledThreadPoolExecutor(定时任务和延时任务都可以):如果有个任务出现异常也不会影响其他,同时任务出现异常也不会抛出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vZ37nh0-1626533466000)(D:\2021年秋招历程\新建文件夹\image\image-20210412134104485.png)]

8 提交任务执行方法

  • execute 只能接收Runnable的任务
  • submit 可以接收Runnable,Callable 底层调用了execute方法
  • invokeAll 提交tasks中的所有任务
  • invokeAny 提交task所有任务,那个任务先执行成功,返回这个结果,其他任务取消
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5bkuWijz-1626533466002)(D:\2021年秋招历程\新建文件夹\image\image-20210412130744069.png)]

submit和execute区别(其他的不重要):

1.execute传参是Runnable,而submit传参是Callable或Runnable类型。

2.execute执行没有返回值,而submit会返回一个Future类型的对象。

3.execute方法无法得到异常信息,submit可以使用get方法得到异常信息

9 关闭线程池

  • shutdown

    会将线程池的状态变为SHUTDOWN,不会接收新任务,但是已经提交的任务会执行完,此方法不会阻塞调用线程的运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oghK3Sb6-1626533466004)(D:\2021年秋招历程\新建文件夹\image\image-20210412131824588.png)]

  • shutdownNow

    线程池状态变为STOP

    不会接收新任务,会将队列中任务返回,并用interrupt的方式打断正在执行的任务

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N32OCUzS-1626533466004)(D:\2021年秋招历程\新建文件夹\image\image-20210412131936105.png)]

10 线程池异常处理

因为线程池一般有一场也不会抛出异常,不打印,怎么办?

  • 可以自己写try,catch 补货打印
  • 配合future,futrue的get方法可以抛出异常信息

11 jdk1.7新加入的Fork/Join线程池

fork是执行,join是获取结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgtku3fn-1626533466006)(D:\2021年秋招历程\新建文件夹\image\image-20210412134852017.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-322fgyZr-1626533466007)(D:\2021年秋招历程\新建文件夹\image\image-20210412135233738.png)]

23 AQS

全称是 **AbstractQueuedSynchronizer,**是阻塞式锁和相关的同步器工具的框架

特点:

用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取

锁和释放锁

getState - 获取 state 状态

setState - 设置 state 状态

compareAndSetState - cas 机制设置 state 状态

独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList

条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)

tryAcquire (独占模式)

tryRelease (独占模式)

tryAcquireShared (共享模式)

tryReleaseShared (共享模式)

isHeldExclusively 当前线程是否持有锁(独占模式?)

补充:

AQS底层使用的锁是park和unpark,他们如果打断标记是true的时候,park是无效的,打断源码会用到

ReentrantLock加锁流程(独占模式,非公平锁)
  • 首先尝试cas看是否可以获得到锁(把state从0改到1)
  • 如果能就枷锁成功,如果不能就执行acquire方法
  • 在进行一次尝试tryacquire,如果不能获得锁或者是不是可重入锁,如果不行,就把这个节点加到等待队列(addwaiter方法,先尝试快速入队,快速入队失败就尝试自旋入队),然后加入成功之后判断是不是head之后的第一个节点,如果是就在尝试一次,如果成功就获得了锁,不成功就park住。
final void lock() {
  // 首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁
  if (compareAndSetState(0, 1))
  	setExclusiveOwnerThread(Thread.currentThread());
  else
  	// 如果尝试失败,进入 ㈠
  	acquire(1);
}

public final void acquire(int arg) {
    	//第一个tryAcquire表示在尝试一次,然后失败就看看是不是锁重入,如果锁重入那么就直接锁重入
        //addWaiter函数 保证线程加入等待队列,同时返回插入的那个节点的前一个结点。
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt(); //如果被打断过,那么会执行到这里,打断当前线程
}

//非公平锁的TryAcquire实现
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
    		//首先在尝试一次,如果能成功岂不是更好
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    		//这个是锁重入的情况,如果是锁重入就让state+1
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
 }

//addWaiter的实现,队列不为空就先尝试快速入队,失败就往下走自旋入队,自旋入队的时候会判断如果空就先初始化。反正这个函数保证插入队列成功

//如果初始化了就快速入队,加到后面,如果快速入队失败就走下面的自旋入队
//如果没初始化直接进自旋入队函数,这个函数会先初始化在入队,如果线程发生竞争就自旋重试。
//她就是快速入队和自旋入队(如果有两个线程同时竞争就会有自旋入队)两种情况
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //快速入队,如果等待队列不为空,那么就加到尾部,返回这个节点就ok
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
    	//自旋入队,
        enq(node);
        return node;
}

//返回当前节点
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

//申请入队,上面那个是加到队列,然后害的申请入队

//这个简单说是初始化队列然后放进去(记住这个能进来的前提是队列没有初始化)
//看到for(;;)自旋没跑了,自旋就是解决多线程竞争失败情况的。
//这个首先是判断如果等待队列是否是空,如果是空就初始化
//然 
//整体意思就是,看看是不是第一个节点,如果是看能获取到锁吗,如果不能就继续park,别人打断的话,也没用,只要获取不到就继续自旋。。
//不可打断模式也在这里,就是如果打断了,会清楚打断标记,然后自旋,如果获取不到锁继续park。
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //如果是前取是头结点,并且尝试获得锁成功。就把当前节点当做头,把以前的头next改空,让gc
                //回收,同时返回打断标记false,表示不用打断,就是不用执行那句话了。
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //走到这就说明要挂起来了
                //第一个函数表示把该节点接到正确的地方,就是往前找前面节点状态小于等于0的点(0表示初始头结点,-1表示SIGNAL她要唤醒后面的)
                //第二个函数就是中断的意思,就是打断了,阻塞起来。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;//这个标记只是让别人知道是被打断了
                 	//parkAndCheckInterrupt会清楚打断标记的,因为如果不清楚park失效,这里然后返回
               		//这个interrupted是标记一下
                	//被打断过,然后返回这个interrupted
            }
        } finally {
            //这个是必须走的,但是但是 她是清理的,清理两种node
            //1.node不在关联任何线程
            //2.node的waitstatus置为cancelled
            //具体操作就是node出队
            if (failed)
                cancelAcquire(node);
        }
}
打断模式和不可打断模式

打断模式,就是抛出异常

不可打断模式,即使被打断,仍然会留在AQS等待队列,一直等待活的到锁,才能知道自己被打断。

原理就在上面,不可打断模式中,只是设置了一个标志,然后继续去尝试获取锁。只有在获得到锁,才能根据这个标志的返回知道是否被打断。

锁重入原理

如果竞争失败会进入acquire,然后再进入tryacquire方法进行重试,如果当前枷锁线程就是锁持有线程就是锁重入,然后进行state+1。获得锁。

公平锁原理

就在tryacquire,公平锁会检查AQS队列是否有前驱节点,没有才去竞争,而非公平锁没有判断这个步骤

释放锁

太简单了:如果是独占锁先判断是不是当前线程持有,不是报错,否则就继续,将state-1,然后看state是否是0,如果是就唤醒等待队列的head之后的那个节点,如果不是就返回。

public void unlock() {
        sync.release(1);
}

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //只有waitstatus!=0也就是=-1才可以unpark,一般都是-1
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒后继节点
            return true;
        }
        return false;
}

 //首先让state-1,如果=0表示释放锁成功,如果不等于0 表示重入了,重入次数-1,整个释放流程是失败的
 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
 }
条件变量await原理,相对简单很多了

首先加入到condition队列,然后释放所有的锁,然后park主

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //将当前节点加到condaition队列,单向的貌似
            Node node = addConditionWaiter();
    		//释放所有的锁,重入的全部释放,里面调用了release方法,会唤醒head后的那个节点
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
}

条件变量signal,首先找到condition队列的第一个节点,然后把它拿出来,next指针置空,然后把它放入等待队列,放到等待队列的时候,如果前一个节点取消了或者前一个节点设置-1状态失败就unpark了

public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
}

private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        //循环入队
       //在从condition队列到同步队列之前,tail节点被取消了
	   //在从condition队列到同步队列之后,tail节点没被取消,执行完ws > 0 之后,这段时间,被取消了?就通		   过!compareAndSetWaitStatus(p, ws, Node.SIGNAL)再判断一下
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);//
        return true;
}

补充个装饰模式和代理模式的区别

这两个模式都是要实现和对象一样的接口,然后做功能的增强,内部组合了接口对象

看过钢铁侠没?

史塔克穿反浩克战甲(装饰模式)

但是史塔克没能力自己穿,只能让贾维斯帮自己从太空上叫下来(代理模式)

贾维斯只有一个(代理模式不会无限代理)

mark装甲有无数个(装饰模式可以无限增强)

代理模式控制访问

装饰器模式动态增加行为

1.代理模式强调的是控制,装饰模式强调的是增强。
2.代理模式强调的是透明访问,装饰模式强调的是自由构建。

用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。

当我们使用装饰器模 式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。

就像spring中的动态代理,有时候需要对类加事务注解,其实最后使用这个类的时候,用的是代理里类,而根本看不到真实类,对于装饰器模式例如bufferedinputstream就是装饰器,他可以接收Inputstream类型的对象,然后使其具有缓存功能。我们可以自行构建我们需要的功能。

//

ReentrantReadWriteLock 可重入的读写锁

为什么重入时写锁可以降级为读锁(锁降级指的是,拥有写锁的时候获取读锁,然后释放写锁这个过程),但是不支持读锁升级位写锁?

回答:首先因为读锁的拥有者可能是很多个,如果所有线程都去同时竞争写锁,那么会引起巨大的抢占,而且如果竞争失败,那么这些线程应该如何处理。而写锁只能一个线程拥有,写锁到读锁不会有其他线程的抢占问题。

非公平锁的写锁加锁流程:

//首先直接调用acquire方法
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

//这个方法是读写锁重写了的,但是差不多,而且公平非公平也是在这里实现的,非公平的时候writerShouldBlock,函数直接返回false,公平就要看是否是head后的第一节点了,如果有直接返回加锁失败。
//case1:state不等于0,也就是已经被其他线程加锁,如果加的读锁直接失败,如果加的写锁但是加锁线程不是当前线程就失败,如果超过重入次数的最大限制也失败。
//case2:state=0  尝试一下加锁
//失败就走addwaiter 和 acquireQueued流程

//其实这个函数就是看看是否可以加锁:如果state=0,就竞争cas去加锁,如果state不等于0,就看是否当前线程是
//
protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //1 << SHARED_SHIFT) - 1 =max_cout 也就是大于等于2的16次方就报错
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
}

//把节点加入到队列尾部
 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}

//就是加入之后重新申请入队的时候,如果是head之后的第一个节点,就在尝试一下,否则就park住。
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

非公平锁写锁的释放流程:

public void unlock() {
            sync.release(1);
}

//就是去把state的写锁数清0,然后ExclusiveOwnerThread设为null,然后如果队列还有后继节点就去upark后续节点
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}


protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
}

非公平锁读锁加锁流程:

记住锁降级会有写锁的unpark,否则只能本线程一直加读锁,,

public void lock() {
            sync.acquireShared(1);
}


public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

//这个就是尝试获得锁
//
protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
    		//如果有写锁并且不是当前线程的就获取失败返回
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
    		//看获取读锁这步是否应该堵塞,就是看等待队列的第一节点是否是写锁,如果可以更新就让读锁计数+1
    		//但是注意,这里发现一个东西,就是第一次加锁的线程的读锁次数,是不放threadloclmap的,只有第二次以后的才放
    		// 所以如果是第一次加读锁,直接操作firstreader和fisrstreaderholdcount,否则就去改threadlocalmmap
    		//如果不是因为写锁的失败,那就走fullTryAcquireShared去自旋重试
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                //下面是加1可能能否更新成功
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        //这里面有一个thredlocal变量,用来保存线程的读锁计数的
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
    		//这个就是对读锁+1的,
            return fullTryAcquireShared(current);
}


//加入等待队列,然后再判断是否是第一节点,如果是就在尝试一下,否则就park主
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
}

非公平锁读锁释放流程:

public void unlock() {
            sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
}

//就是将对应的state-1,然后第一个获得读锁的和缓存里的那个单独处理,其他的从线程本地变量中取出那个计数字段进行-1,然后冲突就一直自旋重试

protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
}
//如果释放了所有的读锁 就去唤醒下一个节点,这个可能写锁在那等呢,反正唤醒一下倍
 private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

StampedLock jdk1.8 新家的一个更高效的读写锁

必须配合戳一起用

semaphore 信号量 用来限制能同时访问共享资源上线的,可以用来单机限流

例如在限流中,她可以限制单机的线程数量。

两个方法:acquire和release

acquire源码:

//居然是可打断模式
public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}
//这个方法没用 就是调用了tryacuire方法
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())//可打断的原理就是抛出异常
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
}

//非公平锁的实现,对state进行-1,一旦-1之后小于0了,就失败了,如果不小于0就成功了,成功之后就要自旋改变state,直至成功
final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
}

//如果获取失败就要加入队列,然后如果加入后是head之后的第一个节点,就再尝试获取一次锁,失败就park主
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

release源码:

public void release() {
        sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
}

//就是将state+1,然后会判断是否越界,这个越界是整数越界了
protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
 }

//盲猜这个也是唤醒,唤醒第一个节点
private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
 		static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

hashmap源码

8和6(变红黑树以及红黑树退化为链表的阈值) 根据的是泊松分布

为什么table的长度是2的n次方

  • 方便数据迁移
  • 方便计算位置,可以直接变成与运算
//重要参数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //  缺省table大小16

static final int MAXIMUM_CAPACITY = 1 << 30;//table最大的值

static final float DEFAULT_LOAD_FACTOR = 0.75f;//缺省装填因子

static final int TREEIFY_THRESHOLD = 8;//treeify_threshold 树化阈值 链表长度达到就可能变红黑树

static final int UNTREEIFY_THRESHOLD = 6;//untreeify_threshold 树降级为链表的阈值

static final int MIN_TREEIFY_CAPACITY = 64;//也是树化阈值,table中桶个数超过64才可以树化

transient Node<K,V>[] table;//哈希表

transient int size;//当前哈希表元素个数

transient int modCount;//当前哈希表结构修改次数,就是插入,删除都是,更新不是

int threshold;//扩容阈值  当哈希表中元素个数超过她就触发扩容

 final float loadFactor;//负载因子  这个用不到,我们不动 用上面那个默认的 threadhold=她乘以capacity



public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
    	//就下面这个函数有点东西,还不用看,就是把输入的非2的n次方变成2的n次方
        this.threshold = tableSizeFor(initialCapacity);
}

put 方法

1.首先计算hash值,是在原有hash值的基础上进行了两次扰动运算,目的就是让高16位也参与到后续计算table下标的过程,减少hash碰撞的几率。

2.如果table是空,就走resize扩容,hashmap是懒惰初始化的。

3.然后计算key对应的table下标,hash&size-1

4.如果刚好那个桶位是空,那么直接创建一个node节点,然后放进去就可以了

5.如果那个桶位不是空,并且如果要插入的key的等于那个点的key,走替换流程。

6.否则看节点类性是否是红黑树,如果是红黑树,就走红黑树的插入流程,如果是链表就走链表的插入流程

7.其中链表的插入中,如果没找到,并且插入之后元素个数会超过9的时候,就直接树化流程。去判断是否需要转红黑树,只有容量大于等于64的时候才会转红黑树

8.然后进入替换流程,对于存在的点,根据传入参数判断是否需要替换。(红黑树和链表如果存在会返回那个点)

8.最后在判断容量+1是否超过了扩容阈值,如果超过了,要走扩容流程。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
//让key的hash值的高16位也参与到计算下标的运算,减少hash冲突
//因为开始table很小,那么做与远算的时候,只有低位参与,我们这样可以让高位也参与进行
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    	//tab:引用当前的table
    	//p:表示当前散列表的元素,就是table数组位置上的元素
    	//n:表示散列表数组的长度
    	//i:表示路由结果,就是下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    	//table如果是空,这个是延迟初始化,防止浪费空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    	
    	//最简单的情况,寻址招到的桶刚好是null,直接将key,value封装成node放进去
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //e: 招到了一个与当前要插入的key-value相同key的元素
            //k:表示临时的一个key
            Node<K,V> e; K k;
            
            //
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//没找到直接加到后面就完事了
                        p.next = newNode(hash, key, value, null);
                        //因为从0开始,如果bincount=7还没招到,也就是8个都没找到,就要可能树化或者扩容
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1s
                            treeifyBin(tab, hash);
                        break;
                    }
                    //条件成立就找到了,替换就行了
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e不为空,替换操作
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
    	//表示操作次数,替换不算,插入和删除算
        ++modCount;
    	//如果加进来一个  超过阈值 就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}

扩容方法

  • 防止hash冲突过多,导致链化严重,造成查询效率低,扩容可以解决这个问题

1.首先是球数组新容量和新阈值,然后根据新容量和阈值去扩容。

2.如果当前table已经被初始化了,那么就走正常的扩容流程,让新容量=当前容量2倍,新阈值也是当前阈值的二倍。如果table没有被初始化且阈值是0,这时候使用默认的容量和阈值,16和12。如果table没有初始化但是阈值不是0,那么就让新容量等于当前阈值,新阈值=新容量*装填因子。

3.到这就有了新容量和新阈值,然后根据新容量创建一个新table,大小是新容量。

4.如果老table有元素,那么就要把元素移动到新数组里面

5.分三种情况,

  • 如果是单个元素,直接和以前那种计算下标方式计算下标,然后放到新数组中。

  • 如果是红黑树节点。初始化两棵树高位树和低位树,然后根据hash值和原数组大小与运算得到新增加的hash位是1还是0,如果是0,放在低位树,如果是1放在高位数,然后根据他们各自节点个数,如果小于或者等于6,就去红黑树化,否则就转链表。然后放在对应的table里。

  • 如果是链表节点,初始化两个链表,高位链表和低位链表,分别存新参与hash位是1和0的,然后放到对应table下标处。

final Node<K,V>[] resize() {
    //oldTab:引用扩容前的table
    Node<K,V>[] oldTab = table;
    //oldCap:表示扩容之前table数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr:表示扩容之前的扩容阈值,触发本次扩容的阈值 
    int oldThr = threshold;
    //newCap:扩容之后table数组的大小
    //newThr:扩容之后,下次再次触发扩容的条件
    //下面就是要计算这两个数
    int newCap, newThr = 0;
    
    //条件如果成立,说明散列表被初始化过了(就是table被初始化过了),这是一次正常的扩容
    if (oldCap > 0) {
        //如果原table大小已经大于等于最大值了。把阈值搞成Integer的最大值返回就行了,没法扩容了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //oldCap左移一位实现数值翻倍,并且赋值给newCap,newCap要小于最大容量,且扩容之前的阈值》=16
        //这时候,下一次扩容阈值也翻倍,至于>=16只有当自己指定一个大小例如8,才会出现这种情况·	
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)//标号1
            newThr = oldThr << 1; // double threshold
    }
    //oldCap==0,oldThr>0  说明散列表没有初始化呢,table是null
    //new HashMap(initcap,loadFactor)  他们只会初始化thr
    //new HashMap(initcap)
    //new HashMap(map)(map有数据)
    else if (oldThr > 0) // 这个是上面那三种,未初始化的时候,第一次put时候到这
        newCap = oldThr;
    //oldCap==0 oldThr==0
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
    }
    //标号1和2处,的情况会newThr=0,通过newcap计算出一个newThr
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    //至此  上面的所有代码终于求出了newcap和newthr
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //说明桶位中有数据,但是是单个还是链表还是红黑树不知道
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;//方便gc
                //第一种情况 说明桶位是单个元素,直接计算出当前元素存放在新数组中的位置,然后丢进去
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                
                //树的情况
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                
                //链表
                else { // preserve order
                    //低位链表:在新数组中的位置:当前位置
                    Node<K,V> loHead = null, loTail = null;
                    //高位链表:在新数组中的位置:当前位置+扩容前数组的长度
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //这个就是直接能得到新参与的hash那位是1还是0
                        //如果==0 那么那位就是0,放在低位链表,否则放在高位链表
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        //放在高位链表
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    
                    //低位链表有数据,那就把链放到table里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //高位链有数据,就把链放到数组里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}


//红黑树的扩容那
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

红黑树(弱平衡的二叉搜索树)

学她之前要会二叉查找树和平衡二叉树

平衡二叉树一共四种旋转 要画图说明 简单

  • 左旋
  • 右旋
  • 先左在右
  • 先右再左

红黑树的性质:

  • 每个节点要么是黑色要么是红色
  • 根节点是黑色
  • 每个叶子节点是黑色
  • 每个红色节点的两个子节点都是黑色
  • 任意一个节点到每个叶子节点的路径包含相同数量的黑节点,俗称黑高
  • 如果一个节点存在黑子节点,那么该节点一定有两个子节点。(上面的推论)
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kpdOX1XO-1626533466008)(D:\2021年秋招历程\新建文件夹\image\image-20210419210458949.png)]

红黑树能自平衡,靠的是什么?三种操作:左旋、右旋和变色

**1.变色:**节点的颜色由红变黑或者由黑变红。

2.左旋:以某个节点作为支点(旋转节点),其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,左子节点保持不变。

**3.右旋:**以某个节点作为支点(旋转节点),其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,右子节点保持不变。

上面左旋和右旋和平衡二叉树差不多 很简单的,

红黑树的查找和二叉搜索数的查找一样,类似二分

**插入操作:**一定是红色插入,就是把插入点上红色插入,因为红色插入不一定破坏红黑树的性质,但是黑色插入一定破坏红黑树的性质。因为黑的进来,这个路径多个黑,一定破坏。

具体情况:

  • 如果红黑树是空,直接插入,然后变色就可以了。
  • 插入节点key已经存在,直接改变值就可以了。
  • 插入节点的父节点是黑节点,直接插,不影响平衡
  • 插入节点的父节点是红色,影响平衡。

2.树结构常用术语
http://note.youdao.com/noteshare?id=ab5f0ac86b7a1e9aa7905130b093827e

3.二叉搜索树
http://note.youdao.com/noteshare?id=c90f95e292275df28c8f33d5196c37d8

4.红黑树原理讲解
http://note.youdao.com/noteshare?id=9b50b184f00f75af266fd53e334bb819

5.红黑树插入案例分析
http://note.youdao.com/noteshare?id=747937f5e7401e4800c645743d00461a

6.源码讲解文档
http://note.youdao.com/noteshare?id=1e6cf9471d3768dd9f3c4e863431e438

conconrenthashmap

https://blog.csdn.net/ZOKEKAI/article/details/90051567

https://blog.csdn.net/tp7309/article/details/76532366

hashmap(32) put之后容量就是32

1.7 的conhashmap也是32

1.8的就变64了

concurrenthashmap扩容时间点:

  • 链表长度大于8,并且总元素个数小于64
  • 走addcount方法的时候,达到阈值
//为0,表示数组未初始化,且初始容量是0
//正数,如果未初始化,那么记录的是初始容量,如果已经初始化,那么记录的是扩容阈值
//-1 表示数组正在初始化
//小于0不是-1,表示正在扩容,-(1+n),表示此时有n个线程正在共同完成数组的扩容操作
private transient volatile int sizeCtl;

put函数

  • 首先判断key和value是否为空,一个为空就报错
  • 首先计算hash值,然后计算下
  • 如果table空就初始化,初始化中sizectl是-1,初始化后就是扩容阈值
  • 如果当前位置桶没有元素,直接加进去,采用cas进行加的,如果失败就自旋
  • 否则如果当前位置正在扩容,也就是当前节点hash值是MOVED(-1),forword节点,那么就走帮助扩容函数
  • 否则,就把桶加锁,然后判断是链表还是红黑树,分别走链表或者红黑树的插入流程,如果是链表的话,还要判断一下是否需要转红黑树。也就是节点大于8,并且桶的个数大于等于64.
  • 最后在根据元素个数是否达到阈值判断是否需要扩容。走addCount方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//这个一定是正数
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果当前table位置没有元素,直接加进去就行了。
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果当前那个位置的值是moved,表示那个位置正在扩容,不能加,要去协助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //f这把锁是桶锁,只锁当前桶位。
                synchronized (f) {
                    //这个判断是防止树化了,或者扩容了,也就是以前那个元素不在这个位置了。
                    if (tabAt(tab, i) == f) {
                        //大于等于0,就是链表的添加
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    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;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                //如果是列表个数是不是大于8,如果大于把考虑用不用变红黑树。害的table大于等于64,否则就走扩容
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
    	//维护集合的长度,然后加了一个,还要判断是否需要扩容
        addCount(1L, binCount);
        return null;
}

//扰动函数,她可以保证是正数,这个和hashmap不同
(h ^ (h >>> 16)) & HASH_BITS; 
HASH_BITS=0111 1111 1111 1111 1111 最高位是0,其余都是1.
    
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                //让出cpu执行权,但是不阻塞,可以继续抢占
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //防止重复初始化,可能别人初始化了,然后他刚进入while,while没拦住她。
                    if ((tab = table) == null || tab.length == 0) {
                        //sc有值就用sc,如果sc是0,就用默认的。
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);//n-0.25n  0.75n
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
}

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
}

扩容流程,最重要的addcount函数 还有个事红黑树那有个扩容函数,一共就这两个地方有扩容函数

  • 添加元素,所以首先就是修改hash表元素总数。
  • 维护总数和longadder的操作一样,先判断cell数组是否是空,如果空就写base,如果不是空就写cell,如果写cell失败或者cell数组是空或者写base失败,就执行fulladdcount自旋函数,去写
  • 自旋函数fullAddCount:第一种情况是cell数组已经初始化了,第二种情况是没有初始化就走初始化流程,第三种是初始化失败就直接写base。第一种情况可以细分,如果cell是空,就创建cell然后写进去,如果创建失败就自旋。如果cell不是空且是第一次进来full函数,那么直接rehash。否则就在尝试一次,尝试失败就去判断是否可以扩容(如果没有其他人扩容,或者cell个数小于cpu核数就可以扩容)。
  • 经过上面可得元素总数。然后和阈值比较判断是否需要扩容。
private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
    	//第一个if就是维护集合长度,和longadder原理一抹一样
    	//如果cell数组不为空,就进if,如果cell数组为空,就往base加,失败在进if
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //如果是base竞争失败,或者cell数组空,或者是写cell竞争失败,就执行fulladdccount方法
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //如果当前总数大于等于阈值并且table不是空并且没超过最大容量上线,就扩容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                //rs就是个标识,她第17位是1,然后其他值是容量2进制值表示有几个0
                //其实就是个容量标识。不是容量,是标识,表示某个以某个容量扩容。
                int rs = resizeStamp(n);
                if (sc < 0) {
                    //下面是高并发时
                    //case1:判断是否当前容量还是扩容标识那个容量,不是就break,扩容完成了
                    //case2和3:是对扩容线程最小值和最大值的判断
                    //case4,5:确保tranfer中nextTable相关初始化逻辑已经走完了。
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //设置线程数加1,然后执行并发扩容函数
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //第一次扩容,那么把线程数初始设置为2
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
}


private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //第一种情况是cell数组初始化了
            if ((as = counterCells) != null && (n = as.length) > 0) {
                //对应cell为空,直接写cell,如果写失败就自旋,否则返回
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                //如果是cas失败进的full函数就直接rehash一次
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //否则就尝试加对应的cell,成功就返回,失败就rehash之后继续自旋
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                //如果写cell失败,然后看是否可以扩容true是可以扩容,下面是如果as变了或者cell个数大于cpu核数就不润虚扩容
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                //进行扩容
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = ThreadLocalRandom.advanceProbe(h);
            }
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

resizeStamp())的返回值(简称为rs) 高16位置0,第16位为1,低15位存放当前容量n扩容标识,用于表示是对n的扩容。

private static int RESIZE_STAMP_BITS = 16;

/**
 * The bit shift for recording size stamp in sizeCtl.
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

Integer.numberOfLeadingZeros(n)用于计算n转换成二进制后前面有几个0。这个有什么作用呢?
首先ConcurrentHashMap的容量必定是2的幂次方,所以不同的容量n前面0的个数必然不同,这样可以保证是在原容量为n的情况下进行扩容。
(1 << (RESIZE_STAMP_BITS - 1)即是1<<15,表示为二进制即是高16位为0,第16位为1:

0000 0000 0000 0000 1000 0000 0000 0000
1
所以resizeStamp()的返回值(简称为rs) 高16位置0,第16位为1,低15位存放当前容量n扩容标识,用于表示是对n的扩容。
rs与RESIZE_STAMP_SHIFT配合可以求出新的sizeCtl的值,分情况如下:

sc >= 0
表示没有线程在扩容,使用CAS将sizeCtl的值改为(rs << RESIZE_STAMP_SHIFT) + 2)。
sc < 0已经有线程在扩容,将sizeCtl+1并调用transfer()让当前线程参与扩容

//容量n=8
0000 0000 0000 0000 0000 0000 0000 1000
//Integer.numberOfLeadingZeros(8)=28,二进制表示如下:
0000 0000 0000 0000 0000 0000 0001 1100
//rs
0000 0000 0000 0000 1000 0000 0001 1100
//temp = rs << RESIZE_STAMP_SHIFT,即 temp = rs << 16,左移16后temp最高位为1,所以temp成了一个负数。
1000 0000 0001 1100 0000 0000 0000 0000
//第一个线程要扩容时,sc = (rs << RESIZE_STAMP_SHIFT) + 2)
1000 0000 0001 1100 0000 0000 0000 0010

所以sizeCtl,为负数是是下面的情况

高15位低16位
容量n扩容标识并行扩容线程数+1

并发扩容函数transfer

什么时候触发扩容

  • put方法最后调用addcount方法,发生数量达到阈值的时候扩容
  • 扩容状态下,其他线程对map的插入,删除,修改,合并操作遇到forward节点的时候会触发扩容
  • 当插入元素链表长度大于8 但是桶个数少于64的时候触发扩容。

扩容流程:

  • 首先计算每个线程处理的任务单元中桶的个数,如果是单核cpu那么就是要处理所有桶,如果是多核那么任务单元的桶数是数组长度/8/ncpu,然后如果没有超过16就取16。
  • 然后判断是否是第一次扩容,如果是第一次扩容就取创建一个新数组,大小是老数组的二倍
  • 然后是一个自旋函数,她是用来迁移一个任务单元中所有的桶。包括两大部
    • 第一是计算当前线程处理桶的最小下标和最大下标
    • 从大到小对每个桶进行迁移,首先判断当前桶是否为空,如果是空,就直接方一个forward节点,如果当前桶是forward节点就直接继续找(这种情况能发生吗?),然后如果是正常的桶的节点,首先加锁,然后和hashmap的转移流程差不多,除了单个节点不一样,分两种,如果是链表就搞个高位链表和低位链表,然后搞完就放到新数组中对应位置,如果是红黑树也是,只不过要判断要不要红黑树退化。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
    	//扩容是把原数组分成几段,然后每个线程每次负责一端,段的大小就是stride
    	//如果cpu核数是1,就整个交给一个线程
    	//如果多cpu,那么让n/8/ncpu 如果低于16就取16,否则就是这个值,因为最小的任务单元是16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)  //每个线程处理桶的最小数目,可以看出核数越高步长越小,最小16个。
            stride = MIN_TRANSFER_STRIDE; // subdivide range
    	//第一次扩容,nexttab是空,需要初始化新数组
        if (nextTab == null) {
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];  //扩容到2倍
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;  //扩容保护
                return;
            }
            nextTable = nextTab;
            transferIndex = n;  //老数组的长度,扩容总进度,>=transferIndex的桶都已分配出去。
        }
        int nextn = nextTab.length;
          //扩容时的特殊节点,标明此节点正在进行迁移,扩容期间的元素查找要调用其find()方法在nextTable中查找元素。她的hash值是move(-1)
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); 
        //当前线程是否需要继续寻找下一个可处理的节点
        boolean advance = true;
        boolean finishing = false; //所有桶是否都已迁移完成。
    
    	//这个循环用于处理一个 stride 长度的任务,i 后面会被赋值为该 stride 内最大的下标,而 bound 后面会被赋值为该 stride 内最小的下标
   		//通过循环不断减小 i 的值,从右往左依次迁移桶上面的数据,直到 i 小于 bound 时结束该次长度为 stride 的迁移任务
    	//结束这次的任务后会通过外层 addCount、helpTransfer、tryPresize 方法的 while 循环达到继续领取其他任务的效果
    	//注意这里的i和bound是局部变量,也就是说是线程私有的。
    	//数据迁移是每个线程负责一部分的。
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //此循环的作用是确定当前线程要迁移的桶的范围或通过更新i的值确定当前范围内下一个要处理的节点。
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)  //每次循环都检查结束条件
                    advance = false;
                //迁移总进度<=0,表示所有桶都已迁移完成。
                else if ((nextIndex = transferIndex) <= 0) {  
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {  //transferIndex减去已分配出去的桶。
                    //确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            //当前线程自己的活已经做完或所有线程的活都已做完,第二与第三个条件应该是下面让"i = n"后,再次进入循环时要做的边界检查。
            //判断当前线程任务是否做完了。
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {  //所有线程已干完活,最后才走这里。
                    nextTable = null;
                    table = nextTab;  //替换新table
                    sizeCtl = (n << 1) - (n >>> 1); //调sizeCtl为新容量0.75倍。
                    return;
                }
                //当前线程已结束扩容,sizeCtl-1表示参与扩容线程数-1。
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
	                //还记得addCount()处给sizeCtl赋的初值吗?相等时说明没有线程在参与扩容了,置finishing=advance=true,为保险让i=n再检查一次。
                    //这个是判断是否是最后一个扩容线程
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)   
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            
            //下面是线程对数据进行迁移
            //如果当前位置是空,没必要迁移,放个fwd就ok
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);  
            //如果当前位置已经有线程去迁移了,那么就去寻找下一个可以迁移的节点
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                //迁移成功  才放fwd节点
                synchronized (f) {  //桶内元素迁移需要加锁。和添加用同一把锁,只能一个获得
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {  //>=0表示是链表结点
                            //由于n是2的幂次方(所有二进制位中只有一个1),如n=16(0001 0000),第4位为1,那么hash&n后的值第4位只能为0或1。所以可以根据hash&n的结果将所有结点分为两部分。
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            //找出最后一段完整的fh&n不变的链表,这样最后这一段链表就不用重新创建新结点了。
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            //lastRun之前的结点因为fh&n不确定,所以全部需要重新迁移。
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //低位链表放在i处
                            setTabAt(nextTab, i, ln);
                            //高位链表放在i+n处
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);  //在原table中设置ForwardingNode节点以提示该桶扩容完成。
                            advance = true;
                        }
                        else if (f instanceof TreeBin) { //红黑树处理。
                            ...

get函数:很简单的,不能看到正在修改的数据,只能看到修改成功的数据,因为volatile所以只要修改成功就能看到。

不用加锁,如果正在扩容,那么就调用fwd节点的find方法去取

  • 如果数组是空或者下标是空返回null
  • 如果正在扩容中,hash值小于0,那么就调用fwd节点的find方法取
  • 否则就遍历就完事了

 public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

常见题

1.chm的负载因子可以修改吗?

不可以,经验值。hashmap可以。

2.为什么NODE.hash字段一般情况下大于等于0,为什么?

负值有其他意义,就是扩容流程中的迁移过程,如果某个桶迁移完成,会放个fwd节点,她的hash值是-1,还有一个是桶里那个红黑树的根节点hash值是-2。

3.sizeCtl值的意义

sizeCtl=-1:表示正在进行初始化,并发情况下,防止多个线程进行初始化。

sizeCtl>=0:表示扩容的阈值,达到这个值就要考虑扩容了

sizeCtl<0 但是不等于-1:高15位表示容量标识,低16位表示扩容线程数+1;

4.扩容线程戳的计算方式?

首先计算出容量的二进制表示前面有多少个连续的0用这个数,然后用这个数和1左移15位进行或运算,然后再将结果整体左移16位。

5.怎么保证写安全?

如果当前桶是空,就用cas进行写,如果cas竞争写失败了,就用synchorize加锁,保证并发安全,加锁只加的是当前桶。

6.chm如何统计当前散列表数据量?

采用的是longadder的做法,代码一样。

7.为什么不采用Atomiclong统计?

因为她同一时刻只能有一个成功,浪费cpu资源。

8.触发扩容条件的线程,执行的预处理工作有呢些?

修改sizeCtl,搞一个扩容唯一标识戳,还有创建一个新table。

9.forwardingnode除了标记迁移完,还有其他功能吗?

她还提供了一个查询的方法,从新表查询。

10.散列表正在扩容,再来写请求怎么处理?

如果写的那个桶没有迁移,加锁直接写,如果正在迁移,那么就要等。

11.最后一个退出扩容任务的线程需要执行呢些收尾工作?

就是会扫描一下桶,看有没有遗留的桶没迁移,心老表替换,然后计算新阈值。

threadlocal

https://javap.blog.csdn.net/article/details/114035062

https://www.cnblogs.com/dennyzhangdd/p/7978455.html

https://blog.51cto.com/u_15064648/2573651

java中引用类型

  • **强引用:**常用的都是强引用,正常new 赋值都是强引用

  • **软引用:**内存不够会回收,内存够用不会被gc回收。

    非常适合做缓存,内存够就待着,内存不够就出去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tsd0rFVP-1626533466009)(C:\Windows\System32\image-20210424142834938.png)]

  • **弱引用:**最难 考的最多,threadlocal使用的是弱引用,只要垃圾回收就会被回收

  • **虚引用:**只有一个作用,管理直接内存,就是直接访问操作系统内存,不用在拷贝到jvm内存里面了,通过一个指针直接指向操作系统内存。但是jvm的gc只能回收jvm的内存,系统内存怎么回收,这就是虚引用的作用。

    虚引用只起到一个通知的作用,也就是说如果回收虚引用,就把她放到一个队列里面,然后回收系统内存是根据队列去回收的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O95kZ5i1-1626533466009)(C:\Windows\System32\image-20210424143929985.png)]

注意:

  • Entry中的key是一个虚引用,指向ThreadLocal,而程序中new的那个是指向ThreadLocal的强引用
  • TheadLocalMap中的Entry数组存储数据,初始长度是16,后续2倍扩容。

其实每次set值,都是获取到线程自己threadlocalmap,然后根据Threadlocal获取到对应的Entry中的value值

set流程:

  • 首先根据hash值计算在threadlocalmap中entry数组中的下标
  • 然后从上面的下标开始遍历,如果key相等,就覆盖value,如果key为null,就用心的key,value覆盖,如果entry是空,就直接放进去,然后让seize++,然后从当前位置往后找几个点清理赃key,然后在判断是否达到卡扩容阈值,如果达到就执行rehash函数,reshah函数首先执行一次赃key的清理,然后再判断是否达到阈值的四分之三,如果达到就扩容
    • 上面有个点,为啥要先走一个cleanSomeSlots(采样清理),因为后续的那个遍历所有的赃key比较耗时。
    • 为什么达到四分之三就扩容了?没有解决:因为key是弱引用,也就是说有一些key可能因为gc被回收了,也就是虽然现在事四分之三,但是之前可能已经达到了扩容阈值
//总结就是,根据线程招到线程里面的map,然后把t当key,value当value放进去就ok了。
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//返回Thread类的map,也就是线程自己的map
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);//根据hash码招到数组下表

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				//如果key相等就覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
				//如果k是空,用心的key和value覆盖,同时清理历史key==null的数据
                if (k == null) {
                    //replaceStaleEntry 替换过时的entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
    		//超过阈值就扩容函数,不一定扩容,要清理一次旧数据,清理万如果还是大于等于四分之三阈值就扩容。
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}
//清理一部分脏key,n/2+1表示次数对吧。最少执行一次,对每个位置判断是否过期,如果过期,就清理,同时遍历所有与该节点冲突的点,如果脏key就直接全赋值null,否则就调整到对应下标(expungeStaleEntry方法)。
private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

private void rehash() {
 2             expungeStaleEntries();// 清理一次陈旧数据
 3 
 4             // 清理完陈旧数据,如果>= 3/4阀值,就执行扩容,避免迟滞
 5             if (size >= threshold - threshold / 4)
 6                 resize();
}


static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
}

问题:

1.ThreadLocal中的ThreadLocalMap采用什么方法,解决hash冲突

开放定址法:容易产生堆积问题,具体内容是,当发生冲突的时候,通过某种方式招到数组中的空位,将数据填入

。具体实现有线性探测,平方探测(1,-1,4,-4),Threadlocal使用的是线性探测。

2.都呢些地方去清除过期的key了。

  • resize扩容的时候,如果发现key=null,会让value=null
  • set的时候,有采样去除过期key,以及全量清除过期key,还有如果在放值的时候遇到过期key,直接覆盖。

3.扩容因子是多少?,为什么rehash中使用更小的阈值

初始化中默认是2/3,set中使用的是他的四分之三。,应该是这样,因为过期对象被删除了,这样一来,实际存储的对象个数就减少了,本来是阈值的,但是删除之后可能达不到了,所以要在更小的时候扩容,防止扩容的来的太慢,也就是避免滞后性。因为可能已经很多 冲突了,但是删除了过期key,达不到扩容要求,然后set的时候又冲突,但是又有过期key,也就是冲突可能已经很频繁了,但是因为过期的存在,迟迟不能扩容,所以要在更小的阈值扩容,防止滞后性的发生。

4.怎么解决内存泄漏?

  • 当我们用完threadlocal的时候调用remove方法去删除threaadlocal。就像连接必须关闭一样。

5.什么是内存泄漏?

在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。

常见的内存泄漏的原因:

  • 各种连接,如果不关闭,就会造成内存泄漏

  • 静态集合类,容器中的对象在程序结束之前不能被释放

  • 改变hash值,如果一个对象放到set集合中,然后害修改了hash值字段,那么就没法删除了

  • 如果一个变量定义的范围大于使用的范围,就可能出现内存泄漏。T

6.threadlocal怎么产生内存泄漏的?

  • 首先threadlocal创建的时候是强引用指向,然后map里面也是强的,这样如果不用了,前面那个强引用置空,但是map中依然是强引用,这样就泄露了,所以,tl采用了弱引用的key,也就是如果外面那个强引用如果不指向了,那么内部的key指向的tl对象会被gc,但是还有问题,就是key对应的value存在内存泄漏,所以在set方法,扩容方法,都有删除过期key的操作,就是防止内存泄漏。
  • 综上,目前的tl只有一种内存泄漏,就是当key过期的时候,value没有释放,这个时候就要考虑,怎么去避免这种情况,也就是怎么去解决内存泄漏。:首先我们如果不用tl的时候,一定要使用remove去删除过期key,就向使用连接一定要关闭连接一样。其实我们做的就这一点。

7.谈一谈threadlocal?

ThreadLocal是用来提供线程局部变量的,她通过为每个线程提供一个独立的变量副本解决了变量访问并发冲突的问题。

她在内部维护了一个Threadlocalmap,这个threadlocalmap是每个线程独有的,里面存储的是entry对象,entry对象有两个字段,key和value,key是存储Threadlocal实例的弱引用的,value存的是那个并发访问的变量。这个key是弱引用,所以在垃圾回收的时候,如果没有外面的强引用,那么他也就被回收了,这样就产生了key为null 的entry,也就产生了内存泄漏。所以我们用完的时候,一定要手动remove,防止内存泄漏

在threadlocal的,resize,set和remove方法,都有清除过期key的行为。

8.为什么重写equals一定要重写hashcode

https://blog.csdn.net/xl_1803/article/details/80445481

countdownlatch(闭锁)

redis集群,mysql,sql语句,spring,代码题,rocketmq,cdn,

countdown是准备工作的意思。

首先能join一定可以用他,但是join不能精准控制,例如如果要求某些线程执行一部分,就可以执行另一个线程,join就控制不了

闭锁:可以延迟线程的进度直到线程线程到达终止状态。一个闭锁工作起来就像是一道大门:直到闭锁达到终点状态之前,门一直是关闭的,没有线程能够通过,在终点状态到来的时候,所有线程都可以通过。

它允许一个或多个线程一直等待,直到其他线程执行完后再执行。

应用场景:

  1. 确保一个服务不会开始,直到它依赖的其他服务都已经开始

注意:

  • 只能用一次,
  • 构造函数必须指定计数器的值,也就是多少个线程执行cuntdown之后闭锁就把阻塞线程全部唤醒

补充:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yMFyRTDj-1626533466010)(D:\2021年秋招历程\新建文件夹\image\image-20210514215717282.png)]

countDown源码分析:

public void countDown() {
        sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
}

//通过自旋使得state-1  如果减1之后等于0返回true,否则就返回false
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)  return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
           return nextc == 0;
     }
}

//实现思路就是从头到尾的遍历列队中所有的节点为shared状态的
private void doReleaseShared() {
        //死循环
        for (;;) {
            //获取当前列队的头节点
            Node h = head;
            //列队中可能为空列队,也有可能只有一个node节点
            if (h != null && h != tail) {
                //获取头节点的状态
                int ws = h.waitStatus;
                //如果头节点为SIGNAL状态, 说明后继节点需要唤醒
                if (ws == Node.SIGNAL) {
                    //将头结点的waitstatue设置为0,以后就不会再次唤醒后继节点了。
                    //这一步是为了解决并发问题,保证只unpark一次!!不成功就继续
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //(释放)唤醒头节点的后继节点
                    unparkSuccessor(h);
                }// 状态为0并且不成功,继续
                else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;// loop on failed CAS
            }
            // 若头结点改变,继续循环 
            if (h == head) // loop if head changed
                break;
        }

摩尔投票法

摩尔投票法又叫多数投票法。

**解决的问题
**如何在任意多的候选人中,选出获得票数最多的那个

算法包括两个阶段

\1. 对抗阶段:分属两个候选人的票数两两对抗抵消
\2. 计数极端:计算对抗结果中最后留下的候选人票数是否有效

代码框架

for n in nums:
if count == 0:
major = n
if n == major:
count++
else count–

复杂度
线性时间复杂度,常数级空间复杂度

**题目
**Leetcode #169 多数元素
Leetcode #面试题 17.10 主要元素
Leetcode #229 求众数II

总结
\1. 如果至多选一个代表,那么他的票数要至少超过1/2的票数
\2. 如果至多选两个代表,票数至少要超过1/3的票数
\2. 如果至多选m个代表,那他们的票数至少要超过1/(m+1)

排序算法大总结

乐观锁的实现方式:

1.版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

2.CAS (比较和交换)

  • 需要读写的内存位置V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个 native 原子操作)。一般情况下,这是一个自旋操作,即不断的重试

乐观锁的缺点:

  • ABA问题

  • 循环开销

  • 只能保证一个共享变量的原子操作

  • jps查看所有java进程

  • jstack pid 查看某个进程的所有线程状态

  • jconsole 查看某个java进程中线程的运行情况 图形界面

  • sleep线程被interpret之后就不执行后面的代码而且线程进入TERMINATED状态(亲测)

  • park线程被interpre之后会执行后面的代码,当然也是最后终止状态

  • 操作系统层面有五种线程状态:新建 就绪 运行 终止 阻塞

  • java层面一共有六种

    • 新建状态
    • RUNNABLE :就绪 阻塞(有包括blocked,waiting,timed_waitting) 运行,其实就绪和运行就叫runnable,但是阻塞不会显示 会具体显示
    • 终止
  • Sleep和wait

    • sleep随便用,是Thread的方法,而wait必须用在同步代码块中,因为必须获取对象所,因为她是放在monitor的wiatlist中的,而且需要被唤醒才能进入entryset(blocked)队列。
  • 什么时候进行轻量级锁升级:如果一个线程已经拥有了该轻量级锁,那么其他线程来竞争使用cas,会失败,这样就会锁升级位重量级锁。

  • 偏向锁的撤销:

    • 当调用对象的hashcode方法的时候会撤销,因为偏向锁存放的是线程id,如果调用hashcode会导致偏向锁被撤销
    • 其他线程也用该偏向锁对象的时候,就会导致偏向锁
    • 调用wait和notify的时候,因为这是重量级锁特有的
  • 批量冲偏向和批量撤销都是针对类的,不是针对对象的

    • 批量重偏向:如果撤销偏向锁的个数超过20(其实就是20个升级位轻量级锁了),那么就会把剩余对象锁的偏向修改为当前线程,这些对象都是同一个类的哦。
    • 批量撤销:当撤销次数(撤销在上面那三种),超过40次,就会把整个类的所有对象弄成不可偏向,而且新建的对象也是不可偏向的。
    • 总结:就是随着偏向锁升级清凉锁的个数增加(或者叫做撤销次数),那么会先批量重定向 再批量撤销。

int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

//实现思路就是从头到尾的遍历列队中所有的节点为shared状态的
private void doReleaseShared() {
//死循环
for (;😉 {
//获取当前列队的头节点
Node h = head;
//列队中可能为空列队,也有可能只有一个node节点
if (h != null && h != tail) {
//获取头节点的状态
int ws = h.waitStatus;
//如果头节点为SIGNAL状态, 说明后继节点需要唤醒
if (ws == Node.SIGNAL) {
//将头结点的waitstatue设置为0,以后就不会再次唤醒后继节点了。
//这一步是为了解决并发问题,保证只unpark一次!!不成功就继续
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//(释放)唤醒头节点的后继节点
unparkSuccessor(h);
}// 状态为0并且不成功,继续
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;// loop on failed CAS
}
// 若头结点改变,继续循环
if (h == head) // loop if head changed
break;
}




# 摩尔投票法

**摩尔投票法又叫多数投票法。**

**解决的问题
**如何在任意多的候选人中,选出获得票数最多的那个

**算法包括两个阶段**

\1. 对抗阶段:分属两个候选人的票数两两对抗抵消
\2. 计数极端:计算对抗结果中最后留下的候选人票数是否有效

**代码框架**

> for n in nums:
>   if count == 0:
>     major = n
>   if n == major:
>     count++
>   else count--

**复杂度**
线性时间复杂度,常数级空间复杂度

**题目
**Leetcode #169 多数元素
Leetcode #面试题 17.10 主要元素
Leetcode #229 求众数II

**总结**
\1. 如果至多选一个代表,那么他的票数要至少超过1/2的票数
\2. 如果至多选两个代表,票数至少要超过1/3的票数
\2. 如果至多选m个代表,那他们的票数至少要超过1/(m+1)

# 排序算法大总结



# 乐观锁的实现方式:

## 1.版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

## 2.CAS  (比较和交换)

- 需要读写的内存位置V
- 进行比较的值 A
- 拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(**比较和替换是一个 native 原子操作**)。一般情况下,这是一个**自旋操作**,即**不断的重试**。

**乐观锁的缺点:**

- ABA问题
- 循环开销
- 只能保证一个共享变量的原子操作





- jps查看所有java进程
- jstack pid  查看某个进程的所有线程状态
- jconsole 查看某个java进程中线程的运行情况  图形界面
- sleep线程被interpret之后就不执行后面的代码而且线程进入TERMINATED状态(亲测)
- park线程被interpre之后会执行后面的代码,当然也是最后终止状态
- 操作系统层面有五种线程状态:新建  就绪 运行 终止 阻塞 
- java层面一共有六种
  - 新建状态
  - RUNNABLE  :就绪   阻塞(有包括blocked,waiting,timed_waitting)    运行,其实就绪和运行就叫runnable,但是阻塞不会显示  会具体显示
  - 终止
- Sleep和wait
  - sleep随便用,是Thread的方法,而wait必须用在同步代码块中,因为必须获取对象所,因为她是放在monitor的wiatlist中的,而且需要被唤醒才能进入entryset(blocked)队列。
- 什么时候进行轻量级锁升级:如果一个线程已经拥有了该轻量级锁,那么其他线程来竞争使用cas,会失败,这样就会锁升级位重量级锁。
- 偏向锁的撤销:
  - 当调用对象的hashcode方法的时候会撤销,因为偏向锁存放的是线程id,如果调用hashcode会导致偏向锁被撤销
  - 其他线程也用该偏向锁对象的时候,就会导致偏向锁
  - 调用wait和notify的时候,因为这是重量级锁特有的
- 批量冲偏向和批量撤销都是针对类的,不是针对对象的
  - 批量重偏向:如果撤销偏向锁的个数超过20(其实就是20个升级位轻量级锁了),那么就会把剩余对象锁的偏向修改为当前线程,这些对象都是同一个类的哦。
  - 批量撤销:当撤销次数(撤销在上面那三种),超过40次,就会把整个类的所有对象弄成不可偏向,而且新建的对象也是不可偏向的。
  - 总结:就是随着偏向锁升级清凉锁的个数增加(或者叫做撤销次数),那么会先批量重定向 再批量撤销。
- 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值