JAVA学习笔记 -- JUC并发编程

本文围绕Java多线程与并发编程展开,介绍了进程、线程的概念、创建与运行,以及线程状态和常见方法。还阐述了共享模型之管程,包括线程安全、Monitor和synchronized优化原理。此外,讲解了多种设计模式和并发工具,如线程池、JUC等,以及线程安全集合类的原理。

1 进程、线程

进程就是用来加载指令、管理内存、管理IO。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程可以被视为程序的一个实例。

线程,一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行

java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器

在这里插入图片描述

并行、并发

线程轮流使用CPU的做法 称为并发(concurrent)

多核cpu下,每个 核(core)都可以调度运行线程,这时候线程可以是并行的

在这里插入图片描述

异步、同步
从方法调用角度:

· 需要等待结果返回,才能继续运行 即 同步
· 不需要等待结果返回,就能继续运行 即 异步

(同步在多线程中:让多个线程步调一致 )

1、单核cpu下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu,不至于一个线程总占用cpu,别的线程无法干活

2、多核cpu可以并行跑多个线程,但能否提高程序运行效率还是要分情况

3、IO操作不占用cpu,只是我们一般拷贝文件使用的是【阻塞IO】,这时相当于线程虽然不用cpu,但需要一直等待IO结束,没能充分利用线程

1.1 创建线程

方法一:直接使用Thread
在这里插入图片描述
方法二:使用Runnable配合Thread

在这里插入图片描述

方法三:FutureTask配合Thread

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
在这里插入图片描述

1.2 线程运行

栈、栈帧

每个线程启动后,虚拟机就会为其分配一块栈内存

· 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
· 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 

线程的栈内存是相互独立的,每个线程拥有自己的独立的栈内存,里面有多个栈帧

线程上下文切换

因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程:

· 线程的cpu时间片用完
· 垃圾回收
· 有更高优先级的线程需要运行
· 线程自身调用了sleep、yield、wait、join、park、synchronized、lock等方法

1.3 常见方法

在这里插入图片描述
在这里插入图片描述
sleep

· 调用sleep会让当前线程从Running 进入 Timed Waiting状态

· 其它线程可以使用 interrupt 方法打断正在睡眠的线程,睡眠结束后的线程未必会立刻得到执行

yield

· 调用 yield 会让当前线程从 Running 进入 Runnable 状态,然后调度执行其它同优先级的线程。(如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果)

· 具体的实现依赖于操作系统的任务调度器

防止CPU占用100%
在这里插入图片描述

interrupt

打断sleep、wait、join的线程会清空打断状态

打断正常运行的线程,标记为true,线程可通过标记来决定是否要停止

打断park线程,不会清空打断状态,在被打断后,再次执行park方法会失效(使用interrupted方法可使park方法再次有效)

多线程设计模式:两阶段终止模式

在这里插入图片描述

在这里插入图片描述
注意:isInterrupted( )不会清除打断标记,interrupted( )会清除打断标记

在这里插入图片描述

守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫 守护线程,只要其它非守护线程运行结束,即使守护线程的代码没有执行完,也会强制结束。

setDaemon(true)即代表设置为守护线程

· 垃圾回收器就是一种守护线程
· Tomcat中的Acceptor 和 Poller 线程都是守护线程,所以Tomcat接收到 shutdown 命令后,不会等待它们处理完当前请求

1.4 线程状态

操作系统层面

在这里插入图片描述

JAVA API 层面,根据Thread.State 枚举,分为6种状态

在这里插入图片描述

2 共享模型之管程

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竟态条件

上下文切换

· 阻塞式解决方案:synchronized,Lock
· 非阻塞式解决方案:原子变量

synchronized是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断

在这里插入图片描述
锁住的是this对象
在这里插入图片描述
锁住的是类对象

2.1 线程安全

在这里插入图片描述
private 修饰防止子类对父类的方法进行重写,一定程度上提供了线程安全,如果父类公共方法也不想被子类影响,则加 final 修饰符

常见线程安全类

在这里插入图片描述
线程安全类方法组合不一定就线程安全,需要额外添加锁

String、Integer等为不可变类,因为其内部的状态不可改变,因此它们的方法都是线程安全的

2.2 Monitor

在这里插入图片描述
在这里插入图片描述

Monitor 监管器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

在这里插入图片描述

· synchronized 必须是进入同一个对象的 monitor 才有上述的效果
· 不加 synchronized 的对象不会关联监视器,不遵从以上规则。

2.3 synchronized优化原理

2.3.1 轻量级锁

如果一个对象虽有多线程访问,但多线程访问的时间是错开的(无竞争),可使用轻量级锁优化,轻量级锁对使用者是透明的(即语法仍然是synchronized)

· 创建锁记录(Lock Record)对象,每个线程栈的栈帧会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
在这里插入图片描述
· 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将 Mark Word的值存入锁记录在这里插入图片描述
01 表示无锁状态、00为轻量级锁状态
· 如果cas替换成功,对象头中存储了 锁记录地址和状态00 ,表示由该线程给对象加锁
在这里插入图片描述
· 如果cas失败,有两种情况

· 如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
· 如果是自己执行了synchronized锁重入,则再添加一条Lock Record作为重入的计数

在这里插入图片描述
· 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
在这里插入图片描述
· 当退出synchronize代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

· 成功,则解锁成功
 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

2.3.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

在这里插入图片描述
· 这时Thread-1加轻量级锁失败,进入锁膨胀流程

· 为Object对象申请Monitor锁,让Object指向重量级锁地址,后两位是10
· 自己进入Monitor的EntryList BLOCKED

在这里插入图片描述
· 当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

2.3.3 自旋优化

重量级锁竞争时,可使用自旋进行优化
如果当前线程自旋成功(这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在这里插入图片描述

2.3.4 偏向锁

轻量级锁在无竞争时,每次重入仍需执行CAS操作。

Java 6中引入偏向锁进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示无竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有

在这里插入图片描述
偏向状态
· 如果开启了偏向锁(默认开启),对象创建后,markword值为0x05即最后三位为101
· 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可加VM参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟

偏向撤销

1、调用对象hashCode

调用对象的hashcode会禁用对象的偏向锁
hashcode用的时候才会产生,默认是0,只有第一次调用对象的hashcode才会产生对象的哈希码,才在对象头的markword里填充哈希码

· 轻量级锁会在锁记录中记录hashCode
· 重量级锁会在Monitor中记录hashCode

2、其它线程使用对象
当其它线程使用 偏向锁对象,会将偏向锁升级为轻量级锁

3、调用 wait/notify

批量重偏向
如果对象虽然被多个线程访问,但无竞争,这是偏向线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID

当撤销偏向锁阈值超过20次后,jvm会在给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过40次后,jvm会将整个类的所有对象都变为不可偏向,新建对象也是不可偏向的。

2.3.5 锁消除

如果对象没有逃出方法的作用范围,JIT即时编译器会把synchronized优化掉,可通过-XX:-EliminateLocks 关闭锁消除

2.3.6 wait notify

在这里插入图片描述

· Owner 线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
· BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用CPU时间片
· BLOCKED 线程会在 Owner 线程释放锁时唤醒
· WAITING 线程会在 Owner 线程调用 notify 或 notifyALL 时唤醒,但唤醒后并不意味立刻获得锁,仍需进入 EntryList 重新竞争

api

· obj.wait() 让进入object监视器的线程到waitSet等待,会释放对象的锁,从而使其它线 程有机会获得对象的锁。无限制等待直到notify为止
· wait(long n) 有时限的等待,到n毫秒后结束等待,或是被notify
· obj.notify() 在object上正在waitSet 等待的线程中挑一个唤醒
· obj.notifyAll() 让object上正在waitSet等待的线程全部唤醒

注:它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法

与sleep区别
1、sleep是Thread方法,而wait是Object的方法
2、sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
3、sleep在睡眠同时,不会释放对象锁,但wait在等待的时候会释放对象锁

共同点:它们状态均为TIMED_WAITING

3 设计模式

3.1 同步模式之保护性暂停

即Guarded Suspension,用在一个线程等待另一个线程的执行结果

要点:

· 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
· 如果有结果不断从一个线程到另一个线程可以使用消息队列
· JDK中,join的实现、Future的实现,采用的就是此模式

在这里插入图片描述
例子:

class GuardedObject{//包含超时效果
	private Object response;//结果
	
	//获取结果
	public Object get(long timeout){
		synchronized(this) {
			//开始时间
			long begin = System.currentTimeMillis();
			
			//经历时间
			long passedTime = 0;
			
			//无结果
			while(response == null){
				//本轮循环应等待的时间
				//防止虚假唤醒导致真正等待的时间 超过 设置的超时时间
				long waitTime = timeout - passedTime;
				//经历的时间超过最大等待时间,退出循环
				if(waitTime <= 0){
					break;
				}
				try{
					this.wait(waitTime);
				}catch (InterruptedException e){
					e.printStackTrace();
				}
				// 经历时间
				passedTime = System.currentTimeMillis() - begin;
			}
			return response;
		}
	}
	
	//产生结果
	public void complete(Object response){
		synchronized(this){
			//给结果成员变量赋值
			this.response = response;
			this.notifyAll();
		}
	}

}

3.2 异步模式之生产者/消费者

要点:

· 不需要产生结果和消费结果的线程一一对应
· 消费队列可用来平衡生产和消费的线程资源
· 生产者仅负责生产结果数据,不关心数据该如何处理,而消费者专心处理结果数据
· 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
· JDK中各种阻塞队列,采用的就是这种模式 

在这里插入图片描述
例子:

public class MessageQueue {
    final class Message{
        private int id;
        private Object value;

        public Message(int id, Object value) {
            this.id = id;
            this.value = value;
        }

        public int getId() {
            return id;
        }

        public Object getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "Message{" +
                    "id=" + id +
                    ", value=" + value +
                    '}';
        }
    }

    private LinkedList<Message> list = new LinkedList();
    private int capacity;


    //获取消息
    public Message take(){
         synchronized (list){
             //检查消息是否为空
             while (list.isEmpty()){
                 try {
                     list.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
             //从队列头部获取消息
             Message message = list.removeFirst();
             list.notifyAll();
             return message;

         }

    }


    //存入消息
    public void put(Message message){
        synchronized (list){
            //检测对象是否已满
            while (list.size() == capacity) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //将消息加入到队列尾部
            list.addLast(message);
            list.notifyAll();
        }
    }
}

3.3 Park&Unpark

在这里插入图片描述
unpark可以在park前调用,也可以在park后调用

与 Object 的 wait&notify 相比

· wait、notify和notifyAll必须配合Object Monitor一起使用,而unpark不必
· park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程
· park & unpark 可以先 unpark,而 wait & notify 不能先 notify

每个线程都有自己的一个Parker对象,由三部分组成_counter,_cond和_mutex
在这里插入图片描述
调用park时:

在这里插入图片描述

调用unpark时:

在这里插入图片描述
先调用unpark再调用park:

在这里插入图片描述

3.4 线程状态转换

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

多把锁

将锁的粒度细分

· 好处:可以增强并发度
· 坏处:如果一个线程需要同时获得多把锁,就容易发送死锁

活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。

ReentrantLock

相对于synchronized ,它具备的特点:

· 可中断
· 可设置超时时间
· 可设置为公平锁
· 支持多个条件变量

与synchronized一样,都支持可重入
synchronized是在关键字级别保护临界区,reentrantLock是在对象级别保护临界区

基本语法:
在这里插入图片描述
ReentrantLock的lockInterruptibly( )方法在无竞争时会获取对象锁,如果有竞争会进入阻塞队列,可被其它线程用 interrupt 方法打断

tryLock() 尝试获取锁,返回值为boolean,tryLock(long,TimeUnit)设定时间内尝试获取锁

条件变量
synchronized中有条件变量,即当条件不满足时进入waitSet等待

ReentrantLock的条件变量是支持多个条件变量的

	· synchronized是那些不满足条件的线程都在一间休息室等消息
	· 而ReentrantLock支持多间休息室

使用流程:

· await前需要获得锁
· await执行后,会释放锁,进入conditionObject等待
· await的线程被唤醒(或打断、或超时)去重新竞争lock锁
· 竞争lock锁成功后,从await后继续执行

3.5 同步模式之顺序控制

3.5.1 固定运行顺序

可通过wait和notify、park和Unpark实现

3.5.2 交替输出

可通过wait和notify、await和signal、park和unpark实现

4 共享模型之内存

4.1 Java内存模型

JMM,JMM体现在以下几个方面:

· 原子性 - 保证指令不会受到线程上下文切换的影响
· 可见性 - 保证指令不会受 CPU 缓存的影响
· 有序性 - 保证指令不会受 CPU 指令并行优化的影响

4.2 设计模式

4.2.1 两阶段终止模式 - volatile

在这里插入图片描述

class twophaseterminal{
        //监控线程
        private Thread monitorThread;
        //终止标志
        private volatile boolean stop = false;

        //启动监控线程
        public void start(){
            monitorThread = new Thread(()->{
                while (true){
                    Thread thread = Thread.currentThread();
                    //是否打断
                    if (stop){
                        //结束
                        break;
                    }
                    try{
                        Thread.sleep(1000);
                        //监控处理

                    } catch (InterruptedException e) {
                    }
                }
            },"monitor");
            monitorThread.start();
        }

        //停止监控线程
        public void stop(){
            stop = true;
            monitorThread.interrupt();//通过interrupt可立即终止监控线程
        }
    }

如果存在线程处于睡眠状态,如果不通过interrupt方法,则只能等待休眠时间结束,才可终止

4.2.2 同步模式之Balking

Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做,直接结束返回

在这里插入图片描述
还经常用来实现线程安全的单例:

在这里插入图片描述

4.3 volatile原理

底层实现原理:内存屏障(Memory Barrier)

· 对 volatile 变量的写指令后会加入写屏障
· 对 volatile 变量的读指令前会加入读屏障

如何保证可见性

· 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

在这里插入图片描述

· 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

在这里插入图片描述

在这里插入图片描述

如何保证有序性

· 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

在这里插入图片描述

· 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

在这里插入图片描述

在这里插入图片描述

不能解决指令交错:

· 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
· 有序性的保证也只是保证了本线程内相关代码不被重排序

在这里插入图片描述

5 共享模型之无锁

CAS

CAS的底层是 lock cmpxchg 指令(X86架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性

获取共享变量时,为了保证该变量的可见性,需使用volatile修饰。CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果

CAS特点:
在这里插入图片描述

5.1 原子整数

JUC并发包提供:

· AtomicBoolean
· AtomicInteger
· AtomicLong

其中, AtomicInteger的incrementAndGet、getAndIncrement好比++i、i++,但具有原子性

updateAndGet、getAndUpdate传参函数式接口,自定义运算操作,具有原子性

例如 updateAndGet底层原理:通过传入函数式接口,调用其方法(具体实现由传入时决定)
在这里插入图片描述

5.2 原子引用

原子引用类型:

· AtomicReference
· AtomicMarkableReference
· AtomicStampedReference

AtomicReference 存在ABA问题:主线程仅能判断出共享变量的值与最初值是否相同,无法感知其他线程是否对最初值进行过更改。如果主线程希望只要有其它线程【动过了】共享变量,则自己的cas就算失败,此时仅比较值是不够的,需要再加一个版本号

AtomicStampedReference可以给原子引用加上版本号,追踪原子引用整个的变化过程,但有时候并不关心引用变量更改了几次,只单纯关心是否更改过,即AtomicMarkableReference

5.3 原子数组

· AtomicIntegerArray
· AtomicLongArray
· AtomicReferenceArray

5.4 原子更新器

· AtomicReferenceFieldUpdater // 域 字段
· AtomicIntegerFieldUpdater
· AtomicLongFieldUpdater

利用字段更新器,可针对对象的某个域(Field)进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常

5.5 原子累加器

· LongAccumulator
· LongAdder
· DoubleAccumulator
· DoubleAdder

性能提升的原因:在有竞争时,设置多个累加单元,Thread-0累加Cell[0],Thread-1累积Cell[1]…最后将结果进行汇总。它们在累加时操作的不同的Cell变量,减少了CAS重试失败,从而提高性能

伪共享
Cell累加单元
在这里插入图片描述
由于CPU与内存的速度差异大,需要靠预读数据至缓存来提升效率,缓存以缓存行为单位,每个缓存行对应着一块内存(64byte 8个long)

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU要保证数据一致性,如果某个CPU核心更改了数据,其它CPU核心对应的整个缓存行必须失效

在这里插入图片描述
通过@sun,misc.Contended注解解决该问题,原理:在使用此注解的对象或字段的前后各增加128字节大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,即不会造成对方缓存行的失效

在这里插入图片描述

5.6 Unsafe

Unsafe 对象提供了非常底层的操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

在这里插入图片描述
cas相关方法
如果在执行cas操作期间,有其它线程干扰,则需要嵌套while(true),反复重试获取最新值进行修改
在这里插入图片描述

通过unsafe对象操作数据时,需要先得到域的偏移量,在得到偏移量时可能抛出找不到域的异常,需要转化成非受查异常。

例如:自定义AtomicInteger类:

在这里插入图片描述

6 共享模型之不可变

6.1 不可变设计

类、类中所有属性都是final的

· 属性用final修饰保证了该属性是只读,不可修改
· 类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

保护性拷贝
以String的substring为例:
在这里插入图片描述
内部是调用String的构造方法创建一个新字符串

在这里插入图片描述
构造新字符串对象时,会生成新的char[] value,对内容进行复制。

这种通过创建副本对象来避免共享的手段称为 保护性拷贝

6.2 享元模式

Flyweight pattern
当需要重用数量有限的同一类对象时

体现:在JDK中Boolean、Byte、Short、Integer、Long、Character等包装类提供了valueOf方法。例如Long的valueOf会缓存-128·127之间的Long对象,在该范围内会重用对象,大于范围才会新建对象

在这里插入图片描述
String的串池、BigDecimal、BigInteger

6.3 final原理

设置final变量原理

在这里插入图片描述
从字节码角度,final变量的复制通过putfield指令来完成,在该条指令后加入写屏障(指令不会重排序到写屏障后),保证在其它线程读到它的值时不会出现为0的情况

获取final变量原理

final修饰的静态变量,其它类获取时将值复制到该类的栈中(如果值超过了定义范围,则是在常量池中),如果去掉final修饰,则需要该类到变量所在类中去get获取,即通过共享内存(比通过栈内存 性能要低)

6.4 无状态

成员变量保存的数据也可称为状态信息,没有成员变量则称之为【无状态】

7 并发工具

7.1 线程池

在这里插入图片描述
自定义阻塞队列:

//自定义线程池 阻塞队列
public class BlockingQueue<T> {
    //任务队列
     private Deque<T> queue = new ArrayDeque<>();
    //锁
    private ReentrantLock lock = new ReentrantLock();
    //生产者条件变量
    private Condition fullWaitSet = lock.newCondition();
    //消费者条件变量
    private Condition emptyWaitSet = lock.newCondition();
    //容量
    private int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    //带超时的阻塞获取
    public T poll(long timeout, TimeUnit unit){
        lock.lock();
        try {
            //将timeout统一转换成 纳秒
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()){
                try {
                    if (nanos < 0) return null;
                    //返回剩余时间
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            //唤醒生产者
            fullWaitSet.signal();
            return t;
        }finally {
            lock.unlock();
        }
    }

    //阻塞获取
    public T take(){
        lock.lock();
        try {
            while (queue.isEmpty()){
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            //唤醒生产者
            fullWaitSet.signal();
            return t;
        }finally {
            lock.unlock();
        }
    }
    //阻塞添加
    public void put(T element){
        lock.lock();
        try{
            while (queue.size() == capcity){
                try {
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.addLast(element);
            //唤醒消费者
            emptyWaitSet.signal();
        }finally {
            lock.unlock();
        }
    }

    //带超时时间的阻塞添加
    public boolean offer(T task,long timeout,TimeUnit timeUnit){
        lock.lock();
        try{
            long nanos = timeUnit.toNanos(timeout);
            while (queue.size() == capcity){
                try {
                    if (nanos <= 0) return false;
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.addLast(task);
            //唤醒消费者
            emptyWaitSet.signal();
            return true;
        }finally {
            lock.unlock();
        }
    }

    //获取容量
    public int size(){
        lock.lock();
        try{
            return queue.size();
        }finally {
            lock.unlock();
        }
    }

}

自定义线程池

public class ThreadPool {
    //任务队列
    private BlockingQueue<Runnable> taskQueue;
    //线程集合
    private HashSet<Worker> workers = new HashSet<>();
    //核心线程数
    private int coreSize;
    //获取任务的超时时间
    private long timeout;
    //时间单位
    private TimeUnit timeUnit;

    //执行任务
    public void execute(Runnable task){
        //当任务数没有超过 核心线程数时,直接交给worker去执行
        //当任务数超过 核心线程数时,加入任务队列暂存
        synchronized (workers){
            if (workers.size() <= coreSize){
                Worker worker = new Worker(task);
                workers.add(worker);
                worker.start();
            }else taskQueue.put(task);
        }
    }

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int capcity) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue(capcity);
    }

    //封装线程的worker类
    class Worker extends Thread{

        private Runnable task;

        public Worker(Runnable task){
            this.task = task;
        }

        @Override
        public void run() {
            //当task不为空时,执行任务
            //当task执行完毕,再接着从任务队列获取任务并执行
            //while (task!=null || (task = taskQueue.take())!=null){
            while (task!=null || (task = taskQueue.poll(timeout,timeUnit))!=null){
                try{
                    task.run();
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    task = null;
                }
            }
            synchronized (workers){
                workers.remove(this);
            }
        }
    }
}

策略模式 - 拒绝策略
将权利下放至调用者,自定义实现:

public class Test {
    private final static int coreSize = 1;
    private final static long timeout = 1000;
    private final static TimeUnit timeunit = TimeUnit.MILLISECONDS;
    private final static int queueCapcity = 1;

    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(coreSize,timeout,timeunit,queueCapcity,
                (queue,task) ->{
                    //拒绝策略
                    //1 死等
                    //queue.put(task);

                    //2超时等待
                    //queue.offer(task,1500,TimeUnit.MILLISECONDS);

                    //3让调用者放弃任务执行
                    //System.out.println("放弃任务 "+task);

                    //4让调用者抛出异常, 后续的任务都不会执行
                    //throw new RuntimeException("任务执行失败 "+task);

                    //5让调用者自己执行任务
                    task.run();

        });

        for (int i = 0; i < 4; i++) {
            int j = i;
            threadPool.execute(()->{
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(j);
            });
        }
    }
}

7.1.1 ThreadPoolExecutor

线程池状态
使用int的高3位表示线程池状态,低29位表示线程数量

在这里插入图片描述
将线程池状态与线程个数合二为一,则可通过一次cas原子操作进行赋值

构造方法

在这里插入图片描述
最大线程数 = 核心线程数 + 救急线程数

救急线程针对于突发的大规模任务量时才会被创建,任务执行完毕,超过生存时间(核心线程没有生存时间)则会被销毁。只有当阻塞队列是有界队列时,任务超过队列大小,则会创建救急线程

当救急线程都被用完时,才会执行拒绝策略

JDK实现的四种策略:
在这里插入图片描述
在这里插入图片描述
· Dubbo的拒绝策略实现,是对AbortPolicy的扩展,在抛出拒绝异常之前会记录日志,并dump线程栈信息,方便定位问题
· Netty的实现,是创建一个新线程来执行任务
· ActiveMQ的实现,带超时等待(60s)尝试放入队列
· PinPoint的实现,使用一个拒绝策略链,会逐一尝试链中每个拒绝策略

newFixedThreadPool
Executors类的创建一个固定大小的线程池
在这里插入图片描述
特点:
· 核心线程数 == 最大线程数(没有救急线程被创建),无需超时时间
· 阻塞队列是无界的,可以放任意数量的任务

还可通过new线程工厂来自定义线程名
在这里插入图片描述

newCachedThreadPool
带缓存的线程池
在这里插入图片描述
特点:
· 核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s

	· 全部都是救急线程(60s可回收)
	· 救急线程可无限创建

· 队列采用SynchronousQueue实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)

整个线程池表现为线程数会根据任务量不断增长,无上限,当任务执行完毕,空闲1分钟后释放线程。适合任务数比较密集,但每个任务执行时间较短的情况。

newSingleThreadExecutor
单线程线程池

多个任务排队执行。线程数固定为1,任务数多于1时,会放入无界队列排队。当任务执行完毕,这唯一的线程也不会被释放

在这里插入图片描述
在这里插入图片描述

7.1.1.1 submit

在这里插入图片描述
Callable类型的任务会有返回值,通过Future获取任务执行结果,利用保护性暂停模式在两线程间进行接收结果

7.1.1.2 invokeAll

提交tasks中所有任务

7.1.1.3 invokeAny

提交tasks中所有任务,哪个任务先成功执行完毕,则返回该任务执行结果,其他任务取消

7.1.1.4 停止

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

shutdownNow
同上,但会将队列中的任务返回,并用interrupt的方式中断正在执行的任务

7.1.1.5 任务调度线程池

首先可通过使用 java.util.Timer 来实现定时功能,Timer的优点在于简答易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行,同一时间只能一个任务在执行,前一个任务的延迟或异常都将会影响之后的任务。

ScheduledThreadPoolExecutor

通过设置多个线程池,使第一个线程不管是延迟执行、发生异常等都不会让第二个线程受影响

7.1.1.6 Tomcat 线程池

由Connector与Container部分构成,Connector对外交流沟通、Container实现Servlet规范运行Servlet组件

Connector NIO EndPoint组件构成:
在这里插入图片描述
在这里插入图片描述
如果总线程数达到maximumPoolSize,不会立刻抛异常,而是再次尝试将任务放入队列,如果还失败,才抛出异常

7.1.2 Fork/Join线程池

Fork/Join是JDK1.7后加入的新线程池,体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算

Fork/Join默认会创建与cpu核心数大小相同的线程池

7.1.3 异步模式之工作线程

让有限的工作线程(Worker Thread)轮流异步处理无限多的任务,也可将其归类为分工模式,典型实现就是线程池,也体现了经典设计模式中的享元模式。

固定大小线程池会有饥饿现象,不同的任务类型应使用不同的线程池

· 过小会导致程序不能充分地利用系统资源、容易导致饥饿
· 过大会导致更多的线程上下文切换,占用更多内存

CPU密集型运算
通常采用cpu 核数+1 能够实现最优的CPU利用率,+1是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证CPU时钟周期不被浪费

I/O密集型运算
CPU不总是处于繁忙状态,可利用多线程提高它的利用率

公式:

线程数 = 核数 * 期望CPU利用率 * 总时间(CPU计算时间+等待时间) /  CPU计算时间

7.2 JUC

7.2.1 AQS原理

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

在这里插入图片描述
自定义锁
不可重入锁:

//自定义锁(不可重入锁)
class Mylock implements Lock{
    // 独占锁 同步器类
    class MySync extends AbstractQueuedSynchronizer{
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0,1)){//保证修改state是原子性的
                //加上了锁,并设置owner为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            //state是volatile修饰的,保证了它之前的对属性的修改对其他线程可见
            setExclusiveOwnerThread(null);
            setState(0);

            return true;
        }

        @Override //是否持有独占锁
        protected boolean isHeldExclusively() {
            return getState()==1;
        }

        public Condition newCondition(){
            return new ConditionObject();
        }
    }

    private MySync sync = new MySync();

    @Override //加锁(不成功会进入等待队列)
    public void lock() {
        sync.acquire(1);
    }

    @Override // 加锁 可打断
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override // 尝试加锁(一次)
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override // 尝试加锁,带超时
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    @Override // 解锁
    public void unlock() {
        sync.release(1);
    }

    @Override // 创建条件变量
    public Condition newCondition() {
        return sync.newCondition();
    }
}

7.2.2 ReentrantLock 原理

在这里插入图片描述
非公平锁实现原理
无竞争时,将state设置成为1,然后将OwnerThread改为当前线程
在这里插入图片描述

在这里插入图片描述
当有竞争时,执行acquire
在这里插入图片描述
在这里插入图片描述
Thread1会先CAS尝试修改state的值,失败,进入tryAcquire逻辑,但结果仍然失败,随后执行addWaiter构造Node队列

在这里插入图片描述
当前线程进入acquireQueued逻辑
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
解锁竞争成功流程
在这里插入图片描述
unlock方法调用同步器的release方法,进入tryRelease流程
在这里插入图片描述
在这里插入图片描述
当前队列不为null,并且head的waitStatus=-1,进入unparkSuccessor流程在这里插入图片描述
找到队列中离head最近的一个Node(没取消的),unpark恢复其运行,如图中的Thread-1,回到Thread-1的acquireQueued流程
在这里插入图片描述
在这里插入图片描述
解锁竞争失败流程
如果在Thread-1恢复运行时,有其它线程来竞争,如图中Thread-4来竞争
在这里插入图片描述

7.2.2.1 锁可重入原理

获得锁
在这里插入图片描述
释放锁
在这里插入图片描述

7.2.2.2 可打断原理

不可打断模式
在该模式下,线程被打断,仍会驻留在AQS队列中(一直会得不到打断响应),等获得锁后才能继续运行(才能知道有其它线程打断我)

即 获得锁后,打断才可生效
在这里插入图片描述

可打断模式
被打断后,抛出异常,不会继续在AQS队列里等待
在这里插入图片描述

7.2.2.3 公平锁原理

非公平锁实现:
如果state状态为0,则直接去CAS修改去竞争锁,不会检测ASQ队列
在这里插入图片描述

公平锁原理:
会先检测AQS队列,再决定是否进行CAS修改
在这里插入图片描述

7.2.2.4 条件变量

每个条件变量对应一个等待队列,其实现类是ConditionObject

await
在这里插入图片描述
开始时Thread-0持有锁,调用await,进入ConditionObject的addConditionWaiter流程,创建新Node状态值为-2,关联Thread-0,加入等待队列尾部
在这里插入图片描述
进入AQS的fullyRelease流程,释放同步器上的锁,将state设为0,exclusiveOwnerThread设为null
在这里插入图片描述
unpark AQS队列中的下一个节点去竞争锁,例如:假设无其它竞争线程,则Thread-1竞争成功
在这里插入图片描述
park阻塞Thread-0
在这里插入图片描述

signal
在这里插入图片描述

假设Thread-1要来唤醒Thread-0
在这里插入图片描述
进入ConditionObject的doSignal流程,取得等待队列中第一个Node,即图中的Thread-0所在Node
在这里插入图片描述
执行transferForSignal流程,将该Node加入AQS队列尾部,将Thread-0的waitStatus改为0,Thread-3的waitStatus改为-1在这里插入图片描述
在这里插入图片描述
Thread-1释放锁,进入unlock流程

7.2.3 读写锁

7.2.3.1 ReentrantReadWriteLock

当读操作远高于写操作时,可使用 读写锁 让 读-读 并发,提供性能

读-读可以并发,但读-写、写-写是互斥的

注意事项:
· 读锁不支持条件变量,写锁支持条件变量
· 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

在这里插入图片描述
· 重入时支持降级:即持有写锁的情况下去获取读锁

在这里插入图片描述

7.2.3.2 读写锁原理

读写锁用同一个Sycn同步器,等待队列、state也是同一个

t1 w.lock,t2 r.lock

1)t1成功上锁(写锁状态占state的低16位,读锁使用state的高16位)
在这里插入图片描述

在这里插入图片描述
2)t2执行r.lock,这时进入读锁的sync.acquireShared流程,首先进入tryAcquireShared流程。如果有写锁占据,则返回 -1 表失败(成功则返回 1 )
在这里插入图片描述
3)随后进入sync.doAcquireShared流程。首先调用addWaiter添加节点,不同之处在于节点被设置为Node.SHARED模式而非Node.EXCLUSIVE模式,此时 t2 仍处于活跃状态
在这里插入图片描述
在这里插入图片描述
4)t2会检查自己前驱节点是否是头结点,如果是,则会再次调用 tryAcquireShared 尝试获取锁

5)如果没成功,则会通过shouldParkAfterFailedAcquire把前驱节点的waitStatus改为-1,随后再次循环一次尝试tryAcquireShared,如果还不成功,则在parkAndCheckInterrupt处park

t3 r.lock,t4 w.lock
在上述情况下,假设又有t3加读锁、t4加写锁,期间 t1 仍持有锁
在这里插入图片描述
t1 w.unlock,走到写锁的sync.release流程,调用sync.tryRelease成功
在这里插入图片描述
在这里插入图片描述
随后执行唤醒流程sync.unparkSuccessor,即让第二个节点(t2)恢复运行,t2在doAcquireShared内parkAndCheckInterrupt()处恢复运行,再次for循环一次去执行tryAcquireShared,成功则让读锁计数加一
在这里插入图片描述
t2调用setHeadAndPropagate(node,1),它原本所在节点被设置为头节点
在这里插入图片描述
在setHeadAndPropagate方法内会检查下一个节点是否是shared,如果是,则调用doReleaseShared()将head状态从-1改为0(目的是避免其它线程的干扰)并唤醒第二个节点(如图中t3),此时t3在doAcquireShared内parkAndCheckInterrupt()处恢复运行
在这里插入图片描述
在这里插入图片描述
t3再次循环执行tryAcquire,成功则让读锁计数加一
在这里插入图片描述
t3接下来调用setHeadAndPropagate(node,1),它原本所在节点被设置为头节点
在这里插入图片描述
下一个节点(t4)不是shared,则不会继续唤醒t4所在节点

t2 r.unlock,t3 r.unlock
t2进入sync.releaseShared调用tryReleaseShared,让计数减一,此时计数还不为0
在这里插入图片描述
在这里插入图片描述
t3进入sync.releaseShared调用tryReleaseShared,让计数减一,此时计数为0,进入doReleaseShared()将头节点从-1改为0并唤醒第二个节点(t4)
在这里插入图片描述
在这里插入图片描述
之后t4在acquireQueued中parkAndCheckInterrupt处恢复运行,再次循环检查自己是第二个节点,且没有其他竞争,tryAcquire成功,修改头节点,流程结束
在这里插入图片描述
在这里插入图片描述

7.2.4 StampedLock

特点:在使用读锁、写锁时都必须配合【戳】使用

加解读锁
在这里插入图片描述
加解写锁
在这里插入图片描述
乐观读,StampedLock支持tryOptimisticRead()方法(乐观读),读取完毕后需要做一次 戳校验,如果校验通过,表示这期间无写操作,数据可以安全使用,如果校验未通过,需重新获取读锁,保证数据安全
在这里插入图片描述

注意!!!
· StampedLock不支持条件变量
· StampedLock不支持重入

7.2.5 Semaphore

信号量,用来限制能同时访问共享资源的线程上限
在这里插入图片描述
· 使用Semaphore限流,在访问高峰期时,让请求线程阻塞,高峰期过后再释放许可,它只适合限制单机线程数量,且仅是限制线程数,而不是限制资源数

原理

加锁解锁流程
Semaphore好比一个停车场,permits为停车位数量,当线程获得了permits就像是获得了停车位,然后停车场显示空余车位减一
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.2.6 CountdownLatch

用来进行线程同步协作,等待所有线程完成倒计时。其中,构造参数用来初始化等待计数值,await()用来等待计数归零,countDown()用来让计数减一

例子:模拟多玩家游戏加载完毕,游戏开始

public class TestCountdownLatch {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(10);
        CountDownLatch count = new CountDownLatch(10);

        String[] strings = new String[10];
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            int index = i;
            service.submit(()->{
                for (int j = 0; j <= 100; j++) {
                    try {
                        Thread.sleep(random.nextInt(500));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    strings[index] = j +"%";
                    System.out.print("\r"+ Arrays.toString(strings));
                }
                count.countDown();
            });
        }

        count.await();
        System.out.println("\n ----游戏开始----");
        service.shutdown();
    }
}

子线程与主线程交换结果,可通过future

7.2.7 CyclicBarrier

循环栅栏,用于进行线程协作,等待线程满足某个计数。
构造时设置【计数个数】,每个线程执行到某个需要“同步”的时刻调用await( )方法进行等待,当等待的线程数满足【计数个数】时,继续执行

注意!!!
· 当【计数个数】减为0时,会重置为 最初设置的值,供继续使用

· 线程数与要计数个数保持一致

7.2.8 线程安全集合类概述

在这里插入图片描述
遗留的安全集合,如Hashtable,其方法都是用synchronized来修饰
在这里插入图片描述

修饰的安全集合,如SynchronizedMap,直接调用map的方法,但会增加synchronized的修饰
在这里插入图片描述
JUC 安全集合
· Blocking大部分实现基于锁,并提供用来阻塞的方法
· CopyOnWrite 之类容器修改开销相对较重
· Concurrent 类型的容器

	· 内部很多操作使用cas优化,一般可提供较高吞吐量
	· 弱一致性
		· 遍历时弱一致性,如当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的(fail-save机制)
		· 求大小弱一致性,size操作未必是100%准确
		· 读取弱一致性

遍历时如果发送修改,对于非安全容器来说,使用 fail-fast 机制,即让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历。

7.2.8.1 ConcurrentHashMap原理

hashMap
在jdk8中,桶下标相同的元素,后加入的元素会放入链表采用尾插法,但jdk7中采用头插法。

当数组元素超过阈值(数组长度的3/4)时,会进行数组扩容,此时各元素桶下标会重新计算。但在多线程的情况下进行扩容时,会造成并发死链问题

hashmap 并发死链
前提:JDK7环境下
resize() 中节点(Entry)转移的源代码:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;//得到新数组的长度   
    // 遍历整个数组对应下标下的链表,e代表一个节点
    for (Entry<K,V> e : table) {   
        // 当e == null时,则该链表遍历完了,继续遍历下一数组下标的链表 
        while(null != e) { 
            // 先把e节点的下一节点存起来
            Entry<K,V> next = e.next; 
            if (rehash) {              //得到新的hash值
                e.hash = null == e.key ? 0 : hash(e.key);  
            }
            // 在新数组下得到新的数组下标
            int i = indexFor(e.hash, newCapacity);  
             // 将e的next指针指向新数组下标的位置
            e.next = newTable[i];   
            // 将该数组下标的节点变为e节点
            newTable[i] = e; 
            // 遍历链表的下一节点
            e = next;                                   
        }
    }
}

在这里插入图片描述

原因:在多线程环境下使用了非现场安全的map集合

JDK8虽对扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)

7.2.8.2 ConcurrentHashMap 8 原理
属性、内部类

在这里插入图片描述

重要方法

在这里插入图片描述

构造器

实现懒惰初始化,在构造方法中仅仅计算table的大小,以后在第一次使用时才会真正创建
在这里插入图片描述
通过tableSizeFor计算出大小为2的n次方,因此传入的设置初始大小不一定是最终创建出来的初始大小

get流程

全程没有加synchronized锁
在这里插入图片描述

put流程

数组简称(table)、链表简称(bin)
hashmap允许 键或值 为null,但concurrenthashmap不允许

第一个分支是初始化table,第二个分支是:在无头节点时,创建链表头节点,然后将键、值包装成node放入
在这里插入图片描述
第三个分支是:hashtable进行扩容时,每完成一个链表的扩容,则将链表头设为forwardingnode,hash值为-1。如果头节点为MOVED(值为-1),表示当前链表正在被其它线程进行扩容,当前线程通过helpTransfer函数锁住当前链表,去帮忙其它线程进行扩容
在这里插入图片描述
第四个分支是:发生桶下标冲突时,此时需要synchronized锁。
第一种情况,头节点hash码是否>=0,>=0表示普通节点。key如果存在的话,需要进行更新操作,onlyIfAbsent默认为false表示覆盖;如果key不存在的话,需要进行追加操作
在这里插入图片描述
第二种情况:头节点hash值<0,表示红黑树的头节点TreeBin (默认值为-2),完成往红黑树中添加节点的逻辑
在这里插入图片描述
binCount代表链表中节点个数,最后判断是否超过阈值,进行树化。通过treeifyBin将链表转成红黑树,首先先进行扩容,等整个hash表的长度超过64后,如果链表长度还是>=8,则进行红黑树化
在这里插入图片描述

initTable

第一个线程进来时通过CAS将sizeCtl设置为-1,然后其它线程则会调用yield进入忙等待(不是阻塞状态)。第一个线程在创建完table会重新计算一下sc的值,代表下一次要扩容时的阈值
在这里插入图片描述

addCount

增加hash表中元素的计数。当size计数超过阈值时,会进行扩容操作
采取类似LongAddr的思想,有多个累加单元,能保证多线程计数增长时,cas冲突就会减少

fullAddCount方法里 添加了@Contended注解防止数据伪共享(保证数据分布在不同的缓存行,避免数据更新互相影响)
在这里插入图片描述

size

size计算实际发生在put、remove改变集合元素的操作之中

· 没有竞争发生,向 baseCount 累加计数
· 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
	· counterCells 初始有两个 cell
	· 如果计数竞争比较激烈,会创建新的 cell 来累加计数

在这里插入图片描述

transfer

如果nextTab为空,则创建新的hash表数组(原来容量的2倍),然后再赋值给nextTab
在这里插入图片描述
然后开始做节点的搬迁工作,以链表为单位。如果链表头为null,则表明处理完成,需要将链表头设为forwardingNode
在这里插入图片描述
然后判断节点是否是普通节点还是树节点,分别进入不同的逻辑进行节点搬迁

7.2.8.3 ConcurrentHashMap 7 原理

它维护了一个 segment 数组,每个 segment 对应一把锁

· 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与jdk8中是类似的
· 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

在数组创建完毕后,下标为0的元素也会被创建出来
在这里插入图片描述
构造完成后,如图:每个segment对应一个hash表
在这里插入图片描述

构造器分析

定位segment
this.segmentShift 和 this.segmentMask 的作用是决定将key的hash结果匹配到哪个segment
在这里插入图片描述
在这里插入图片描述

put流程

除第一个segment不是懒惰加载,其他的segment对象是懒惰加载
在这里插入图片描述
segment继承了可重入锁(ReentrantLock),它的put方法为:
首先会尝试加锁,如果失败则会进入scanAndLockForPut,不断忙循环尝试至64次,则进入lock流程
在这里插入图片描述
如果trylock成功,则会往下执行,此时segment已经被成功加锁,每个segment为一个hashentry数组,计算桶下标,找到对应的头节点开始遍历,然后是两种操作:更新、新增

更新操作
在这里插入图片描述
新增操作
在这里插入图片描述

rehash流程

发生在put 新增中,元素超过阈值,因为此时已经获得了锁,所以rehash时不需要考虑线程安全

首先创建一个容量为原先2倍的新hash表
在这里插入图片描述
开始遍历旧hash表中节点 ,第一种情况,没有后续节点,则直接移动到新hash表中。如图中最左侧节点
在这里插入图片描述
在这里插入图片描述
第二种情况,遍历hash表,找到与上一次hash码不同的节点,如图中9,5,1节点,则会从9节点开始直接搬迁到新hash表中相应的下标位置

在这里插入图片描述
在这里插入图片描述
剩下的节点需要新建hashEntry,如图中节点3
在这里插入图片描述
在这里插入图片描述
在扩容完成以后才会加入新的节点,最后将新table替换掉旧table

在这里插入图片描述

get流程

get时未加锁,用UNSAFE 的getObjectVolatile方法保证取segment对象或取链表头时的可见性,扩容过程中,get先发生就从旧表取内容,get后发生就从新表取内容

在这里插入图片描述

size计算流程

· 计算元素个数前,先不加锁计算两次,如果前后两次结果都一样,则认为个数正确返回
· 如果不一样,进行重试,重试次数超过3,将所有segment锁住,重新计算个数返回

在这里插入图片描述

7.2.8.4 LinkedBlockingQueue原理
入队出队

在这里插入图片描述
初始化链表 last = head = new Node< E >(null);Dummy节点用来占位,item为null
在这里插入图片描述
当一个节点入队 last = last,next = node; 表示新入队的节点作为最后一个节点的next
在这里插入图片描述
在这里插入图片描述
出队

在这里插入图片描述
h = head
在这里插入图片描述
first = h.next
在这里插入图片描述h.next = h,防止h不乱指向其它节点,保证其安全回收
在这里插入图片描述
head = first
在这里插入图片描述
Dummy只是个占位节点,真正要出队的节点为first,将值赋给一个临时变量,然后设置为null,返回临时变量
在这里插入图片描述
first节点成为新的Dummy节点
在这里插入图片描述

加锁分析

标志:加了两把锁dummy节点

· 用一把锁,同一时刻,最多只允许有一个线程(消费者或生产者)执行
· 用两把锁,同一时刻,可允许两个线程(消费者和生产者)同时执行
	· 消费者和消费者线程仍然串行
	· 生产者和生产者线程仍然串行

线程安全分析
· 当节点总数 > 2 时(包含dummy节点),putLock保证的是last节点的线程安全,takeLock保证的是head节点的线程安全。两把锁保证了入队和出队没有竞争

· 当节点总数 = 2 时(一个dummy节点、一个正常节点),此时仍是两把锁锁对象,不会竞争

· 当节点总数 = 1 时(就一个dummy节点),此时take线程会被notEmpty条件阻塞,有竞争,会阻塞

在这里插入图片描述

put操作

在这里插入图片描述

take操作

在这里插入图片描述

与ArrayBlockingQueue性能比较

· Linked支持有界,Array强制有界
· Linked实现是链表,Array实现是数组
· Linked是懒惰的,而Array需要提前初始化Node数组
· Linked每次入队会生成新Node,而Array的Node是提前创建好的
· Linked两把锁,Array一把锁

7.2.8.5 ConcurrentLinkedQueue

· 两把【锁】,同一时刻,可以运行两个线程(生产者与消费者)同时执行
· dummy节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
· 【锁】使用了cas来实现

在这里插入图片描述

7.2.8.6 CopyOnWriteArrayList

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

写操作
在这里插入图片描述
读操作,并未加锁
在这里插入图片描述
适合【读多写少】的场景

存在get 弱一致性
Thread-0先get获取Array的引用,随后Thread-1get到Array做拷贝,在拷贝的数组上删除元素1,随后setArray再替换掉旧数组,但此时Thread-0还是会读取到元素1
在这里插入图片描述
迭代器弱一致性

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Swing_zzZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值