并发编程复习

并行和并发的区别:

从操作系统角度看,线程是CPU分配的最小单位。

并行:同一时刻多个线程都在执行,需要多个cpu核心

并发:同一时刻,只有一个线程在执行,一段时间内有多个线程在执行,依赖于cpu的线程上下文切换,如果是不同进程则是进程上下文切换,线程为线程上下文切换。

什么是进程?什么是线程?什么时候用进程?什么时候用线程?为什么多进程?为什么多线程?

进程简单点就是在电脑上的一个应用,是系统进行资源分配和调度的基本单位,jvm就是一个进程,拥有自己独立的地址和资源,运行时数据区;

线程是CPU调度和分配资源的基本单位,就像是一个java进程里面的main线程,同时也可以有很多处理其他任务的线程,线程之间共享进程间的资源,线程之间通信也比较简单,可以通过共享资源进行通信;以jvm线程为例子,每个线程都会有自己的程序计数器(存储当前线程在执行的java方法的JVM指令地址)、java虚拟机栈(局部变量表、操作数栈(表达式计算过程中的中间结果,以及在方法调用和返回过程中传递参数和返回值)、动态链接(主要支持方法调用过程中的动态绑定)、返回地址、附加信息)和本地方法栈。

多进程适用于需要更严格的隔离和更高的稳定性的场景,而多线程适用于需要更轻量级的并发处理更高的资源利用率的场景(频繁切换、频繁销毁删除,联系后面回答)。进程创建需要分配地址而线程只需要创建一个栈。在上下文切换的时候,进程需要切换地址,而线程不需要。同时进程间通信需要用到IPC通信协议(有名管道、无名管道、信号、消息队列、sockets、共享内存等),而线程之间只需要共享内存和消息通信就可以。但是有数据不安全问题!!

多进程可以发挥多核处理器性能,多线程可以将耗时的放在后台执行,以确保流畅性。

讲讲协程?

协程是一种用户态的轻量级线程,不由操作系统内核管理,由应用程序控制和调度,提供了更细粒度的并发控制,切换不需要切换上下文和保存/恢复状态,开销更小。

JDK21虚拟线程:

Tomcat11支持JDK21虚拟线程,用户态线程

在原本的线程模型里,为了提高并发量,就是采用了异步非阻塞的方式,但是不方便调试。

创建和销毁通过线程池解决了,但是线程多了频繁的切换线程怎么解决呢?

通过虚拟线程解决,无限放大了并发量。

当一个线程阻塞的时候就会被调度到其它平台线程。

主要是IO密集型任务阻塞的时候,不适合CPU密集型,只是增大了规模。不需要池化,用完即抛。

线程创建方式?

继承Thread类,实现Runable接口,实现Callable接口配合FutureTask。本质都是Runable;

为什么要调用start方法,不直接使用run方法?

直接调用run方法就是在当前线程中串行执行,而start会创建出一个新的线程进行任务处理。

线程的状态?

操作系统层面:

java层面:

常用调度方法?

什么是线程上下文切换?什么是进程上下文切换?

线程:

分为相同进程和不同进程:不同进程就和进程切换一样,同一个进程CPU资源的分配采用了时间片轮换,当当前线程的时间片结束后,就会从运行状态切换到就绪状态,让给其他线程。信息保留在程序计数器中,切换到另一个栈帧。包括寄存器里的私有数据。

进程:CPU资源从一个进程切换到另一个进程,需要保留内存空间的指针,运行指令等等,再读入下个进程。

PCB条目:进程标识符(PID唯一)、进程状态、程序计数器(下一条指令的地址)、CPU寄存器(当前进程的所有寄存器值,上下文切换的时候用到)、CPU调度信息(进程优先级和其它参数)、内存管理信息(进程地址空间的指针等)、IO状态等等。

孤儿进程?

守护线程?

线程分为守护线程和用户线程,当最后一个非守护线程结束的时后,jvm就退出。

线程之间的通信方式?

volatile关键字和synchronized关键字。保证可见性和有序性。

管道输入/输出流 用于线程间的通信

Thread.join() 因为是共享内存,所以也可以这样获得共享变量;

原子性、可见性、有序性及解决办法?

原子性:多线程操作共享资源,预期结果与最终结果一致,即一个线程操作,另一个线程不会影响到。联想i++这个操作,获取i,i+1,返回i三个操作,获取到i之后当前线程阻塞了,但是还有其他线程进行了操作

解决:synchronized拿锁串行、CAS会导致ABA问题,引入版本号、Lock类似与synchronized串行,volatile不行,可能还没刷新回去又拿了旧的,或者其他线程已经修改了,重复了。

可见性:CPU三级缓存(L1、L2独有,L3都有),没有及时同步,导致线程数据不一致问题。MESI 中间两个和主存一致,前后两个是更改过或者无效。要读取数据从ES那边。

解决:volatile,转换为汇编会加一个lock,缓存行的数据立即写回主内存,这个写回的数据在其他cpu缓存中标志无效。synchronized将同步代码块里的变量从CPU缓存行中移除,必须去拿新的数据,释放后也会立即更新。lock有volatile修饰的state字段,缓存行的数据也会和上面一样。final不可以修改,kafka生产者这样做的。

有序性:指令重排

解决:volatile有内存屏障,阻止屏障之前的写操作排到屏障之后,组织屏障之后的读操作放到屏障之前。

volatile重点!!!

重排序可以分为编译器重排序和处理器重排序,volatile 保证有序性,就是通过分别限制这两种类型的重排序。

  1. 在每个 volatile 写操作的前面插入一个StoreStore屏障
  2. 在每个 volatile 写操作的后面插入一个StoreLoad屏障
  3. 在每个 volatile 读操作的后面插入一个LoadLoad屏障
  4. 在每个 volatile 读操作的后面插入一个LoadStore屏障

ThreadLocal是什么?

每个线程都有一个ThreadLocalMap(堆中,线程对象本身在堆中,这个是线程对象里的,也在堆中),key为ThreadLocal,value为值,弱引用。

有内存泄漏,尤其在线程池中ThreadLocalMap生命周期太长,没有引用了但是这个线程还在运行,使用后调用remove();垃圾回收将弱引用回收,所引用value的强引用又回收不了,又拿不到。

那么key为什么要用弱引用呢?

通过判断Entry不为null,但是key为null判断当前对象应该被清理了。如果是强引用,在线程池中,无法被清理。减少内存泄漏,ThreadLocalMap内部有清理机制弱引用和显式remove()!!!

ThreadLocal结构?

虽然是map但是并没有实现map接口,结构和HashMap还是类似的。存储entry的数组,散列算法hash取余,每次创建一个ThreadLocal对象都会新增一个黄金分割数。带来的就是hash分布很均匀。

怎么解决hash冲突的?

开放地址法,被占了就去找下一个位置。

扩容机制?

上面已经提过ThreadLocalMap内部有清理机制(set、get、remove),当在调用set执行完清理之后,且entry数量也到了2/3的扩容阈值,就开始rehash(),rehash()会执行清理,还要根据条件判断是否扩容?扩容为两倍,通过重新计算hash,开放地址法迁移。

父子线程怎么共享数据?

InheritableThreadLocal,同样也是Thread里的一个变量,当这个变量不为空的时候,就会给子线程。

对java内存模型的理解?不熟!

主要用来定义多线程中变量的访问规则,解决变量的可见性、有序性和原子性问题。

那为什么要有自己的内存呢?不熟!

直接操作共享变量的话,会引发很多竞争,增加了线程安全问题的复杂度。

CPU为了执行效率,可能会对指令进行乱序执行(指令重排),在不影响最终执行结果的前提下,使得CPU有更大的自由度。

什么是指令重排?

为了提高性能,编译器和处理器通常会常常对指令做出重排序。

编译器重排序-指令集并行重排序-内存系统重排序

在单例模式中常常出现。

2、3步骤重排后导致返回未初始化。

线程同步?

为了安全,按照预定的次序访问共享资源,避免造成混乱。

sy和lock

sy和lock的区别?

sy是关键字,lock是对象

sy不用自动释放,lock要自动释放

lock没有锁升级,同时可以中断等待,有非公平锁,有读写锁,condition比wait那些功能强。

重磅!ReetrantLock源码分析!下面是1.8的自己看了下和新版差别还是有一些的!

首先是最重要的AQS!

JUC包下面的一个基类,JUC下很多比如RL、ThreadPoolExcutor,阻塞队列、CountDownLatch、CyclicBarrier都是基于AQS实现的!

有一个volatile修饰,并且采用CAS方式的int state的变量

AQS维护了一个双向链表,有head和tail,head属于是个虚拟节点,不存储Thread;

static final class Node {
    //等待模式
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;//共享、互斥
    //表示线程被取消,进入此状态的线程不会变成其他状态
        static final int CANCELLED =  1;
    //当前线程释放锁或资源时,需要通知后续的等待线程
        static final int SIGNAL    = -1;
    //表示线程已经被移动到Condition队列中了
        static final int CONDITION = -2;
    //用于表示当一个线程在某些同步器中成功释放资源或锁时,应该将信号传播给其他等待的线程。
//信号量用到了
        static final int PROPAGATE = -3;

    //0表示当前节点在阻塞队列中,等待着获取锁,把上面几个写到这里面
        volatile int waitStatus;


        volatile Node prev;


        volatile Node next;


        volatile Thread thread; 
}

公平锁流程:

lock方法:

// 非公平锁
final void lock() {
    // 上来就先基于CAS的方式,尝试将state从0改为1
    if (compareAndSetState(0, 1))
        // 获取锁资源成功,会将当前线程设置到exclusiveOwnerThread属性,代表是当前线程持有着锁资源
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 执行acquire,尝试获取锁资源
        acquire(1);
}

// 公平锁
final void lock() {
    //  执行acquire,尝试获取锁资源
    acquire(1);
}

acquire:

public final void acquire(int arg) {
    // tryAcquire:再次查看,当前线程是否可以尝试获取锁资源
    if (!tryAcquire(arg) &&
        // 没有拿到锁资源
        // acquireQueued:查看我是否是第一个排队的节点,如果是可以再次尝试获取锁资源,如果长时间拿不到,挂起线程
        // 如果不是第一个排队的额节点,就尝试挂起线程即可
        acquireQueued(
        // addWaiter(Node.EXCLUSIVE):将当前线程封装为Node节点,插入到AQS的双向链表的结尾
        addWaiter(Node.EXCLUSIVE), arg))
        // 中断线程的操作
        selfInterrupt();
}

tryacquire就不一样了,非公平锁是判断state是否为0然后直接抢锁,后面有一些重入锁的逻辑,而公平锁需要在state=0的情况下去判断等待队列里面有没有排队的,没有或者自己是第一个就去拿一下,后面也是锁重入。

// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取了state熟属性
    int c = getState();
    // 判断state当前是否为0,之前持有锁的线程释放了锁资源
    if (c == 0) {
        // 再次抢一波锁资源
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            // 拿锁成功返回true
            return true;
        }
    }
    // 不是0,有线程持有着锁资源,如果是,证明是锁重入操作
    else if (current == getExclusiveOwnerThread()) {
        // 将state + 1
        int nextc = c + acquires;
        if (nextc < 0) // 说明对重入次数+1后,超过了int正数的取值范围
            // 01111111 11111111 11111111 11111111
            // 10000000 00000000 00000000 00000000
            // 说明重入的次数超过界限了。
            throw new Error("Maximum lock count exceeded");
        // 正常的将计算结果,复制给state
        setState(nextc);
        // 锁重入成功
        return true;
    }
    // 返回false
    return false;
}
// 公平锁实现
   protected final boolean tryAcquire(int acquires) {
       // 获取当前线程
       final Thread current = Thread.currentThread();
       // ....
       int c = getState();
       if (c == 0) {
           // 查看AQS中是否有排队的Node
           // 没人排队抢一手 。有人排队,如果我是第一个,也抢一手
           if (!hasQueuedPredecessors() &&
               // 抢一手~
               compareAndSetState(0, acquires)) {
               setExclusiveOwnerThread(current);
               return true;
           }
       }
       // 锁重入~~~
       else if (current == getExclusiveOwnerThread()) {
           int nextc = c + acquires;
           if (nextc < 0)
               throw new Error("Maximum lock count exceeded");
           setState(nextc);
           return true;
       }
       return false;
   }

   // 查看是否有线程在AQS的双向队列中排队
   // 返回false,代表没人排队
   public final boolean hasQueuedPredecessors() {
       // 头尾节点
       Node t = tail; 
       Node h = head;
       // s为头结点的next节点
       Node s;
       // 如果头尾节点相等,证明没有线程排队,直接去抢占锁资源
       return h != t &&
           // s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我)
           (s == null || s.thread != Thread.currentThread());
   }

try完了就是进入下一个判断addWaiter,公平不公平都得走这里,封装成节点,加到尾巴上

// 没有拿到锁资源,过来排队,  mode:代表互斥锁
private Node addWaiter(Node mode) {
    // 将当前线程封装为Node,
    Node node = new Node(Thread.currentThread(), mode);
    // 拿到尾结点
    Node pred = tail;
    // 如果尾结点不为null
    if (pred != null) {
        // 当前节点的prev指向尾结点
        node.prev = pred;
        // 以CAS的方式,将当前线程设置为tail节点
        if (compareAndSetTail(pred, node)) {
            // 将之前的尾结点的next指向当前节点
            pred.next = node;
            return node;
        }
    }
    // 如果CAS失败,以死循环的方式,保证当前线程的Node一定可以放到AQS队列的末尾
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        // 拿到尾结点
        Node t = tail;
        // 如果尾结点为空,AQS中一个节点都没有,构建一个伪节点,作为head和tail
        if (t == null) { 
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 比较熟悉了,以CAS的方式,在AQS中有节点后,插入到AQS队列的末尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

最后就是acquireQueued方法,一直在循环里面,此时已经是尾巴了,看看前面那个节点是不是头,是就再获取一下资源。不是就尝试挂起,挂起要判断前面节点的状态是不是正常的,>0都不正常!

// 当前没有拿到锁资源后,并且到AQS排队了之后触发的方法。  中断操作这里不用考虑
final boolean acquireQueued(final Node node, int arg) {
    // 不考虑中断
    // failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 死循环…………
        for (;;) {
            // 拿到当前节点的前继节点
            final Node p = node.predecessor();
            // 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。
            if (p == head && tryAcquire(arg)) {
                // 获取锁资源成功
                // 设置头结点为当前获取锁资源成功Node,并且取消thread信息
                setHead(node);
                // help GC
                p.next = null; 
                // 获取锁失败标识为false
                failed = false;
                return interrupted;
            }
            // 没拿到锁资源……
            // shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,
            // 如果不能,就返回false,继续下次循环
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 这里基于Unsafe类的park方法,将当前线程挂起
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            // 在lock方法中,基本不会执行。
            cancelAcquire(node);
    }
}
// 获取锁资源成功后,先执行setHead
private void setHead(Node node) {
    // 当前节点作为头结点  伪
    head = node;
    // 头结点不需要线程信息
    node.thread = null;
    node.prev = null;
}

// 当前Node没有拿到锁资源,或者没有资格竞争锁资源,看一下能否挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点
    // 1,CANCELLED状态:代表当前节点以及取消了
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 上一个节点为-1之后,当前节点才可以安心的挂起线程
        return true;
    if (ws > 0) {
        // 如果当前节点的上一个节点是取消状态,我需要往前找到一个状态不为1的Node,作为他的next节点
        // 找到状态不为1的节点后,设置一下next和prev
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

trylock都是非公平锁的逻辑,拿不到就算了,不等。

trylock(time),可以中断,分为公平锁和非公平锁逻辑,拿不到就看时间是否挂起,没时间就算了,有时间就挂起这么久,但是唤醒的时候要看是中断、到时间了、被唤醒,前两个要取消节点。

取消节点分析:

这时候得看是哪个位置?尾巴?头后面?还是中间的?不同的处理逻辑,

// 取消在AQS中排队的Node
private void cancelAcquire(Node node) {
    // 如果当前节点为null,直接忽略。
    if (node == null)
        return;
    //1. 线程设置为null
    node.thread = null;

    //2. 往前跳过被取消的节点,找到一个有效节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    //3. 拿到了上一个节点之前的next
    Node predNext = pred.next;

    //4. 当前节点状态设置为1,代表节点取消
    node.waitStatus = Node.CANCELLED;

    // 脱离AQS队列的操作
    // 当前Node是尾结点,将tail从当前节点替换为上一个节点
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // 到这,上面的操作CAS操作失败
        int ws = pred.waitStatus;
        // 不是head的后继节点
        if (pred != head &&
            // 拿到上一个节点的状态,只要上一个节点的状态不是取消状态,就改为-1
            (ws == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) 
            && pred.thread != null) {
            // 上面的判断都是为了避免后面节点无法被唤醒。
            // 前继节点是有效节点,可以唤醒后面的节点
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 当前节点是head的后继节点
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

lockInterruptibly方法,拿不到锁就死等,要么被唤醒,要么被中断,中断唤醒抛异常,取消节点。

释放锁:

流程就是释放state,看head的waitstatus去唤醒后面有效的节点,从后往前找最近的节点,因为在取消节点那里存在前面找不到后面,但是后面指着前面。

public void unlock() {
    // 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
    sync.release(1);
}

// 释放锁的核心流程
public final boolean release(int arg) {
    // 核心释放锁资源的操作之一
    if (tryRelease(arg)) {
        // 如果锁已经释放掉了,走这个逻辑
        Node h = head;
        // h不为null,说明有排队的(录课时估计脑袋蒙圈圈。)
        // 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。
        if (h != null && h.waitStatus != 0)
            // 唤醒排队的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// ReentrantLock释放锁资源操作
protected final boolean tryRelease(int releases) {
    // 拿到state - 1(并没有赋值给state)
    int c = getState() - releases;
    // 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // free,代表当前锁资源是否释放干净了。
    boolean free = false;
    if (c == 0) {
        // 如果state - 1后的值为0,代表释放干净了。
        free = true;
        // 将持有锁的线程置位null
        setExclusiveOwnerThread(null);
    }
    // 将c设置给state
    setState(c);
    // 锁资源释放干净返回true,否则返回false
    return free;
}

// 唤醒后面排队的Node
private void unparkSuccessor(Node node) {
    // 拿到头节点状态
    int ws = node.waitStatus;
    if (ws < 0)
        // 先基于CAS,将节点状态从-1,改为0
        compareAndSetWaitStatus(node, ws, 0);
    // 拿到头节点的后续节点。
    Node s = node.next;
    // 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境
        for (Node t = tail; t != null && t != node; t = t.prev)
            // 从后往前找到状态小于等于0的节点
            // 找到离head最新的有效节点,并赋值给s
            if (t.waitStatus <= 0)
                s = t;
    }
    // 只要找到了这个需要被唤醒的节点,执行unpark唤醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

ConditionObject

为了实现wait和notify的方法

await:

单向链表!同样是用AQS的Node实现的,但是会判断中断,没中断就会加入单向链表里面,同时也会干掉取消了的Node,状态设置为-2,然后挂起,释放state

当这个线程重新启动的时候,看是中断唤醒还是被唤醒,或者是唤醒后被中断了,根据中断标志位和状态去判断去操作,因为正常唤醒的都在AQS队列,后面就需要去移除condition这些之类的

signal:

只有持有锁资源的线程才能signal,脱离队列,改状态0,加入AQS队列

ReetrantReadWriteLock

这里是共享模式的AQS

写互斥、读共享,读基于state的高16位置,写基于低16位

阻塞队列-只写我总结的点,源码不写了

ArrayBlockingQueue:

基于ReentrantLock实现的,一把锁,同时是循环数组,生产者消费者有各自的Condition,简化并发操作但是性能不行,因为通过条件变量去控制,两把锁无法保证并发安全,例如count;

线程挂起被唤醒的时候采用的while防止虚假唤醒,适合CPU密集

LinkedBlockingQueue:

有哨兵节点,两把锁,两个Condition互相唤醒,两把锁为了性能,适合IO密集

PriorityBlockingQueue:

数组实现的无界二叉堆(可自定义比较器),最大容量为Integer.MAX_VALUE - 8,分虚拟机,一把锁一个Condition,通过一个volatile变量CAS实现扩容,此时会释放锁,小于64会2倍,大于64会1.5倍,长度超过最大就会失败,其他的线程就等待新数组生成写新数组,会调整结构所以一把锁。

DelayQueue:

一把锁,一个Condition,同理上面,基于优先级队列实现的,通过condition的awaitnanos去等

SynchronousQueue:

存消费者和生产者的,进行匹配,主要是内部类Transfer,两种实现方式TransferStack、TransferQueue前者非公平,后者公平,比较少,就不看了。。。

线程池!

五种线程池

固定线程池newFixedThreadPool固定线程数,LinkedBlockingQueue;

单例线程池newSingleThreadPool一个线程,LinkedBlockingQueue;

缓存线程池newCachedThreadPool,核心线程0,最大线程无限,使用的SynchronousQueue,只要提交工作就有工作线程能处理,使用SynchronousQueue的原因是理念相似,快速任务处理,避免累积任务,减少资源占用,同时动态调整;

定时任务线程池newScheduleThreadPool,基于DelayQueue

newWorkStealingPool,是基于ForkJoinPool构造出来的,有多个阻塞队列,分而治之

线程池源码!

七个参数:

public ThreadPoolExecutor(
    int corePoolSize,           // 核心工作线程(当前任务执行结束后,不会被销毁)
    int maximumPoolSize,        // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
    long keepAliveTime,         // 非核心工作线程在阻塞队列位置等待的时间
    TimeUnit unit,              // 非核心工作线程在阻塞队列位置等待时间的单位
    BlockingQueue<Runnable> workQueue,   // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
    ThreadFactory threadFactory,         // 构建线程的线程工作,可以设置thread的一些信息
    RejectedExecutionHandler handler) {  // 当线程池无法处理投递过来的任务时,执行当前的拒绝策略
    // 初始化线程池的操作
}

四种拒绝策略:

AbortPolicy:当前拒绝策略会在无法处理任务时,直接抛出一个异常

CallerRunsPolicy:当前拒绝策略会在线程池无法处理任务时,将任务交给调用者处理

DiscardPolicy:当前拒绝策略会在线程池无法处理任务时,直接将任务丢弃掉

DiscardOldestPolicy:当前拒绝策略会在线程池无法处理任务时,将队列中最早的任务丢弃掉,将当前任务再次尝试交给线程池处理

自定义实现接口。

  1. 核心属性ctl的AtomicInteger是volatile修饰的
  2. public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    //截取自原子类!!!!!!!!!!
        private volatile int value;

  3. // 当前是线程池的核心属性
    // 当前的ctl其实就是一个int类型的数值,内部是基于AtomicInteger套了一层,进行运算时,是原子性的。
    // ctl表示着线程池中的2个核心状态:
    // 线程池的状态:ctl的高3位,表示线程池状态
    // 工作线程的数量:ctl的低29位,表示工作线程的个数
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    
    // Integer.SIZE:在获取Integer的bit位个数
    // 声明了一个常量:COUNT_BITS = 29
    private static final int COUNT_BITS = Integer.SIZE - 3;
    00000000 00000000 00000000 00000001
    00100000 00000000 00000000 00000000
    00011111 11111111 11111111 11111111
    // CAPACITY就是当前工作线程能记录的工作线程的最大个数
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
    
    // 线程池状态的表示
    // 当前五个状态中,只有RUNNING状态代表线程池没问题,可以正常接收任务处理
    // 111:代表RUNNING状态,RUNNING可以处理任务,并且处理阻塞队列中的任务。
    private static final int RUNNING    = -1 << COUNT_BITS;
    // 000:代表SHUTDOWN状态,不会接收新任务,正在处理的任务正常进行,阻塞队列的任务也会做完。
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    // 001:代表STOP状态,不会接收新任务,正在处理任务的线程会被中断,阻塞队列的任务一个不管。
    private static final int STOP       =  1 << COUNT_BITS;
    // 010:代表TIDYING状态,这个状态是否SHUTDOWN或者STOP转换过来的,代表当前线程池马上关闭,就是过渡状态。
    private static final int TIDYING    =  2 << COUNT_BITS;
    // 011:代表TERMINATED状态,这个状态是TIDYING状态转换过来的,转换过来只需要执行一个terminated方法。
    private static final int TERMINATED =  3 << COUNT_BITS;
    
    // 在使用下面这几个方法时,需要传递ctl进来
    
    // 基于&运算的特点,保证只会拿到ctl高三位的值。
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    // 基于&运算的特点,保证只会拿到ctl低29位的值。
    private static int workerCountOf(int c)  { return c & CAPACITY; }

    高三位:线程池状态,低29位:最大线程数,-1到3五个状态,-1为running

  4. 有参构造主要是健壮性校验,核心线程是可以为0的哦

    // 有参构造。无论调用哪个有参构造,最终都会执行当前的有参构造
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        // 健壮性校验
        // 核心线程个数是允许为0个的。
        // 最大线程数必须大于0,最大线程数要大于等于核心线程数
        // 非核心线程的最大空闲时间,可以等于0
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            // 不满足要求就抛出参数异常
            throw new IllegalArgumentException();
        // 阻塞队列,线程工厂,拒绝策略都不允许为null,为null就扔空指针异常
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        // 不要关注当前内容,系统资源访问决策,和线程池核心业务关系不大。
        this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
        // 各种赋值,JUC包下,几乎所有涉及到线程挂起的操作,单位都用纳秒。
        // 有参构造的值,都赋值给成员变量。
        // Doug Lea的习惯就是将成员变量作为局部变量单独操作。
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  5. excute方法,如果当前线程数量小于核心线程,就会增加工作线程,addWorker是可以通过参数控制创建核心线程还是普通线程的

    // 提交任务到线程池的核心方法
    // command就是提交过来的任务
    public void execute(Runnable command) {
        // 提交的任务不能为null
        if (command == null)
            throw new NullPointerException();
        // 获取核心属性ctl,用于后面的判断
        int c = ctl.get();
        // 如果工作线程个数,小于核心线程数。
        // 满足要求,添加核心工作线程
        if (workerCountOf(c) < corePoolSize) {
            // addWorker(任务,是核心线程吗)
            // addWorker返回true:代表添加工作线程成功
            // addWorker返回false:代表添加工作线程失败
            // addWorker中会基于线程池状态,以及工作线程个数做判断,查看能否添加工作线程
            if (addWorker(command, true))
                // 工作线程构建出来了,任务也交给command去处理了。
                return;
            // 说明线程池状态或者是工作线程个数发生了变化,导致添加失败,重新获取一次ctl
            c = ctl.get();
        }
        // 添加核心工作线程失败,往这走
        // 判断线程池状态是否是RUNNING,如果是,正常基于阻塞队列的offer方法,将任务添加到阻塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            // 如果任务添加到阻塞队列成功,走if内部
            // 如果任务在扔到阻塞队列之前,线程池状态突然改变了。
            // 重新获取ctl
            int recheck = ctl.get();
            // 如果线程池的状态不是RUNNING,将任务从阻塞队列移除,
            if (!isRunning(recheck) && remove(command))
                // 并且直接拒绝策略
                reject(command);
            // 在这,说明阻塞队列有我刚刚放进去的任务
            // 查看一下工作线程数是不是0个
            // 如果工作线程为0个,需要添加一个非核心工作线程去处理阻塞队列中的任务
            // 发生这种情况有两种:
            // 1. 构建线程池时,核心线程数是0个。
            // 2. 即便有核心线程,可以设置核心线程也允许超时,设置allowCoreThreadTimeOut为true,代表核心线程也可以超时
            else if (workerCountOf(recheck) == 0)
                // 为了避免阻塞队列中的任务饥饿,添加一个非核心工作线程去处理
                addWorker(null, false);
        }
        // 任务添加到阻塞队列失败
        // 构建一个非核心工作线程
        // 如果添加非核心工作线程成功,直接完事,告辞
        else if (!addWorker(command, false))
            // 添加失败,执行决绝策略
            reject(command);
    }

    通过上面可以看到,当添加核心线程失败的时候,可能会出现多种情况,第一种是ctl变化,可能是状态变化,具体看下面的addWorker。然后回去判断状态以及添加进阻塞队列,并不是直接就去创建工作线程!如果状态变化就会执行拒绝策略和移除任务!如果是running或者任务移除失败,也要确保有线程可以去执行任务!如果添加阻塞队列失败就会添加线程,添加失败就执行拒绝策略!就是说当阻塞队列满了或者是运行状态为shutdown才会去创建新线程的。

  6. Worker:

    // Worker继承了AQS,目的就是为了控制工作线程的中断。
    // Worker实现了Runnable,内部的Thread对象,在执行start时,必然要执行Worker中断额一些操作
    private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
      
    // =======================Worker管理任务================================  
        // 线程工厂构建的线程
        final Thread thread;
    
        // 当前Worker要执行的任务
        Runnable firstTask;
    
        // 记录当前工作线程处理了多少个任务。
        volatile long completedTasks;
    
        // 有参构造
        Worker(Runnable firstTask) {
            // 将State设置为-1,代表当前不允许中断线程
            setState(-1); 
            // 任务赋值
            this.firstTask = firstTask;
            // 基于线程工作构建Thread,并且传入的Runnable是Worker
            this.thread = getThreadFactory().newThread(this);
        }
    
        // 当thread执行start方法时,调用的是Worker的run方法,
        public void run() {
            // 任务执行时,执行的是runWorker方法
            runWorker(this);
        }
    
    
    // =======================Worker管理中断================================   
        // 当前方法是中断工作线程时,执行的方法
        void interruptIfStarted() {
            Thread t;
            // 只有Worker中的state >= 0的时候,可以中断工作线程
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    // 如果状态正常,并且线程未中断,这边就中断线程
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
      
        protected boolean isHeldExclusively() {
            return getState() != 0;
        }
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }
    
      
    }

    runworker:总而言之就是在while里面不断循环执行任务,其中有很多状态判断以及超时检验,拿不到任务就会销毁,销毁的可能是核心线程,可能是非核心线程,看哪个线程没任务了

    // 工作线程启动后执行的任务。
    final void runWorker(Worker w) {
        // 拿到当前线程
        Thread wt = Thread.currentThread();
        // 从worker对象中拿到任务
        Runnable task = w.firstTask;
        // 将Worker中的firstTask置位空
        w.firstTask = null;
        // 将Worker中的state置位0,代表当前线程可以中断的
        w.unlock(); // allow interrupts
        // 判断工作线程是否是异常结束,默认就是异常结束
        boolean completedAbruptly = true;
        try {
            // 获取任务
            // 直接拿到第一个任务去执行
            // 如果第一个任务为null,去阻塞队列中获取任务
            while (task != null || (task = getTask()) != null) {
                // 执行了Worker的lock方法,当前在lock时,shutdown操作不能中断当前线程,因为当前线程正在处理任务
                w.lock();
                // 比较ctl >= STOP,如果满足找个状态,说明线程池已经到了STOP状态甚至已经要凉凉了
                // 线程池到STOP状态,并且当前线程还没有中断,确保线程是中断的,进到if内部执行中断方法
                // if(runStateAtLeast(ctl.get(), STOP) && !wt.isInterrupted()) {中断线程}
                // 如果线程池状态不是STOP,确保线程不是中断的。
                // 如果发现线程中断标记位是true了,再次查看线程池状态是大于STOP了,再次中断线程
                // 这里其实就是做了一个事情,如果线程池状态 >= STOP,确保线程中断了。
                if (
                    (
                        runStateAtLeast(ctl.get(), STOP) ||  
                        (     Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)   )
                    )
                    && !wt.isInterrupted())
                    wt.interrupt();
                try {
                    // 勾子函数在线程池中没有做任何的实现,如果需要在线程池执行任务前后做一些额外的处理,可以重写勾子函数
                    // 前置勾子函数
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        // 执行任务。
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        // 前后置勾子函数
                        afterExecute(task, thrown);
                    }
                } finally {
                    // 任务执行完,丢掉任务
                    task = null;
                    // 当前工作线程处理的任务数+1
                    w.completedTasks++;
                    // 执行unlock方法,此时shutdown方法才可以中断当前线程
                    w.unlock();
                }
            }
            // 如果while循环结束,正常走到这,说明是正常结束
            // 正常结束的话,在getTask中就会做一个额外的处理,将ctl - 1,代表工作线程没一个。
            completedAbruptly = false;
        } finally {
            // 考虑干掉工作线程
            processWorkerExit(w, completedAbruptly);
        }
    }
    // 工作线程结束前,要执行当前方法
    private void processWorkerExit(Worker w, boolean completedAbruptly) {
        // 如果是异常结束
        if (completedAbruptly) 
            // 将ctl - 1,扣掉一个工作线程
            decrementWorkerCount();
    
        // 操作Worker,为了线程安全,加锁
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 当前工作线程处理的任务个数累加到线程池处理任务的个数属性中
            completedTaskCount += w.completedTasks;
            // 将工作线程从hashSet中移除
            workers.remove(w);
        } finally {
            // 释放锁
            mainLock.unlock();
        }
    
        // 只要工作线程凉了,查看是不是线程池状态改变了。
        tryTerminate();
    
        // 获取ctl
        int c = ctl.get();
        // 判断线程池状态,当前线程池要么是RUNNING,要么是SHUTDOWN
        if (runStateLessThan(c, STOP)) {
            // 如果正常结束工作线程
            if (!completedAbruptly) {
                // 如果核心线程允许超时,min = 0,否则就是核心线程个数
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                // 如果min == 0,可能会出现没有工作线程,并且阻塞队列有任务没有线程处理
                if (min == 0 && ! workQueue.isEmpty())
                    // 至少要有一个工作线程处理阻塞队列任务
                    min = 1;
                // 如果工作线程个数 大于等于1,不怕没线程处理,正常return
                if (workerCountOf(c) >= min)
                    return; 
            }
            // 异常结束,为了避免出现问题,添加一个空任务的非核心线程来填补上刚刚异常结束的工作线程
            addWorker(null, false);
        }
    }

死锁发生?避免死锁?排查死锁?

  • 互斥条件:资源不能被多个线程共享,一次只能由一个线程使用。如果一个线程已经占用了一个资源,其他请求该资源的线程必须等待,直到资源被释放。
  • 持有并等待条件:一个线程至少已经持有至少一个资源,且正在等待获取额外的资源,这些额外的资源被其他线程占有。
  • 不可剥夺条件:资源不能被强制从一个线程中抢占过来,只能由持有资源的线程主动释放。
  • 循环等待条件:存在一种线程资源的循环链,每个线程至少持有一个其他线程所需要的资源,然后又等待下一个线程所占有的资源。这形成了一个循环等待的环路

jstack pid,可视化工具查看死锁

并发集合?

线程安全的集合该怎么选择?

List、Set线程安全的有Vector、Collections.synchronizedList、CopyOnWrite系列(lock)。

数据量大怎么考虑?

不考虑CopyOnWrite,因为在写操作的时候会复制一套副本,在副本中进行操作,读操作读原生数据,因为要复制。

不在业务代码实现的话,就选择Collections.synchronizedList

也可以仿照Collections.synchronizedList,直接做一个装饰着模式,套一层

多列集合就用ConCurrent系列;

ConcurrentHashMap存储数据结构是什么样子呢?

数组+链表+红黑树

链表到红黑树是8,红黑树到链表是6,红黑树上也维持着双向链表!

可以用跳表吗?

可以,但是不支持修改结构,同时跳表空间占用率较高,但是写效率不像红黑树这么复杂。

ConcurrentHashMap的负载因子可以重新指定吗?

final修饰,不能改,同时没有基于负载因子计算阈值,是直接位运算的,改不改没区别,HashMap可以改,小了导致频繁扩容,大了导致冲突多,同时也要重新计算泊松比。

ConcurrentHashMap的散列算法?相比HashMap有什么区别?

都是对key进行hashCode进行取模高16位和低16位进行异或,但是高位还是保留着的!!!,就是对长度-1进行与运算。

源码:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ConcurrentHashMap不允许key或者value出现为null的值,跟HashMap的区别
    if (key == null || value == null) throw new NullPointerException();
    // 根据key的hashCode计算出一个hash值,后期得出当前key-value要存储在哪个数组索引位置
    int hash = spread(key.hashCode());
    // 一个标识,在后面有用!
    int binCount = 0;
    // 省略大量的代码……
}

// 计算当前Node的hash值的方法
static final int spread(int h) {
    // 将key的hashCode值的高低16位进行^运算,最终又与HASH_BITS进行了&运算
    // 将高位的hash也参与到计算索引位置的运算当中
    // 为什么HashMap、ConcurrentHashMap,都要求数组长度为2^n
    // HASH_BITS让hash值的最高位符号位肯定为0,代表当前hash值默认情况下一定是正数,因为hash值为负数时,有特殊的含义
    // static final int MOVED     = -1; // 代表当前hash位置的数据正在扩容!
    // static final int TREEBIN   = -2; // 代表当前hash位置下挂载的是一个红黑树
    // static final int RESERVED  = -3; // 预留当前索引位置……
    return (h ^ (h >>> 16)) & HASH_BITS;
    // 计算数组放到哪个索引位置的方法   (f = tabAt(tab, i = (n - 1) & hash)
    // n:是数组的长度
}

sizeCtl作用?

初始化和扩容的一个控制作用。-1代表正在初始化,-n代表正在扩容并代表代表有n-1个线程正在扩容。=0啥事没有,>0两个意思要么没初始化代表初始化数组的长度,初始化了代表下次扩容的阈值。

扩容整体流程?

计算扩容标识戳,包括sizeCtl、transferIndex、nextTable等,一起协调扩容过程,到达sizeCtl阈值开始扩容。

第一个扩容的线程--给sizeCtl赋值,计算自己的步长,初始化为两倍开始迁移,一直循环领迁移任务,直到结束。

后面有线程进来就将sizeCtl+1,计算步长、领取任务、循环扩容结束。

扩容标识戳?

基于oldTable长度计算出来的,第十七位一定是1,然后左移15位,第一个进来+2,后面进来+1,然后赋值给sizeCtl。

那为啥基于长度计算?

确保扩容的线程,要和正在扩容的线程要扩成一样的。

如何统计个数?

为了确保安全以及效率,选择的是LongAdder,CAS++,--操作,同时还有一个CounterCells数组,多位置记录。

ConcurrentHashMap在JDK1.7和1.8如何保证写数据线程安全?

1.7基于ReentrantLock实现的,一把锁锁多个格子;

1.8基于CAS和Synchronized,CAS填格子,Syn锁Node;

扩容期间可以查询数据吗?

直接在老数组上查,扩容完成会有一个ForwardingNode,代表去新数组了。

为何保留一套双向链表?

写操作时,为了平衡,会左旋和右旋,指针会变化,但是有双向链表就方便读了;

迁移的时候,以及退化的时候也很方便。

三个工具类:

CountDownLatch:

简化理解旅游团,大家自己去玩,司机在等待,最后来一个countdown一个,然后==0了一起回家。基于AQS

就是state,await就是插入双向链表挂起线程,如果countdown完之后state为0就去唤醒等待的节点。同时也要看状态-1唤醒,然后改成0避免重复唤醒。

CyclicBarrier

简化理解为旅游团,大家全部集合完了一起出发,基于ReentrantLock,用来保证count,可以重复使用,如果有等待线程中断,可以抛出异常,避免无限等待。线程中断后唤醒所有线程,然后使用reset()就能重复使用了。

Semaphore

基于AQS的State做一个维护,保证一个资源可以被多个线程访问。做一些流量控制,-3表示是否应该唤醒多个线程同时获得资源。

  • 28
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值