多线程(二)---高阶篇

一.CAS

1.概述

(1)技术背景
线程执行的任务,任务量比较小的时候,线程安全需要使用synchronized(多个线程同时竞争对象锁)加锁,效率比较低(竞争失败的线程很快的在阻塞态和被唤醒状态之间切换)
(2)使用前提
代码执行速度非常快
(3)目的
在安全的前提下优化效率—使用较多的场景:对变量修改保证线程安全
(4)原理
使用CAS,不造成线程阻塞(一直处于运行态)

2.什么是CAS

全称Compare and swap,即比较并交换
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号,可见CAS是一个乐观锁
实现源码

public final class Unsafe{
     
public final int getAndSetInt(Object var1,int var4){
     
	int vars;
	do{
     
		var5 = this.getInVolatile(var1,var2);
	}while(this.compareAndSwapInt(var1,var2,var5,var4));
	return vars;

注意:

  • native方法
  • 非阻塞方法
  • 给定三个参数:原始值,预期值,修改值,版本号
    原始值:主内存中变量真实的值
    预期值:拷贝到工作内存中的值
    修改值:n++就是1
    多线程(二)---高阶篇_第1张图片

第一个线程修改成功:主存变量值=1,
第二个线程修改:失败,compareAndSwapXXX()不阻塞,直接返回false,再次执行do代码—>重新获取最新的变量值,再修改
基于原始值+修改值 尝试修改操作,判断是否等于预期值,不满足就是不修改并返回false,满足则直接修改

3.存在的问题

ABA问题
**产生的原因:**当前线程拷贝主存值到工作内存进行修改的时间段,其他线程把主存中变量值A—>B—>A相当于已经被其他线程修改过了,但是在这个期间我们不清楚这个过程,还在改…
解决方案:采取乐观锁的设计,引入版本号做控制

4.jdk中,采用CAS实现的API:

  • java.util.concurrent.atomic,原子性的并发包下的api
  • synchronized中,多个线程不同时间点执行同步代码块时,jdk优化会采取CAS
  • 其他的,如1.8版本ConcurrentHashMap实现,put操作时,结点为空,采取CAS

5.CAS的实现

自旋尝试设置值的操作

二.常见锁策略

设计上解决线程安全的一种思想

1.乐观锁VS悲观锁

设计上解决线程安全的一种思想
(1)乐观锁
设计上总是乐观的认为数据修改大部分场景都是线程并发修改,少量情况下才存在,线程安全上采取版本号来控制—用户自己判断版本号,并处理
(2)悲观锁
悲观的认为总是有其他线程并发修改,每次都是加锁操作,
悲观锁存在的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程,所以性能不高
乐观锁存在的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度

2.自旋锁(Spin Lock)

实现原理:

  • 循环死等
  • 可中断的方式—>interrupt
  • 判断循环次数,达到阈值退出
  • 判断循环的总耗时,达到阈值退出
while(抢锁(lock) == 失败){
     
}

缺点

  • 前提:很快执行,如果满足不了,线程就一直处于运行态循环执行CAS,性能消耗比较大
  • 线程数量比较多,导致前提可能满足不了.或者CPU在很多线程之间切换—>性能消耗比较大

3.读写锁

技术背景:
数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥,如果两种场景下都用同一个锁,就会产生极大的性能消耗,读写锁因此而产生
读写锁:在执行加锁操作时需要额外表名读写意图,复数读者之间并不互斥,则写者则要求任何人互斥

4.可重入锁

允许同一个线程多次获取同一把锁,比如一个递归函数里有加锁操作,递归过程中这个锁不会阻塞自己,称为可重入锁(可重入锁也叫递归锁)
java中只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有线程的lock实现类,包括synchronized关键字锁都是可重入的

三.synchronized原理(重点)

1.实现原理

通过对象头加锁操作
monitor机制:编译为字节码时,生成monitorenter,monitorexit

2.对象头锁状态

(1)无锁
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功
(2)偏向锁
偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放
偏向锁的撤销:
需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁
如果线程处于活动状态,升级为轻量级锁的状态
(3)轻量级锁
当锁是偏向锁的时候,被第二份线程B访问,此时偏向锁就会升级为轻量级锁,线程B通过自旋的形式尝试获取锁,线程不会阻塞.从而提高性能
当前只有一个等待线程,则该线程将通过自旋进行等待,但是当自旋超过一定次数时,轻量级锁便会升级为重量级锁,当有一个线程已经持有锁,另一个线程在自旋,而此时又有第三个线程来访问时,轻量级锁会升级为重量级锁
(4)重量级锁
当有一个线程获取锁后,其余所有等待获取该锁的线程都会处于阻塞状态
注意点:java虚拟机需要确保锁在正常执行路径,以及异常执行路径上都能够被解锁

3.JVM对synchronized的优化方案

根据不同场景,使用不同的锁机制
注意:以下锁策略的级别由低到高!!!
(1)偏向锁:

  • 针对同一个线程再次申请已持有的对象锁
  • 最乐观的一种锁:从始至终只有一个线程请求某一把锁
  • 实现原理:CAS

(2)轻量级锁:

  • 大概率是在同一个时间点,只有一个线程申请对象锁
  • 实现原理:CAS
  • 缺点:线程会阻塞,唤醒

(3)重量级锁:

  • 大概率是在同一个时间点,多个线程竞争同一个对象锁
  • 实现原理:使用操作系统的mutex锁
  • 缺点:涉及到操作系统的调度,用户态到内核态切换,开销非常大,线程会阻塞.唤醒

说明:synchronized锁只能升级不能降级(为了提高获得锁和释放锁的效率),
其他优化方案:

  • 锁粗化:一个线程对同一个对象锁反复获取释放的操作,中间没有其他受影响的代码时,可以合并为一个锁
public class Test{
     
private static StringBuffer sb = new StringBuffer();
	public static void main(String[] args){
     
		StringBuffer sb = new StringBuffer();
		sb.append("a");
		sb.append("b");
		sb.append("c");
	}
}
  • 锁消除:临界区代码中,没有对象逃逸出当前线程,说明本身是线程安全的操作,可以直接删除锁,不使用锁
public class Test{
     
	public static void main(String[] args){
     
		StringBuffer sb = new StringBuffer();
		sb.append("a").append("b").append("c");
	}
}

四.死锁

1.概述

死锁是这样一种情况,多个线程同时被阻塞,他们中的一个或者前部都在等待某个资源被释放,由于线程被无限期的阻塞.因此程序不可能正常终止

2.前提条件

同步的本质在于:一个线程等待另外一个线程执行完毕后才可以继续执行,但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁
说明:
至少有两个线程,互相持有对方申请的对象锁,造成相互等待,导致没法执行

四个必要条件

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
  • 请求和保持,即当资源请求在请求其他的资源的同时保持对原有资源的占有
  • 循环等待,即存在一个等待队列:p1占有p2的资源,p2占有p3的资源,p3占有p1的资源,这样就形成了一个等待环路

当上述四个条件都成立的时候,便形成死锁,打破任何一个条件,便可让死锁消失

3.后果

线程阻塞等待,无法向下执行

4.解决方案

  • 资源一次性分配(破坏请求与保持条件)
  • 可剥夺资源:在线程满足条件时,释放掉已有的资源
  • 资源有序分配:系统为每类资源赋予一个编号,每个线程按照标号请求资源,释放则相反

5.检测死锁的手段

  • 使用jdk的监控工具(jconsole.jstack查看线程状态)
  • 银行家算法

6.银行家算法

一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产。
银行家算法需要确保以下四点:
1、当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
2、顾客可以分期贷款, 但贷款的总数不能超过最大需求量;
3、当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
4、当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金。

五.Lock体系

1.简介

jdk提供的一种除Synchronized之外的加锁方式,定义了锁对象来进行操作
和synchronized相比:
失去了Synchronized关键字隐式加锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种Synchronized关键字所不具备的同步特性

Lock lock = new ReentrantLock();
lock.lock();//设置当前线程的同步状态,并在队列中保存线程及线程的同步状态
//设置同步成功,往下执行,失败,则阻塞在上行所示代码
try{
     
   ......
}finally{
     
	lock.unlock();
}

注意:synchronized同步块执行完成或者遇到异常时锁会自动释放,而Lock必须调用unlock()方法释放锁,因此在finally中释放锁

2.Lock锁的实现原理:AQS

AQS(AbstractQueueSynchronizer)
AQS: 队列式的同步器

  • 实现原理:双端队列保存线程及线程同步状态,并通过CAS提供设置同步状态的方法,如ReentranLock实现时,调用lock.lock()操作,会不停的设置线程同步状态
  • 关于队列:(1)双端的(2)AQS中保存了队列的头尾节点
  • AQS提供的模板方法可以分为3类
    1.独占式获取与释放同步状态
    2.共享式获取与释放同步状态
    3.查询同步队列中等待线程情况
    多线程(二)---高阶篇_第2张图片

3.Lock锁的特点

(1) 提供公平锁和非公平锁
是否按照入队的顺序设置线程的同步状态—多个线程申请加锁操作时,是否按照时间顺序来加锁
(2)AQS提供的独占式和共享式设置同步状态(独占锁,共享锁)
独占式:只允许一个线程获取到锁
共享式:一定数量的线程共享式获取锁
(3)带Reentrant关键字的Lock包下的API:可重入锁
允许多次获取同一个Lock对象的锁

4.Lock体系中提供的读写锁API:ReentranReadWriteLock

  • 使用场景:多线程执行某个操作时,允许读-读并发/并行执行,不允许读-写,写-写并发/并行执行,如多线程读写文件
  • 读锁和写锁之间锁只能降级不能升级(写锁—>读锁)
public class ReadWriteTest {
     
    private static ReadWriteLock LOCK = new ReentrantReadWriteLock();
    private static Lock READLOCK = LOCK.readLock();
    private static Lock writeLock = LOCK.writeLock();
    public static void readFile(){
     
        try {
     
            READLOCK.lock();
            //IO读文件
        } finally {
     
            READLOCK.unlock();
        }
    }
    public static void writeFile(){
     
        try {
     
            writeLock.lock();
            //IO写文件
        } finally {
     
            writeLock.unlock();
        }
    }
      public static void main(String[] args) {
     
        //20个线程读文件
        for(int i = 0;i < 20;i++){
     
            //
        }
        //20个线程写文件
        for(int i = 0;i < 20;i++){
     
            //
        }
    }


}

优势:针对读读并发执行,提高运行效率
condition:线程间通信

public static Lock LOCK = new ReemtrantLock();
public static Condition CONDITION = LOCK.newCondition();
public static void t3(){
     
	try{
     
		LOCK.lock()//=synchronized()加锁的代码
		while(库存达到上限){
     
			CONDITION.wait();//=wynchronized锁的对象.wait()
		}
		System.out.println("t3");
		CONDITION.signal();//=synchronized锁对象.notify();
		CONDITION.signalAll();//=synchronized锁对象.notifyAll()
		}finally{
     
		LOCK.unlock();
		}

六.AQS的实现/应用

1.CountDownLatch

  • 使用场景:在某个线程A某个地方,阻塞等待,直到一组线程执行完毕之后,再执行A后续的代码
  • 注意事项:只提供计数器减的操作

案例:入口线程执行t方法:入口线程阻塞等待,直到所有子线程执行完毕

public class Main {
     
    //第一种方式:yield()
    @Test
    public void t1(){
     
        for(int i = 0;i < 20;i++){
     
            new Thread(()->{
     
                System.out.println(Thread.currentThread().getName());
            }).start();
        }
        while(Thread.activeCount() > 1){
     
            Thread.yield();
        }
        System.out.println("执行完毕:"+Thread.currentThread().getName());
    }
    //第二种方式:join()
    @Test
    public void t2() throws InterruptedException {
     
        List<Thread> threads = new ArrayList<>();
        for(int i = 0;i < 20;i++){
     
            Thread t = new Thread(()->{
     
                System.out.println(Thread.currentThread().getName());
            });
            threads.add(t);
            t.start();
        }
        for(Thread t : threads){
     
            t.join();
        }
        System.out.println("执行完毕:"+Thread.currentThread().getName());
    }
    //第三种方式:CountDownLatch
    @Test
    public void t3() throws InterruptedException {
     
        CountDownLatch cdl = new CountDownLatch(20);//计数器的初始值
        for(int i = 0;i < 20;i++){
     
            Thread t = new Thread(()->{
     
                System.out.println(Thread.currentThread().getName());
                cdl.countDown();//计数器的值-1
            });
            t.start();
        }
        cdl.await();//当前线程阻塞等待,直到计数器的值等于0
        System.out.println("执行完毕:"+Thread.currentThread().getName());
    }
}

2.Semaphore

一个计数信号量,主要用于控制多线程对共同资源库访问的控制
(1)使用场景:

  • 和CountDownLatch一样的地方
  • 多线程有限资源的访问

(2)常见API

  • new Semaphore(int);//给定数量的初始值,无参的默认为0
  • release(int);//计数器值增加给定的数量
  • acquire(int);//当前线程获取Semaphore对象中的给定数量的资源,获取到:资源数量减少,当前线程往下执行,获取不到:当前线程阻塞等待
public class Semaphore {
     
    //阻塞等待一组线程执行完毕,再执行XX任务
    @Test
    public void t1() throws InterruptedException {
     
        java.util.concurrent.Semaphore s = new java.util.concurrent.Semaphore(0);
        for(int i = 0;i < 20;i++){
     
            Thread t = new Thread(()->{
     
                System.out.println(Thread.currentThread().getName());
                s.release();//释放资源量(无参的话就是1)
            });
            t.start();
        }
        s.acquire(20);//获取资源量,子线程执行完毕后,获取到,子线程执行不完,一直阻塞等待
        System.out.println("执行完毕:"+Thread.currentThread().getName());
    }
    //限制有限资源访问
    //模拟服务端接收客户端http请求:只1000个并发
    //在一个时间点,客户端任务数达到1000.再有客户端请求,将阻塞等待
    @Test
    public void t2() throws InterruptedException {
     
        java.util.concurrent.Semaphore s = new java.util.concurrent.Semaphore(1000);
        for(;;){
     
            Thread t = new Thread(()->{
     
                try {
     
                    s.acquire();//获取资源量,如果没有,一直阻塞等待
                    //模拟每个线程处理客户端http请求
                    System.out.println(Thread.currentThread().getName());
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }finally{
     
                    s.release();//释放资源量(无参的话就是1)
                }
            });
            t.start();
        }
    }
}

七.ThreadLocal

1.概念

ThreadLocal用于提供线程局部变量.,在多线程环境下可以保证各个线程里的变量独立于其他线程里的变量,也就是说ThreadLocal可以为每个线程创建一个[单独的变量副本],相当于线程的private static 类型变量
ThreadLocal的作用和同步机制有些相反,同步机制是为了保证多线程环境下数据的一致性,而ThreadLocal是保证了多线程环境下数据的独立性

2.使用场景

隔离线程间的变量.保证每个线程是使用自己线程内的变量副本

public class ThreadLocalTest {
     
    private static ThreadLocal<String> HOLDER = new ThreadLocal<>();

    @Test
    public void t1() {
     
        //都和线程绑定,里边的值每个线程间是隔离开的
//        HOLDER.get();//获取当前线程中,某个ThreadLocal对象的值
//        HOLDER.remove();//移除当前线程中,某个ThreadLocal对象的值
//        HOLDER.set("abc");//设置当前线程中,某个ThreadLocal对象的值
        try {
     
            HOLDER.set("abc");
            for (int i = 0; i < 20; i++) {
     
                final int j = i;
                new Thread(() -> {
     
                    try {
     
                        HOLDER.set(String.valueOf(j));
                        if (j == 5) {
     
                            Thread.sleep(500);
                            System.out.println(HOLDER.get());
                        }
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }finally{
     
                        HOLDER.remove();
                    }
                }).start();
            }
            while (Thread.activeCount() > 1) {
     
                Thread.yield();
            }
            System.out.println(HOLDER.get());
        } finally {
     
            HOLDER.remove();//只要在某个线程设置了ThreadLocal值,在线程结束前,一定要remove.
        }
    }
}


说明:只要在某个线程设置了ThreadLocal值,在线程结束前,一定要remove
代码推荐写法:
定义类变量:static ThreadLocal<保存的数据类型> ThreadLocal = new ThreadLocal<>();
ThreadLocal多个线程使用的都是同一个,但是里边的值是和线程绑定的,线程间不相干

当有线程设置值的时候,在线程结束前,remove
new Thread(()->{
     
	try{
     
	threadLocal.set(设置的值);
	}finally{
     
		threadLocal.remove();
	}
}).start();

3.原理

多线程(二)---高阶篇_第3张图片
Thread对象中都有自己的ThreadLocalMap.调用ThreadLocal对象设置值set(value),获取值操作get(),删除值操作remove(),都是对当前线程中ThreadLocalMap对象的操作,所以每个线程中变量是隔离开的

4.内存泄漏

多线程(二)---高阶篇_第4张图片

如果Entry没有继承弱引用类型(设置为键引用),导致在ThreadLocalMap的Set(),get()和remove()方法中,都有清除无效Entry的操作.这样做是为了降低内存泄漏发生的可能,Entry中的key使用了弱引用的方式,这样做是为了降低内存泄漏的概率,但不能完全避免内存泄漏假设Entry中的key没有使用弱引用的方式,而是使用了强引用:由于ThreadLocalMap的生命周期和当前线程一样长,那么当引用ThreadLocal的对象被回收后,由于ThreadLocalMap还持有ThreadLocal和对应value的强引用,ThreadLocalMap和对应的value是不会被回收的,这就导致了内存泄漏,所以Entry以弱引用的方式避免了ThreadLocal没有被回收而导致的内存泄漏,但是此时的value仍然是无法回收的,依然会导致内存泄漏,ThreadLocalMap已经考虑到这种情况,并且有一些防护措施:在调用ThreadLocalMap的get(),set()和remove()的时候都会清除当前线程ThreadLocalMap中所有key为null的value,这样可以降低内存泄漏发生的概率,所以我们在使用ThreadLocal的时候,每次用完ThreadLocal都调用remove()方法,清除数据,防止内存泄漏

如果Entry没有继承弱引用类型(设置为键引用),导致线程没有使用值时,也一直有引用指向V,产生内存泄漏
假设线程长时间没执行完,K是强引用:ThreadLocal对象一直不能回收,V也没法使用,导致内存泄漏
设置K为弱引用的好处:降低内存泄漏的风险
每次垃圾回收,只要没其他强引用指向ThreadLocal对象,就回收,ThreadLocalMap实现时.检查键为null的,把V变量设置为null.V指向的对象就没有了,可以回收

八.ConcurrentHashMap(重点)

1.HashMap

非线程安全的,JDK1.7基于数组+链表,JDK1.8基于数组+链表+红黑树
(1)JDK1.7:
transient Entry[] table多线程(二)---高阶篇_第5张图片

final float loadFactor;//负载因子
static clas Entry<K,V> implements Map.Entry<K,V>{
     
	final K key;
	V value;
	Entry<K,V> next;//链表(单向)
	int hash;
}
  • 多个构造方法,可以传入参数,指定初始容量(数组长度),也可以不指定,使用默认值(数组长度为16,负载因子为0.75)
  • 底层数据结构:数组+链表
  • 存放数据时,桶的数量达到当前数组长度*负载因子时,数组会进行成倍的扩容,而扩容非常消耗性能(rehash,复制数据等操作)
  • Hash冲突严重时,在桶上的链表会变得越来越长,查询效率就越来越低,时间复杂度为O(n)
  • 空间换时间:如果希望加快key查找的时间,还可以进一步降低负载因子,加大初始容量,以降低哈希冲突的概率
  • 多线程下的数据操作是不安全的,并且可能出现环形链表造成死循环(扩容时操作会改变链表中元素的顺序,从头部插入,多线程执行顺序的不确定性可能出现线程 1.将node1.next指向node2,而接着执行线程2将node2.next指向node1)

(2)jdk1.8
多线程(二)---高阶篇_第6张图片

static class Node<K,V> implements Map.Entry<K,V>{
     
	final int hash;
	final K key;
	V value;
	Node<K,V> next;
}
  • 底层数据结构为:数组+链表+红黑树
  • 存放数据时,链表长度达到阈值后,会转变为红黑树
  • 红黑树中的数据查询效率为O(logN)
  • 迭代器为强一致性迭代器:快速失败迭代器,创建迭代器iterator后,对元素更新,会抛出并发的修改异常

2.Hashtable

线程安全的,1.7和1.8都是数组+链表,全部方法都是基于synchronized加锁,效率非常低

3.ConcurentHashMap

线程安全的,并且很多场景下支持并发操作,提高了效率
(1)JDK1.7
多线程(二)---高阶篇_第7张图片

static final class Segment<K,V> extends ReentrantLock implements Serializable{
     
        transient volatile HashEntry<K,V>[] table;
        final float loadFactor;
        static final class HashEntry<K,V>{
     
            final int hash;
            final K key;
            volatile V value;
            volatile HashEntry<K,V> next;
        }
    } 
  • 底层数据结构还是数组+链表,HashEntry为链表的结点
  • 采用了segment分段锁技术,在多线程并发更新操作时,对同一个segment同步加锁,保证数据的安全,这样就可以基于不同segment进行并发写操作
  • 同步的实现方式是基于ReentrantLock锁机制(Segment继承自ReentranLock)
  • 和HashMap一样,同样存在hash冲突时,链表查询效率低的问题

(2)JDK1.8
多线程(二)---高阶篇_第8张图片

static class Node<K,V> implements Map.Entry<K,V>{
     
	final int hash;
	final K key;
	volatile V val;
	volatile Node<K,V> next;
}
  • 底层数据结构与HashMap1.8b你一样,都是基于数组+链表+红黑树
  • 支持多线程的并发操作,实现原理是:CAS+synchronized保证并发更新
  • put方法存放元素时:通过key对象的hashcode计算出数组的索引,如果没有Node,则使用CAS尝试插入元素,失败则无条件自旋直到插入成功;如果存在Node,则使用synchronized锁住该Node元素(链表/红黑树的头结点),再执行插入操作

(3)JDK1.7和1.8都存在的特性:

  • 键,值迭代器为弱一致性迭代器,创建迭代器后,可以对元素更新
  • 该操作没有加锁,value是volatile修饰的,保证了可见性,所以是安全的
  • 读写分离可以提高效率,多线程对不同的Node/Segment的插入/删除是可以并发,并行执行,对同一个Node/Segment的写操作是互斥的,读操作都是无锁操作,可以并发/并行执行

(4)总结:

  • jdk 1.7:
    基于数组+链表实现,本质上基于Segment分段锁技术,Segment继承了ReentrantLock,不同的Segment之间多线程可以并发操作,同一个Segment之间是使用Lock加锁
  • jdk1.8:
    基于数组+链表+红黑树,本质上使用Synchronized加锁实现线程安全,不同结点线程可以并发操作,不同结点线程可以并发操作,如put操作,同一个结点如果为空使用CAS,如果不为空,使用synchronized加锁
  • 1.7和1.8效果
    读写分离可以提高效率,多线程对不同的Node/Segment的插入/删除是可以并发,并行执行,对同一个Node/Segment的写操作是互斥的,读操作都是无锁操作,可以并发/并行执行

4.HashMap/HashTable/ConcurrentHashMap对比

多线程(二)---高阶篇_第9张图片

你可能感兴趣的:(多线程)