avatar

目录
Java面试之JUC

[toc]

Volatile

volatile是什么?为什么引入了volatile

volatile是java虚拟机提供的一种轻量级同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排(保证了有序性)

高并发下不用synchronized,因为并发性不好;用volatile和juc下面的类;

JMM抽象结构图描述

关于JMM: blog/2018/07/27/15326929363424/#2-3-JAVA内存模型(JMM)

结构图主要描述:

  • 主内存/工作内存/共享变量/拷贝副本
  • JMM是一种抽象概念,不真实存在,它描述的是一组规则/规范

JMM如何保证同步的?

  1. 解锁前,把工作内存的值刷到主内存
  2. 加锁时,从主内存读取最新值到工作内存
  3. 加锁解锁是同一把锁

JMM三大特性是什么?

  1. 可见性
  2. 原子性
  3. 有序性

三大特性使线程安全性获得保证

可见性是什么意思?

某线程如果修改了主内存的共享变量,对其他线程是可见的。

volatile保证可见性代码演示

demo

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.mxx.juc;

import java.util.concurrent.TimeUnit;

/**
* 可见性验证:
* - a线程修改变量值
* - 对b线程(main)是否可见
* 结论:
* - 不加volatile,a线程的修改对b线程不可见
* - 加volatile,a线程的修改对b线程可见
*/
public class VolatileDemo {

// int num = 1;
volatile int num = 1;
public void changeNum(){
this.num = 30;
}

public static void main(String[] args) {
// 线程 操作 资源类
VolatileDemo vd = new VolatileDemo();
new Thread(()->{
System.out.println("进入线程"+Thread.currentThread().getName()+",num="+vd.num);
// 休息,等待num被其他线程读取到
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}

vd.changeNum();
System.out.println("线程"+Thread.currentThread().getName()+"修改num,num="+vd.num);
},"a").start();

while (vd.num==1){
// 一直等待循环
}

System.out.println(Thread.currentThread().getName()+"线程结束,num="+vd.num);

// 加volatile:
//进入线程a,num=1
//线程a修改num,num=30
//main线程结束,num=30
}
}

原子性是什么意思?

某个线程在做具体业务时,要么整体成功,要么整体失败,保证数据一致性

volatile不保证原子性代码演示

demo

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.mxx.juc;
import java.util.concurrent.TimeUnit;
public class VolatileDemo {

// int num = 1;
volatile int num = 1;
public void addNum(){
num++;
}
/**
* 不保证原子性验证:
* - 20个线程,每个线程1000次,做+1操作
* - 20个线程全部跑完后,看main函数打印结果
* 结论:
* - 每次结果都不一样,小于20001
*
* @param args
*/
public static void main(String[] args) {
VolatileDemo vd = new VolatileDemo();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
vd.addNum();
}
},"线程"+i).start();
}
// 主线程+GC线程
while (Thread.activeCount()>2){
// 线程礼让
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"线程: num="+ vd.num);
}
}

volatile不保证原子性理论解释(num++为什么不安全)

num++出现问题底层逻辑(javap -c):

  1. 获得:各线程从主内存读num到工作内存
  2. 修改:各线程在各自工作内存做+1操作,工作内存中的操作互相不可见。
  3. 写回:线程在写回前被挂起了,写回的时候相互覆盖,造成数值丢失。

volatile不保证原子性问题怎么解决?

  1. 加synchronized,并发性能不好
  2. juc的atomic,比如AtomicInteger的getAndIncrement()

AtomicInteger保证原子性代码演示

demo

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
AtomicInteger num2 = new AtomicInteger(1);
public void addNumByAtomic(){
num2.getAndIncrement();
// num2.incrementAndGet();
}
/**
* volatile不保证原子性解决:
* - AtomicInteger
*/
public static void main(String[] args) {
// seeOkByVolatile();
VolatileDemo vd = new VolatileDemo();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
// 不安全
vd.addNum();
// 安全
vd.addNumByAtomic();
}
},"线程"+i).start();
}

// 主线程+GC线程
while (Thread.activeCount()>2){
// 线程礼让
Thread.yield();
}

System.out.println(Thread.currentThread().getName()+"线程: num="+ vd.num);
System.out.println(Thread.currentThread().getName()+"线程: num2="+ vd.num2);
// main线程: num=13808
// main线程: num2=20001
}

* 有序性之Happens-before原则

java内存模型一个列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性

更详细的:blog/2018/07/28/15327607157902/#4-4-有序性

Volatile通过什么实现可见性?

通过加入 内存屏障禁止重排序优化来实现

什么是指令重排?造成什么问题?

  1. Java内存模型中,为提高性能,允许编译器和处理器对指令进行重排序。
  2. 重排时会考虑到指令间的数据依赖性
  3. 不会影响单线程环境下程序执行
  4. 多线程下,线程交替执行,由于优化重排的存在,两线程使用的变量能否保证一致性是无法确定的。结果无法预测。

* 指令重排造成的不安全举例

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a=0;
boolean flag = false;

public void m1(){
a=1; // 1
flag=true; // 2
}

public void m2() {
if(flag){
a = a+5; // 3
System.out.println("revalue="+a);
}
}

解释:

  1. 线程1执行m1,线程2执行m2
  2. 在不重排时,一定是按照123步骤执行,结果为6
  3. 如果发生重排,比如1和2交换了顺序,当m1执行完2时,线程切换,执行m1,这时可以进入if函数,a结果为5

* 什么是内存屏障?

详细:blog/2018/07/28/15327607157902/#4-3-可见性

关键字:

  • load屏障/store屏障
  • volatile读/volatile写
  • 内存屏障还能强制刷出cpu缓存,保证数据最新

如何保证有序性?

保证有序性:volatile、synchronized、Lock

你在哪些地方用到了volatile?

  1. 单例模式在多线程下不安全
  2. 读写锁/手写缓存
  3. cas底层源码分析

单例模式在多线程下不安全代码演示

demo

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class SingletonDemo {

private static SingletonDemo instance = null;

private SingletonDemo() {
System.out.println(Thread.currentThread().getName()+"\t调用构造方法");
}

public static SingletonDemo getInstance() {
if(instance == null){
instance = new SingletonDemo();
}

return instance;
}
public static void main(String[] args) {
// 单线程下,单例模式正常。
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//main 调用构造方法
//true
//true
//true


// 多线程下,单例模式不行
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},"线程"+i).start();
}
//线程0 调用构造方法
//线程4 调用构造方法
//线程3 调用构造方法
//线程2 调用构造方法
//线程1 调用构造方法
}
}

! 单例模式在多线程下不安全解决方案?

DCL双端检索机制+volatile

为什么只用DCL不能保证线程安全?

简答:在创建对象过程中发生指令重排。检测到intance不为null但对象却没有完全创建成功。

双端检索机制:在加锁前和加锁后都进行一次判断

demo:

java
1
2
3
4
5
6
7
8
9
10
public static SingletonDemo getInstance() {
if(instance == null){
synchronized (SingletonDemo.class){
if(instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}

为什么以上代码不一定安全?

  1. 因为有指令重排序的存在
  2. 原因在于:某线程读到instance不为nul时,instance的引用对象可能还没有完成初始化(new SingletonDemo()做到一半)
    instance = new SingletonDemo()分为以下步骤:
    1)分配对象内存空间
    2)初始化对象
    3)设置instance指向内存地址,此时instance != nul
    其中步骤2 3可能发生重排,
  3. 因此多线程下,当线程a访问instance!=null时,instance实例却未必初始化完成(还没做2);此时切到线程b,线程b直接取intance实例,这个实例是未完成初始化的实例。因此线程不安全。

如何解决?

java
1
2
private static volatile SingletonDemo instance = null;
// 告诉编译器禁止指令重排

CAS

提问线路:CAS—> Unsafe—> CAS底层原理 —> 原子引用更新 —> 如何规避ABA问题

compareAndSet怎么用?

比较并交换(compareAndSet)

java
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* boolean compareAndSet(int expect, int update)
* - 如果主内存的值=期待值expect,就将主内存值改为update
* - 该方法可以检测线程a的操作变量X没有被其他线程修改过
* - 保证了线程安全
*/
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 10)+ "\t" + atomicInteger);
System.out.println(atomicInteger.compareAndSet(5, 20)+ "\t" + atomicInteger);
//true 10
//false 10
}

CAS底层原理简述?

  1. Compare-And-Swap。是一条CPU并发原语。(原语:操作系统范畴,依赖硬件,不被中断。)
  2. 功能是判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
  3. 功能描述:
    1. 判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
    2. cas有三个操作数,内存值V,旧预期值A,要更新的值B。仅当预期值A=内存值V时,才将内存值V修改为B,否则什么都不做。
  4. 自旋:比较并交换,直到比较成功
  5. 底层靠Unsafe类保证原子性。

getAndIncrement() 源码解析(用了cas保证线程安全)

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 参数说明:
* this: atomicInteger对象
* valueOffset:对象的内存地址
* unsafe:sun.misc.Unsafe类
* AtomicInteger中变量value使用volatile修饰,保证内存可见。
* 结论:底层依赖CAS操作/Unsafe类
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* compareAndSwapInt:即CAS
* while: 如果修改失败,会一直尝试修改,直到成功。
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

简述:

  1. 调用了Unsafe类的getAndAddInt
  2. getAndAddInt使用cas一直循环尝试修改主内存

对Unsafe的理解?

Unsave类

  1. 该类所有方法都是native修饰,直接调用底层资源。sun.misc包中。
  2. 可以像C的指针一样直接操作内存。java的CAS操作依赖Unsafe类的方法。

! CAS有哪些缺点?

  1. 循环时间长,开销大
    1. 如果cas失败,就一直do while尝试。如果长时间不成功,可能给CPU带来很大开销。
  2. 只能保证一个共享变量的原子操作
    1. 如果时多个共享变量,cas无法保证原子性,只能加锁,锁住代码段。
  3. 引出来ABA问题。

ABA问题

简述ABA问题和解决方案?

简述版:

ABA问题描述: 线程1做CAS操作将A改为B再改为A,而线程2再做CAS时修改成功了,这不符合设计思想。

怎么解决:AtomicStampReference时间戳原子引用

ABA问题描述?问题出在哪?

ABA问题描述:

  • 比如线程1从内存位置V中取出A,此时线程2也取出A。且线程2做了一次cas将值改为了B,然后又做了一次cas将值改回了A。此时线程1做cas发现内存中还是A,则线程1操作成功。这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的。

这个过程问题出在哪?

  • 如果只在乎结果,ABA不介意B的存在,没什么问题
  • 如果B的存在会造成影响,需要通过AtomicStampReference,加时间戳解决。

原子更新引用是啥?

AtomicStampReference,使用时间戳,解决cas中出现的ABA问题。

AtomicReference使用代码演示

demo

java
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 如果希望原子操作的变量是User,Book,此时需要使用AtomicReference类
*/
public static void main(String[] args) {
User z3 = new User("z3",18);
User l4 = new User("l4",19);
AtomicReference<User> atomicReference = new AtomicReference<>(z3);
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
//truecom.mxx.juc.User@4554617c
//false com.mxx.juc.User@4554617c

}

AtomicReference存在ABA问题代码验证

demo

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
AtomicReference atomicReference = new AtomicReference<Integer>(100);
/**
* ABA问题验证:
* 1--ABA
* 2--A,C
* @param args
*/
public static void main(String[] args) {
ABADemo abaDemo = new ABADemo();

new Thread(()->{
abaDemo.atomicReference.compareAndSet(100,101);
abaDemo.atomicReference.compareAndSet(101,100);
},"1").start();

new Thread(()->{
// 睡1s等线程1执行完ABA
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println(abaDemo.atomicReference.compareAndSet(100, 2020)+"\t"+abaDemo.atomicReference.get());
//true 2020

},"2").start();
}

AtomicStampReference解决ABA问题代码验证

解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA变化

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
AtomicStampedReference atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

public static void main(String[] args) {
// ABAProblem();
ABADemo abaDemo = new ABADemo();

new Thread(()->{
// 等线程2读到初始版本号的值
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println("线程1在ABA前的版本号:"+abaDemo.atomicStampedReference.getStamp());
abaDemo.atomicStampedReference.compareAndSet(100,101,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1);
abaDemo.atomicStampedReference.compareAndSet(101,100,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1);
System.out.println("线程1在ABA后的版本号:"+abaDemo.atomicStampedReference.getStamp());
},"1").start();

new Thread(()->{
// 存一下修改前的版本号
int stamp = abaDemo.atomicStampedReference.getStamp();
System.out.println("线程2在修改操作前的版本号:"+stamp);
// 睡1s等线程1执行完ABA
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println(abaDemo.atomicStampedReference.compareAndSet(100,2020,stamp,abaDemo.atomicStampedReference.getStamp()+1)+ "\t" + abaDemo.atomicStampedReference.getReference());
//线程2在修改操作前的版本号:1
//线程1在ABA前的版本号:1
//线程1在ABA后的版本号:3
//false 100

},"2").start();

}

集合类不安全

ArrayList线程不安全演示-什么故障?什么原因?怎么解决?

ArrayList底层是一个数组,默认大小10,超过就扩容,扩原值的一半10+5=15

线程不安全,因为add方法没有加锁。

不安全案例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ContainerNotSafeDemo {

/**
* 1. 故障
* java.util.ConcurrentModificationException 并发修改异常
*
* 2. 原因
* 线程并发修改导致,线程1正在写入,线程2抢占资源,导致数据不一致。
*
* 3. 解决
* 1 new Vector(); add方法加锁,线程安全,并发性差。
* 2 Collections.synchronizedList(new ArrayList<>()); 包装成安全的,还是加锁,并发性差。
* 3 new CopyOnWriteArrayList<>(); juc的类,写时复制
* 4. 优化
*/
public static void main(String[] args) {
// List<String> list = new ArrayList<>();
// List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
// List<String> list = new CopyOnWriteArrayList<>();

for (int i = 1; i <= 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(Thread.currentThread().getName()+"\t"+list);
},String.valueOf(i)).start();
}
}
}

CopyOnWriteArrayList原理?它有什么好?

参考:https://blog.csdn.net/linsongbin1/article/details/54581787

add源码:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean add(E e) {
// 1. 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 2. 拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 3. 新增元素到新数组
newElements[len] = e;
// 4. 将array引用指向新数组
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}

解释写时复制:

  1. 写操作时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。整个add操作都是在锁的保护下进行的
  2. 读操作时,如果写完成且引用指向新数组,则读到的是最新数据;否则读到的是原始数组数据。可见读操作是不加锁的

CopyOnWriteArrayList 缺点&使用场合

  1. 消耗内存。写操作,拷贝数组,消耗内存,数组大的话可能导致gc
  2. 不能实时读。拷贝新增需要时间,读到的可能是旧数据,能保证最终一致性,但不满足实时要求。

因此,适合读多写少的场景。

CopyOnWriteArrayList透露的思想

  1. 读写分离,提高并发
  2. 最终一致性
  3. 通过另辟空间,来解决并发冲突

集合类不安全之Set-演示/故障/解决

Set同理

HashSet > Collections.synchronizedSet() > CopyOnWriteArraySet

且CopyOnWriteArraySet底层还是用的CopyOnWriteArrayList

HashSet底层是HashMap, add(key,一个常量)

集合类不安全之Map-演示/故障/解决

Map类似

HashMap > Collections.synchronizedMap() > ConcurrentHashMap

HashMap底层实现原理-jdk7

可参考:

map的存储结构是什么?

  1. Hash表(数组+链表)
  2. key-value构成一个entry对象
  3. 数组容量:默认16,始终保持 2^n(为了存取高效,减少碰撞,数据分配均匀)

new HashMap<>()底层发生了什么?

创建了长度=16的 Entry table。

源码分析

  • capacity : 当前数组容量
  • loadFactor:负载因子,默认为 0.75。
  • threshold:扩容的阈值,等于 capacity * loadFactor。当元素个数超过这个值就触发扩容。
  • MAXIMUM_CAPACITY = 1 << 30:最大的容量为 2 ^ 30
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//调用无参构造器
public HashMap() {
//默认初始容量大小为16,默认的加载因子为0.75f
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量不能超过MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子不能小于等于0,或者加载因子不能是非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//设置加载因子
this.loadFactor = loadFactor;
//设置临界值
threshold = initialCapacity;
//伪构造,里面没有代码
init();
}

map.put(key1,value1)底层发生了什么?

  1. 求key1的hash值,调用hashCode(),该hash值用来计算Entry数组下标,得到存放位置。
  2. 如果该位置为空,则添加成功。
  3. 如果不为空,意味着该位置存在链表:
    1. 如果hash值与其他key不同,则添加成功。
    2. 如果key1的hash值有key与之相同,则调用equal(),继续比较:
      1. 如果equal返回false,则添加成功。
      2. 如果equal返回true,则使用value1替换旧值(修改操作)

map.get(key1)底层发生了什么?

  1. 根据key1计算hash,找到对应数组下标。
  2. 遍历该位置处的链表,找到key1.equal(key2)为true的entry,返回其value。

扩容的原理?

扩容后,数组大小为原来的 2 倍。

ConcurrentHashMap底层原理-jdk7

关键词:Segment数组/基于分段锁/提高并发

  1. 引入一个Segment数组。每个Segment单元都包含一个与HashMap结构差不多hash表

  2. 读取过程:

    1. 先取key的hash值,取高sshift位决定属于哪个Segment单元。
    2. 接着就是HashMap那一套
  3. Segment继承jucReetrantLock,上锁方便,即分段锁。因此segment[1]锁了,不影响其他Segment单元并发。

HashMap底层实现原理-jdk8

与jdk7的不同的地方:

  1. new HashMap()时,不创建长度为16的数组。

  2. 底层使用Node[], 而不是Entry[]

  3. 数组结构采用

    数组+链表+红黑树

    1. 触发时机:当某索引位置链表长度>8 且 数组长度>64时,次索引位置的链表改为红黑树
    2. 红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。
    3. 目的:加快检索速度

! ConcurrentHashMap底层原理-jdk8

参考:https://www.jianshu.com/p/5dbaa6707017

底层结构

  • 和 1.8 HashMap 结构类似,也是数组+链表+红黑树的。取消了Segment 分段锁

那如何保证线程安全的?

  • CAS + synchronized + volatile 来保证并发安全性,具体的如下

put方法逻辑

源代码解析:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//1. 计算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//2. 判断Node[]数组是否初始化,没有则进行初始化操作
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//3. 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
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
}
//4. 检查到内部正在扩容,就帮助它一块扩容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 如果该坐标Node不为null且没有正在扩容,就加锁,进行链表/红黑树 节点添加操作
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
//5. 当前为链表,在链表中插入新的键值对
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;
}
}
}
// 6.当前为红黑树,将新的键值对插入到红黑树中
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;
}
}
}
}
// 7.判断链表长度已经达到临界值8(默认值),当节点超过这个值就需要把链表转换为树结构
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容
addCount(1L, binCount);
return null;
}

解析:对当前的table进行无条件自循环直到put成功(cas自旋)

  1. 如果数组下标没有Node节点,就用CAS+自旋添加链表头节点。
  2. 如果有Node节点,就加synchronized,添加链表或红黑树节点。

get操作,由于数组被volatile修饰了,因此不用担心数组的可见性问题。

java
1
volatile Node<K,V>[] table;

Map类的各种对比

  • HashMap和ConcurrentHashMap对比
  • HashMap和HashTable的对比
  • HashTable和ConcurrentHashMap对比

Java锁

Java 15锁,列举一些?

1.公平锁 / 非公平锁
2.可重入锁 / 不可重入锁
3.独享锁 / 共享锁
4.互斥锁 / 读写锁
5.乐观锁 / 悲观锁
6.分段锁
7.偏向锁 / 轻量级锁 / 重量级锁
8.自旋锁

公平和非公平锁是什么?两者区别(优缺点)?两种锁举例?

是什么?

  • 公平锁:多个线程按照申请锁的顺序来获取锁。
  • 非公平锁:多个线程获取锁的顺序不是按照申请舒顺序来的,有可能后申请的线程比先申请的线程优先获取锁。
    高并发下,有可能造成优先级反转或者饥饿现象。

区别:

  • 公平锁:保证顺序(队列,FIFO),性能下降。
  • 非公平:先尝试直接占有锁,如果尝试失败,再采用类似公平锁的方式。优点在于吞吐量比公平锁大。

举例:

  • ReentrantLock可以指定创建公平锁或非公平锁,无参构造默认创建非公平锁。
  • synchronized是非公平的。

可重入锁是什么?与不可重入的区别?可重入锁举例?作用?实现原理?

是什么?

  • 也叫递归锁
  • 当一个线程获取某个对象锁后,可以再次获取同一把对象锁。
  • 即,线程可进入任何他所拥有的对象锁所同步着的代码块。

区别?

java
1
2
3
public synchronized  void m1(){
m1();
}
  • 可重入锁:某线程进入外层m1后,可以再次进入递归m1方法。也叫递归锁。
  • 不可重入锁:某线程进入外部m1后,不可再进如内部m1,必须等待锁释放。这里就造成了死锁。

举例:

  • ReentrantLock/synchronized就是典型的可重入锁

作用:

  • 避免死锁。案例:递归

实现原理:

  • 计数器:进入最外层计数器=1,每递归一次,计数器+1,每退出一层,计数器-1,直到计数器=0,说明退出了最外层,此时该线程释放锁对象,其他线程才能获取该锁。

可重入锁代码验证

synchronized

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class SynchronizedLockDemo {

public synchronized void m1(){
System.out.println(Thread.currentThread().getName() + "----m1----");
m2();
}

public synchronized void m2(){
System.out.println(Thread.currentThread().getName() + "----m2----");
}

/**
* 可以看出,m1,m2是同一把锁,只有线程释放最外层锁,其他线程才能占用该锁。
* @param args
*/
public static void main(String[] args) {
SynchronizedLockDemo rd = new SynchronizedLockDemo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
rd.m1();
},String.valueOf(i)).start();
}
}
//0----m1----
//0----m2----
//4----m1----
//4----m2----
//3----m1----
//3----m2----
//2----m1----
//2----m2----
//1----m1----
//1----m2----
}

ReentrantLock

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class ReentrantLockDemo {

Lock lock = new ReentrantLock();

public void m1(){
lock.lock();
// lock.lock();
try {

System.out.println(Thread.currentThread().getName() + "----m1----");
m2();

}finally {
lock.unlock();
// lock.unlock();
}
}

public void m2(){
lock.lock();
try {

System.out.println(Thread.currentThread().getName() + "----m2----");

}finally {
lock.unlock();
}
}

/**
* 锁要成对出现,否则:
* - 多一个lock.lock()会造成锁无法释放,程序卡住
* - 多一个lock.unlock()直接报错
*/
public static void main(String[] args) {
ReentrantLockDemo rd = new ReentrantLockDemo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
rd.m1();
},String.valueOf(i)).start();
}
//0----m1----
//0----m2----
//2----m1----
//2----m2----
//1----m1----
//1----m2----
//3----m1----
//3----m2----
//4----m1----
//4----m2----
}
}

自旋锁是什么?优点?缺点?

  • 自旋锁(spinlock)是指尝试获取锁的对象不会立即阻塞,而是采用循环的方式取尝试获取锁。好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

手写一个自旋锁

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class SpinLockDemo {

/**
* 手写自旋锁
* 自旋锁的核心:while+cas+原子引用线程
*
* A线程加锁,一顿操作5秒钟,解锁。B线程一直自旋等待A线程释放锁,然后获取锁。
* 打印结果:
* A尝试获取锁
* A成功获取锁
* A一顿操作5秒...
* B尝试获取锁
* A释放锁
* B成功获取锁
* B一顿操作
* B释放锁
*/
public static void main(String[] args) {
SpinLockDemo sDemo = new SpinLockDemo();

new Thread(()->{
sDemo.myLock();

System.out.println(Thread.currentThread().getName()+"一顿操作5秒...");
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}

sDemo.myUnLock();

},"A").start();

// 保证线程A先上的锁
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}

new Thread(()->{
sDemo.myLock();

System.out.println(Thread.currentThread().getName()+"一顿操作");

sDemo.myUnLock();

},"B").start();

}

AtomicReference atomicThreadRef = new AtomicReference<Thread>();
// 加锁
public void myLock(){
// 线程A进来,发现是null值,成功操作cas把自己放进去
// 线程B进来,发现不是null值,一直自旋等待线程A释放锁
System.out.println(Thread.currentThread().getName()+"尝试获取锁");
while (!atomicThreadRef.compareAndSet(null,Thread.currentThread())){

}
System.out.println(Thread.currentThread().getName()+"成功获取锁");
}

// 释放锁
public void myUnLock(){
// 线程A发现原子引用是自己,于是cas成功修改为null值,即释放锁
atomicThreadRef.compareAndSet(Thread.currentThread(),null);
System.out.println(Thread.currentThread().getName()+"释放锁");
}
}

独占锁和共享锁是什么?举例?优缺点比较?

是什么?

  • 独占锁:写锁,该锁只能被一个线程所持有。
  • 共享锁:读锁,该锁可被多个线程所持有。

举例

  • ReentrantLock和sychronized都是独占锁。
  • ReentrantReadWriteLock,其读锁是共享锁,写锁是独占锁。

优缺:

  • 共享锁保证并发读是非常高效的;读写、写读、写写过程是互斥的。

验证读写锁ReentrantReadWriteLock

并发读写不安全演示

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class ReadWriteUnsafeDemo {

Map<Integer,String> map = new HashMap<>();

public void myPut(Integer key, String value){
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入:"+key);
map.put(key ,value);
System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:"+key);
}

public void myGet(Integer key){
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取:"+key);
String value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:"+key+","+value);
}

/**
* 并发读写不安全演示
* 打印:
* 0 正在写入:0
* 2 正在写入:2
* 4 正在写入:4
* 3 正在写入:3
* 1 正在写入:1
* 3 写入完成:3
* 0 正在读取:0
* 4 写入完成:4
* 0 读取完成:0,0
* 2 写入完成:2
* 0 写入完成:0
* ...
* 结论:写入不是原子操作,线程不安全
*
* 只锁put不锁get会发生什么?
* 2 正在写入
* 0 正在读取
* 2 写入完成:2
* 造成写时读,不安全。没有保证写的原子性。
*/
public static void main(String[] args) {
ReadWriteUnsafeDemo demo = new ReadWriteUnsafeDemo();
for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myPut(tmp,String.valueOf(tmp));
},String.valueOf(i)).start();
}

for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myGet(tmp);
},String.valueOf(i)).start();
}
}
}

验证读写锁ReentrantReadWriteLock

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ReadWriteLockDemo {

// 缓存资源都加一下volatile,保证线程间可见
volatile Map<Integer,String> map = new HashMap<>();
ReadWriteLock rwLock = new ReentrantReadWriteLock();

public void myPut(Integer key, String value){

rwLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
map.put(key ,value);
System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:"+key);
}catch (Exception e){
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}

public void myGet(Integer key){

rwLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
String value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:"+value);
}catch (Exception e){
e.printStackTrace();
}finally {
rwLock.readLock().unlock();
}
}

/**
* 要求:
* - 读可并发
* - 写和任何操作互斥
* 核心:ReentrantReadWriteLock + volatile
* 打印:
* 2 正在写入
* 2 写入完成:2
* 3 正在写入
* 3 写入完成:3
* 4 正在写入
* 4 写入完成:4
* 4 正在读取
* 2 正在读取
* 1 正在读取
* 1 读取完成:1
* 3 正在读取
* 结论:读写锁保证了写原子性,读并发
*/
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myPut(tmp,String.valueOf(tmp));
},String.valueOf(i)).start();
}

for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myGet(tmp);
},String.valueOf(i)).start();
}
}
}

什么是乐观锁/悲观锁?举例?

悲观锁

  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现

乐观锁

  • 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,采用版本号+cas的方式去保证线程安全。乐观锁适用于多读写少的应用类型,这样可以提高吞吐量。atomic包的类就是基于cas实现乐观锁的。

什么是乐观读/悲观读?

  • 悲观读:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。
  • 乐观读:如果读多写少,就乐观的认为读写同时发生的情况少,因此不采用完全锁定的方式,而是采用cas实现乐观锁。

ReentrantReadWriteLock是乐观读还是悲观读?

读优先:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。

StempedLock作用?

参考:blog/2018/10/27/20181027234153307/#7-5-ReentrantLock与锁

它控制锁有三种模式(写、悲观读、乐观读)。

核心代码:

java
1
2
3
4
5
private final StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
// stamp=0表示没有写锁入侵,
long stamp = sl.readLock(); // 获取悲观读锁
long stamp = lock.writeLock();// 获得写锁
文章作者: Machine
文章链接: https://machine4869.gitee.io/2020/02/21/20200221192831555/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 哑舍
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论