JUC并发编程知识点总结

发布时间:2024年01月21日

JMM

Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

工作内存和主内存

Java内存模型规定所有变量都存储在主内存中,线程不能对主内存直接进行操作,只能加载到工作内存中,这样就会造成不可见性.

  • lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。

  • read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。

  • load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。

  • use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。

  • store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。

  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

不可见性

  • 一个线程对共享变量进行修改时,另一个线程不能够立刻看到,每个 CPU 内核都有自己的缓存,而缓存仅仅对它所在的处理器内核可见,CPU 缓存与内存的数据不容易保证一致。

乱序性

为了优化代码,将指令顺序改变,这样的操作会影响到运行结果

内存屏障

内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;由于内存屏障的作用,避免了volatile变量和其它指令重排序、实现了线程之间通信,使得volatile表现出了锁的特性。

非原子性

进程切换带来了非原子性问题

如何解决?

加锁

使用synchronized在a线程执行时加锁,其他线程就不能执行

public synchronized void increase() {
 ? ?counter++; ?
}

原子变量

在java.util.concurrent 包下面提供一些类,可以在不加锁的情况下实现++操作的原子性. 这些类称为原子类 AtomicInteger.

private AtomicInteger counter = new AtomicInteger(0);
 
public void increase() {
 ? ?counter.getAndIncrement();
}
?

unsafe

// val1: AtomicInteger对象本身,var2: 该对象值得引用地址,var4: 需要变动的数
public final int getAndSetInt(Object var1, long var2, int var4) {
 ? ?int var5;
 ? ?do {
 ? ? ? ?// var5: 用 var1 和 var2 找到的内存中的真实值
 ? ? ? ?var5 = this.getIntVolatile(var1, var2);
 ?  } while(!this.compareAndSwapInt(var1, var2, var5, var4));
?
 ? ?return var5;
}

var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据

CAS机制

CAS 是乐观锁的一种实现,他采用的是自旋的思想,是一种轻量级的锁机制。即每次判断我的预期值和内存中的值是不是相同,如果不相同则说明该内存值已经被其他线程更新过了,因此需要拿到该最新值作为预期值,重新判断。而该线程不断的循环判断是否该内存值已经被其他线程更新过了,这就是自旋的思想。底层是通过 Unsafe 类中的 compareAndSwapInt 等方法实现.CAS是一条CPU的原子指令(cmpxchg指令)不会造成数据不一致问题

缺点

  1. 采用了自旋锁方式,不断循环会导致cpu的消耗

  2. 造成ABA问题

这个问题通常发生在使用CAS操作的并发环境中,我们可以使用版本号的方式来解决这个问题,每次变量更新的时候版本号加1,那么A->B->A这个过程就会被检测出来。AtomicStampedReference就是用过这个原理来解决ABA问题的,它包含一个值和一个版本号,我们可以这样使用:

AtomicStampedReference<Integer> atomicRef = 
 ? ?new AtomicStampedReference<>(100, 0);
 
// 获取当前值和版本号 ?
int stamp = atomicRef.getStamp();
int value = atomicRef.getReference();
 
// 尝试设置新值和版本号
boolean success = atomicRef.compareAndSet(value, 101, stamp, stamp + 1);
if(success) {
 ? ?// 设置成功,获取新版本号
 ? ?stamp = atomicRef.getStamp(); 
} ? 

解决方法

使用版本号1A2B3A,JUC包中提供了AtomicStampedReference来解决,compareAndSet方法会检查版本号和以用,只有全部相同时,才会返回True

Atomic

AtomicInteger 原理:自旋锁 + CAS 算法

CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B)

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B

  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋

volatile

是Java虚拟机提供那个的轻量级的同步机制一旦一个共享变量被volatile修饰后,就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序

底层实现

内存屏障

强制让读写都访问主内存,读写操作时都会加入屏障

缓存一致性

某个处理器更新了共享变量之后,缓存一致性协议就会保证其他的处理器也能看到修改后的值,缓存一致性原则会在读操作前,写操作后都会刷新内存,保证访问的数据都是最新的

总结

  • volatile关键字主要保证可见性、有序性和禁止编译器优化。

  • volatile的底层原理是依赖内存屏障和缓存一致性协议实现的。

  • volatile不能保证原子性,要配合synchronized或Atomic类解决。

  • volatile的实现依赖JMM中的主内存、工作内存和内存屏障等概念。

锁分类

乐观锁和悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度

乐观锁

认为在对同一个数据的并发操作时,是不会发生修改的,其实就是不加锁在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。CAS的实现思想就是乐观锁

悲观锁

与乐观锁相反,认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。适用于写多读少的操作,当一个线程获得锁对象时,其他线程只能在外等待,直到第一个线程释放锁为止

可重入锁和不可重入锁

可重入锁

又名递归锁,在持有一个锁对象时,还可以对另一个同步方法进行操作,synchronized和ReentrantLock都是可重入锁,在一定程度上可以避免死锁的发生

public class AB{
 // 演示可重入锁
 ?synchronized void setA() throws Exception{ ? 
 ? ?Thread.sleep(1000);
 ? ?setB(); ? // 因为获取了setA()的锁(即获取了方法外层的锁)、此时调用setB()将会自动获取setB()的锁,如果不自动获取的话方法B将不会执行 ? ? ? ? ? 
  }
 
 ?synchronized void setB() throws Exception{
 ? ?Thread.sleep(1000);
  }
}

不可重入锁

线程对象的同步方法上锁后其他的同步方法需要排队等候,递归调用时会发生死锁

公平锁和非公平锁

公平锁

线程进入进行等待,队首的元素会获得锁对象,按照获得锁对象的顺序执行代码

优点:不会造成线程饿死的现象

缺点:除了第一个线程其余线程会造成阻塞现象

非公平锁(synchronized)

不按照请求锁的顺序分配,谁抢到谁获得 对于Synchronized而言,是一种非公平锁。并不像ReentrantLock是通过AQS的来实现线程调度,所以synchronized并没有任何办法使其变成公平锁。

共享锁和独占锁

共享锁

并发包中ReadWriteLock就是一个典型的共享锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。

独占锁

该锁一次只能被一个线程所持有,而其他线程就只能等待当前持有锁的线程释放锁才能再次获取锁,Synchronized、ReentrantLock都是独享锁,其实准确的说独占锁也是悲观锁,因为悲观锁也是其他线程只能等待持有锁的线程释放锁才能再次获取到锁资源。

读写锁

读读不互斥,读写和写写之间互斥,防止其他线程在此时写入数据,造成数据的脏读

分段锁

在jdk8之后去除了真正的分段锁,此处的分段锁只是一种思想,将锁的粒度更细化,例如concurrentHashMap中用hash表中的第一个结点当作锁

自旋锁

即当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。尝试获取锁的线程不会立即阻塞(放弃CPU时间片),采用循环的方式尝试获取锁!

对象结构

在 Hotspot 虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java 对象头是实现 synchronized 的锁对象的基础,一般而言,synchronized 使用的锁对象是存储在 Java 对象头里。它是轻量级锁和偏向锁的关键

对象头中有一块区域称为 Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID等等

Synchronized实现

  • synchronized作用在静态方法时,锁的是对象的Class实例,因为Class数据存在于元空间(JDK8前是永久代),因此静态方法锁相当于该类的一个全局锁

  • 当synchronized作用在某一个对象实例时,锁的是括号括起来的对象实例(XXX)

synchronized通过底层指令实现控制

  • 修饰方法时

    • 底层指令添加ACC_SYNCHRONIZED

    • 进入方法时使用monitorenter检测

    • 执行完毕使用monitorexit释放锁

  • 修饰代码块时

    • 进入方法时使用monitorenter检测

    • 执行完毕使用monitorexit释放锁

无锁/偏向锁/轻量级锁/重量级锁

在synchronized包裹起来的代码被称为同步块,在使用过程中,锁的状态会发生一系列升级,主要有四个方面

无锁

没有任何线程使用锁对象

偏向锁

一段代码一直被同一个线程访问,首先读取目标对象的MarkWord,判断是否为可偏向状态

  • 如果为可偏向状态, 则尝试用 CAS 操作, 将自己的线程 ID 写入MarkWord

  • 如果 CAS 操作失败, 则说明, 有另外一个线程 Thread B 抢先获取了偏向锁。 这种状态说明该对象的竞争比较激烈, 此时需要撤销 Thread B 获得的偏向锁,将 Thread B 持有的锁升级为轻量级锁。

  • 如果是已偏向状态, 则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID 。

    • 如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块

    • 如果不等, 则证明该对象目前偏向于其他线程, 需要撤销偏向锁

轻量级锁

当前锁的状态为偏向锁时,此时继续有线程去访问一个代码,线程尝试使用 CAS 操作将对象头中的 Mark Word 替换为指向锁记录的指针,如果CAS操作成功,如果成功,当前线程获得锁

重量级锁

当锁的状态为轻量级锁时,线程自旋获取锁的次数到达一定数量时,锁的状态升级为重量级锁,会让自旋次数多的线程进入到阻塞状态,因为访问大时,线程都自旋获得锁,cpu消耗大.

AQS

AbstractQueuedSynchronizer意为抽象同步队列,内部由32 bit int来维护同步状态,独占模式0表示未加锁,大于0表示加锁状态,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态

  • state 使用 volatile 修饰配合 cas 保证其修改时的原子性

CLH 是一种基于单向链表的高性能、公平的自旋锁,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配

ReentrantLock实现

锁对比

ReentrantLock 相对于 synchronized 具备如下特点:

  1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的

  2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同

  3. 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁

  4. 可中断:ReentrantLock 可中断,而 synchronized 不行

  5. 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁

    • ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的

    • 不公平锁的含义是阻塞队列内公平,队列外非公平

  6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列

    • ReentrantLock 可以设置超时时间,synchronized 会一直等待

  7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象,更细粒度的唤醒线程

  8. 两者都是可重入锁

使用锁

构造方法:ReentrantLock lock = new ReentrantLock();

ReentrantLock 类 API:

  • public void lock():获得锁

    • 如果锁没有被另一个线程占用,则将锁定计数设置为 1

    • 如果当前线程已经保持锁定,则保持计数增加 1

    • 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态

  • public void unlock():尝试释放锁

    • 如果当前线程是该锁的持有者,则保持计数递减

    • 如果保持计数现在为零,则锁定被释放

    • 如果当前线程不是该锁的持有者,则抛出异常

公平锁

基本使用

构造方法:ReentrantLock lock = new ReentrantLock(true)

public ReentrantLock(boolean fair) {
 ? ?sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 默认是不公平的:

public ReentrantLock() {
 ? ?sync = new NonfairSync();
}

说明:公平锁一般没有必要,会降低并发度

非公平锁

static final class NonfairSync extends Sync {
     ? ? ? //实现加锁 
         ? final void lock() {
 ? ? ? ? ? ?if (compareAndSetState(0, 1)) //先尝试获取锁,修改锁的状态
 ? ? ? ? ? ? ? ?setExclusiveOwnerThread(Thread.currentThread());
 ? ? ? ? ? ?else
 ? ? ? ? ? ? ? ?acquire(1);
 ? ? ?  }
         ? ?
    }


// ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
 ? ?return nonfairTryAcquire(acquires);
}
// 抢占成功返回 true,抢占失败返回 false
final boolean nonfairTryAcquire(int acquires) {
 ? ?final Thread current = Thread.currentThread();
 ? ?// state 值
 ? ?int c = getState();
 ? ?// 条件成立说明当前处于【无锁状态】
 ? ?if (c == 0) {
 ? ? ? ?//如果还没有获得锁,尝试用cas获得,这里体现非公平性: 不去检查 AQS 队列是否有阻塞线程直接获取锁 ? ? ? ?
 ?      if (compareAndSetState(0, acquires)) {
 ? ? ? ? ? ?// 获取锁成功设置当前线程为独占锁线程。
 ? ? ? ? ? ?setExclusiveOwnerThread(current);
 ? ? ? ? ? ?return true;
 ? ? ? ? } ? ?
    } ? ?
 ?  // 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示【发生了锁重入】
    else if (current == getExclusiveOwnerThread()) {
 ? ? ? ?// 更新锁重入的值
 ? ? ? ?int nextc = c + acquires;
 ? ? ? ?// 越界判断,当重入的深度很深时,会导致 nextc < 0,int值达到最大之后再 + 1 变负数
 ? ? ? ?if (nextc < 0) // overflow
 ? ? ? ? ? ?throw new Error("Maximum lock count exceeded");
 ? ? ? ?// 更新 state 的值,这里不使用 cas 是因为当前线程正在持有锁,所以这里的操作相当于在一个管程内
 ? ? ? ?setState(nextc);
 ? ? ? ?return true;
 ?  }
 ? ?// 获取失败
 ? ?return false;
}

JUC常用类

ConcurrentHashMap

三种集合:

  • HashMap 是线程不安全的,性能好

  • Hashtable 线程安全基于 synchronized,综合性能差,已经被淘汰

  • ConcurrentHashMap 保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好

    虽然HashTable是线程安全的但是只是单纯在put()方法上加锁,保证插入操作时阻塞其他线程的操作,效率低下,ConcurrentHashMap采用了cas+synchronized保证了多线程的线程安全,他的键值都不能为空(也包括HashTable)防止出现歧义

源码

//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
transient volatile Node<K,V>[] table;
final V putVal(K key, V value, boolean onlyIfAbsent) {
 ? ?// 【ConcurrentHashMap 不能存放 null 值】
 ? ?if (key == null || value == null) throw new NullPointerException();
 ? ?// 扰动运算,高低位都参与寻址运算
 ? ?int hash = spread(key.hashCode());
 ? ?// 表示当前 k-v 封装成 node 后插入到指定桶位后,在桶位中的所属链表的下标位置
 ? ?int binCount = 0;
 ? ?// tab 引用当前 map 的数组 table,开始自旋
 ? ?for (Node<K,V>[] tab = table;;) {
 ? ? ? ?// f 表示桶位的头节点,n 表示哈希表数组的长度
 ? ? ? ?// i 表示 key 通过寻址计算后得到的桶位下标,fh 表示桶位头结点的 hash 值
 ? ? ? ?Node<K,V> f; int n, i, fh;
 ? ? ? ?
 ? ? ? ?// 【CASE1】:表示当前 map 中的 table 尚未初始化
 ? ? ? ?if (tab == null || (n = tab.length) == 0)
 ? ? ? ? ? ?//【延迟初始化】
 ? ? ? ? ? ?tab = initTable();
 ? ? ? ?
 ? ? ? ?// 【CASE2】:i 表示 key 使用【寻址算法】得到 key 对应数组的下标位置,tabAt 获取指定桶位的头结点f
 ? ? ? ?else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
 ? ? ? ? ? ?// 对应的数组为 null 说明没有哈希冲突,直接新建节点添加到表中
 ? ? ? ? ? ?if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
 ? ? ? ? ? ? ? ?break;
 ? ? ?  }
 ? ? ? ?// 【CASE3】:逻辑说明数组已经被初始化,并且当前 key 对应的位置不为 null
 ? ? ? ?// 条件成立表示当前桶位的头结点为 FWD 结点,表示目前 map 正处于扩容过程中
 ? ? ? ?else if ((fh = f.hash) == MOVED)
 ? ? ? ? ? ?// 当前线程【需要去帮助哈希表完成扩容】
 ? ? ? ? ? ?tab = helpTransfer(tab, f);
 ? ? ? ?
 ? ? ? ?// 【CASE4】:哈希表没有在扩容,当前桶位可能是链表也可能是红黑树
 ? ? ? ?else {
 ? ? ? ? ? ?// 当插入 key 存在时,会将旧值赋值给 oldVal 返回
 ? ? ? ? ? ?V oldVal = null;
 ? ? ? ? ? ?// 【锁住当前 key 寻址的桶位的头节点】
 ? ? ? ? ? ?synchronized (f) {
 ? ? ? ? ? ? ? ?// 这里重新获取一下桶的头节点有没有被修改,因为可能被其他线程修改过,这里是线程安全的获取
 ? ? ? ? ? ? ? ?if (tabAt(tab, i) == f) {
 ? ? ? ? ? ? ? ? ? ?// 【头节点的哈希值大于 0 说明当前桶位是普通的链表节点】
 ? ? ? ? ? ? ? ? ? ?if (fh >= 0) {
 ? ? ? ? ? ? ? ? ? ? ? ?// 当前的插入操作没出现重复的 key,追加到链表的末尾,binCount表示链表长度 -1
 ? ? ? ? ? ? ? ? ? ? ? ?// 插入的key与链表中的某个元素的 key 一致,变成替换操作,binCount 表示第几个节点冲突
 ? ? ? ? ? ? ? ? ? ? ? ?binCount = 1;
 ? ? ? ? ? ? ? ? ? ? ? ?// 迭代循环当前桶位的链表,e 是每次循环处理节点,e 初始是头节点
 ? ? ? ? ? ? ? ? ? ? ? ?for (Node<K,V> e = f;; ++binCount) {
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 当前循环节点 key
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?K ek;
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?// key 的哈希值与当前节点的哈希一致,并且 key 的值也相同
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?if (e.hash == hash &&
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?  ((ek = e.key) == key ||
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? (ek != null && key.equals(ek)))) {
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 把当前节点的 value 赋值给 oldVal
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?oldVal = e.val;
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 允许覆盖
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?if (!onlyIfAbsent)
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 新数据覆盖旧数据
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?e.val = value;
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 跳出循环
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?break;
 ? ? ? ? ? ? ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?Node<K,V> pred = e;
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 如果下一个节点为空,把数据封装成节点插入链表尾部,【binCount 代表长度 - 1】
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?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;
 ? ? ? ? ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ? ? ? ?
 ? ? ? ? ? ?// 条件成立说明当前是链表或者红黑树
 ? ? ? ? ? ?if (binCount != 0) {
 ? ? ? ? ? ? ? ?// 如果 binCount >= 8 表示处理的桶位一定是链表,说明长度是 9
 ? ? ? ? ? ? ? ?if (binCount >= TREEIFY_THRESHOLD)
 ? ? ? ? ? ? ? ? ? ?// 树化
 ? ? ? ? ? ? ? ? ? ?treeifyBin(tab, i);
 ? ? ? ? ? ? ? ?if (oldVal != null)
 ? ? ? ? ? ? ? ? ? ?return oldVal;
 ? ? ? ? ? ? ? ?break;
 ? ? ? ? ?  }
 ? ? ?  }
 ?  }
 ? ?// 统计当前 table 一共有多少数据,判断是否达到扩容阈值标准,触发扩容
 ? ?// binCount = 0 表示当前桶位为 null,node 可以直接放入,2 表示当前桶位已经是红黑树
 ? ?addCount(1L, binCount);
 ? ?return null;
}

扰动函数:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,与树化节点和转移节点区分,把高低位都利用起来减少哈希冲突,保证散列的均匀性

static final int spread(int h) {
 ? ?return (h ^ (h >>> 16)) & HASH_BITS; // 0111 1111 1111 1111 1111 1111 1111 1111
}

CopyOnWriteArrayList

CopyOnWriteArrayList 采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,在新数组上执行操作,不影响其它线程的并发读,读写分离

  • 存储结构

private transient volatile Object[] array;  // volatile 保证了读写线程之间的可见性
  • 全局锁

final transient ReentrantLock lock = new ReentrantLock();

CopyOnWriteArraySet

CopyOnWriteArraySet 的实现基于 CopyOnWriteArrayList,不能存储重复数据

CountDownLatch

CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成,允许一个线程等待其他线程执行完成后再执行,内部采用AQS,每次state-1,当减到0时,闭锁线程才会执行

public class CountDownLatchDemo {
?
 ? ?public static void main(String[] args) throws InterruptedException {
?
?
 ? ? ? ?/*
 ? ? ? ? ? ?允许一个线程等待其他线程执行完毕后,在执行
 ? ? ? ? */
 ? ? ? ?CountDownLatch countDownLatch = new CountDownLatch(10);//设置线程总量
?
 ? ? ? ?for (int i = 0; i <10 ; i++) {
 ? ? ? ? ? new Thread(()->{
 ? ? ? ? ? ? ? try {
 ? ? ? ? ? ? ? ? ? Thread.sleep(1000);
 ? ? ? ? ? ? ? } catch (InterruptedException e) {
 ? ? ? ? ? ? ? ? ? e.printStackTrace();
 ? ? ? ? ? ? ? }
 ? ? ? ? ? ? ? System.out.println("aaaaaaaaaaaaa");
 ? ? ? ? ? ? ?countDownLatch.countDown();
 ? ? ? ? ? }).start();
 ? ? ?  }
 ? ? ? ? ?countDownLatch.await();
?
 ? ? ? ?System.out.println("main线程执行");//最后执行的内容
?
 ?  }
}

线程池

基本概述

线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作

线程池作用:

  1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务

  2. 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死

  3. 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务

池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销

ThreadPoolExecutor

在 JDK5 版本中增加了内置线程池实现 ThreadPoolExecutor,同时提供了Executors 来创建不同类型的线程池。Executors 中提供了以下常见的线程池创建方法:

  • newSingleThreadExecutor:一个单线程的线程池。如果因异常结束,会再创建一个新的,保证按照提交顺序执行。

  • newFixedThreadPool:创建固定大小的线程池。根据提交的任务逐个增加线程,直到最大值保持不变。如果因异常结束,会新创建一个线程补充。

  • newCachedThreadPool:创建一个可缓存的线程池。会根据任务自动新增或回收线程

虽然在 JDK 中提供 Executors 类来支持以上类型的线程池创建,但通常情况下不建议开发人员直接使用(见《阿里巴巴 java 开发规范》并发处理)

七个参数

  • corePoolSize:核心池数量

  • maximumPoolSize:最大线程数

  • keepAliveTime:除去核心线程外,如果一个线程的空闲时间大于keepAliveTime将会被销毁

  • unit:时间参数

  • workQueue:阻塞队列

  • threadFactory:线程工厂主要用于创建线程

  • handler:拒绝策略

执行流程

线程池队列

ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,创建时必须设置长度,,按 FIFO 排序量。 LinkedBlockingQueue:基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置是一个最大长度为 Integer.MAX_VALUE

拒绝策略

  1. AbortPolicy:抛出异常

  2. CallerRunsPolicy:由提交任务的线程执行(main线程)

  3. DiscardOlddestPolicy:丢弃最老的一个线程

  4. DiscardPolicy:直接丢弃之后的请求

execute 与 submit 的区别

excute

实现Runnable接口,没有返回值

submit

实现Runnable接口和Callable接口,有返回值

关闭线程池

shutdown

不会接收新的任务,等待所有的线程执行完毕后再关闭

shutdownNow

对正在执行的任务全部发出 interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。

ThreadLocal

意为本地线程变量,意思是 ThreadLocal 中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量

static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
 ? ?@Override
 ? ?protected Integer initialValue() {
 ? ? ? ?return 1;
 ?  }
};
public void set(T value) {
 ? ?// 获取当前的Thread线程
 ? ?Thread t = Thread.currentThread();
 ? ?// 获取当前线程的ThreadLocalMap
 ? ?ThreadLocalMap map = getMap(t);
 ? ?if (map != null)
 ? ? ? ?// 将当前ThreadLocal作为key,保存到ThreadLocalMap中
 ? ? ? ?map.set(this, value);
 ? ?else
 ? ? ? ?// 初始化ThreadLocalMap
 ? ? ? ?createMap(t, value);
}
?
public T get() {
 ? ?// 获取当前线程
 ? ?Thread t = Thread.currentThread();
 ? ?// 获取当前线程的ThreadLocalMap
 ? ?ThreadLocalMap map = getMap(t);
 ? ?if (map != null) {
 ? ? ? ?// 利用当前ThreadLocal作为key获取value
 ? ? ? ?ThreadLocalMap.Entry e = map.getEntry(this);
 ? ? ? ?if (e != null) {
 ? ? ? ? ? ?@SuppressWarnings("unchecked")
 ? ? ? ? ? ?T result = (T)e.value;
 ? ? ? ? ? ?return result;
 ? ? ?  }
 ?  }
 ? ?// 初始化ThreadLocalMap或者直接通过ThreadLocalMap设置value
 ? ?return setInitialValue();
}
?
public void remove() {
 ? ?// 获取当前线程的ThreadLocalMap
 ? ? ThreadLocalMap m = getMap(Thread.currentThread());
 ? ? if (m != null)
 ? ? ? ? // 移除当前ThreadLocalMap中的ThreadLocal对象
 ? ? ? ? m.remove(this);
 }
?

内存泄漏

TreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被 GC 回收,这样就会导致ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,只有 thead 线程退出以后,value 的强引用链条才会断掉。

文章来源:https://blog.csdn.net/weixin_64704029/article/details/135721286
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。