八股文—JAVA并发

JAVA并发八股文

JAVA并发类图

在这里插入图片描述

线程基础

线程状态

Thread.State枚举类中将线程分为6种状态,从java语言层面将线程分为6种状态
在这里插入图片描述

  • NEW、RUNNABLE、BLOCKED、TERMINATED

  • BLOCKED
    被阻止等待监视器锁定的线程的线程状态。
    处于阻塞状态的线程正在等待监视器锁进入同步的块/方法。
    或者在调用Object.wait后重新进入同步块/方法。

  • WAITING:
    由于调用以下方法之一,线程处于等待状态:
    Object.wait with no timeout
    Thread.join with no timeout
    LockSupport.park
    处于等待状态的线程正在等待另一个线程执行特定操作。例如,对某个对象调用了Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()或Object.notifyAll()。一个调用了thread.join()的线程正在等待指定的线程终止。

  • TIMED_WAITING:
    具有指定等待时间的等待线程的线程状态。由于使用指定的正等待时间调用以下方法之一,线程处于定时等待状态:
    Thread.sleep
    Object.wait with timeout
    Thread.join with timeout
    LockSupport.parkNanos
    LockSupport.parkUntil

Monitor

Monitor地址的存储

monitor地址被存储于 java对象markword中的区段,ptr_to_heavyweight_monitor存储monitor对象的地址
在这里插入图片描述

Monitor概念

监视器或者管程,Monitor是重量级锁,每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)锁之后,该对象的Mark Word中就被设置指向monitor对象的指针
在这里插入图片描述
刚开始Monitor中Owner为null
当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
Thread-2上锁过程中,如果Thread-3、Thread-4、Thread-5也来执行synchronized(obj)就会进入EntryList 阻塞队列
Thread-2执行完同步代码后,唤醒 EntryList中等待的线程来竞争锁,竞争的锁是非公平的
WaitSet中的Thread-0和Thread-1是之前获得过锁,但是条件不满足的,进入Waiting状态的线程

JVM锁

轻量级加锁

锁记录(Lock Record)对象:每个线程的栈帧都包含一个锁记录的结构,内部可以存储锁对象的Mark Word
在这里插入图片描述
让锁记录中object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
在这里插入图片描述
如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,
状态00表示轻量级锁(可以查看Monitor中对象头State内容)
在这里插入图片描述

如果cas失败,有两种情况
1)如果是其他线程已经持有了该Object的轻量级锁,这表明锁有竞争,进入锁膨胀过程
2)如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数(同一个线程,对同一个对象加了N次锁,就会有N个锁记录的个数)
在这里插入图片描述
当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
在这里插入图片描述

当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头。
1)成功,则解锁成功
2)失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

在尝试加轻量级锁过程中,CAS操作无法成功,这时有一种情况就是其他线程为对象加上了轻量级锁(有竞争),这时需要进行锁膨胀
当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁(锁状态不是01,是00表示已经加上轻量级锁,cas操作注定失败)
在这里插入图片描述
这时Thread-1加轻量级锁失败,进入锁膨胀流程
1)为Object对象申请Monitor锁,让Object指向重量级锁地址
2)然后自己进入Monitor的EntryList队列进行阻塞
在这里插入图片描述
Thread-0退出同步代码块时,使用cas将Mark Word值恢复给对象头,但是会失败,这时会进入重量级锁解锁流程
即按照Monitor地址找到Monitor对象,设置Owner的值为null,唤醒EntryList中的BLOCKED线程。
注意:这边还有一个问题,解锁后的Thread-0的Lock Record应该归还Hashcode Age Bias 01给Object对象的,这边貌似没有提到,不知道具体实现方式

锁自旋(优化)
  • 重量级锁竞争时,还可以使用自旋来进行优化,如果当前线程自旋成功(即持锁线程已经退出同步块,释放了锁),这时当前线程可以避免阻塞(阻塞的坏处)
  • JAVA6以后自旋锁是自适应的,如果刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次,反之就少自旋或者不自旋
  • 自旋会占用CPU时间,单核CPU自旋就是浪费计算资源,多核CPU才能发挥作用,JAVA7之后不能控制是否开启自旋功能
    在这里插入图片描述
    在这里插入图片描述
锁偏向(优化)

情景:单一线程一直获取到当前锁对象
在轻量级锁时,每次线程对对象进行加锁时,都会使用CAS操作将 线程栈帧中的锁记录 替换成锁对象的 markword,这个操作实际上也会有性能损耗,而偏向锁用来解决这种损耗,不用重新CAS
在这里插入图片描述
在这里插入图片描述

在回顾markword的结构,biased_lock为1 的时候,就是启用了偏向锁在这里插入图片描述

锁偏向的撤销

如果调用了对象的hashcode方法,但偏向锁对象的markword存储的是线程ID,会导致偏向锁被撤销,因为第一次调用hashcode会复制到markword上(这或许也解释了为什么解锁后线程lock record不需要归还hashcode给对象
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
调用wait/notify方法会撤销偏向锁,因为wait notify只有重量级锁才会有,entryList阻塞队列

线程操作

Object.wait & Object.notify

在这里插入图片描述
entryList:竞争锁的线程队列
waitSet:等待着被唤醒进入竞争锁线程队列的线程集合

owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
BLOCKED和WAITING线程都处于阻塞状态,不占用CPU时间
BLOCKED线程会在线程释放锁时唤醒
WAITING线程会在owner线程调用notify或者notifyAll时唤醒,被唤醒后进入EntryList重新竞争锁
notify会在waitSet中挑一个唤醒
notifyAll会唤醒锁有waitSet中的线程

LockSupport
  • wait,notify和notifyAll必须配合Object Monitor一起使用,而unpark不必
  • LockSupport.park & LockSupport.unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒锁有等待线程,就不那么【精确】
  • LockSupport.park & LockSupport.unpark可以先LockSupport.unpark,而wait & notify不能先notify
Thread t1=new Thread(()->{
	sleep(1);
	LockSupport.park();
},"t1");
sleep(2);
// 可以唤醒指定线程
LockSupport.unpark(t1);

每个线程都有自己的一个Parker对象,由三部分组成_counter,_cond和_mutex
调用park会让_counter=_counter-1(_counter>0),如果_counter=0则会阻塞
调用unpark会使得_counter=1
这就解释了为什么可以先unpark,再park
先unpark会使得_counter=1,再park时_counter–(_counter>0)成功,则线程可以继续往下执行
在这里插入图片描述

与interrupt()联动

public void interrupt(){} 给线程打一个 中断标志=true
public boolean isInterrupted(){} 检测下线程是否被中断
public static boolean interrupted() {} 也是检测下线程是否被中断,中断标志=false

// 采自 AbstractQueuedSynchronizer.parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() { 
	// 阻塞线程,等待唤醒
	LockSupport.park(this); 
	// 如果线程走下来,要么是 unpark()了,要么是interrupt()了,而这两者 interrupt() 会将中断状态置为true,导致park()失效
	// interrupted()恰好返回中断状态,并置中断状态为false,使得下一次park() 仍然是生效的
	return Thread.interrupted(); 
}

park与interrupt
调用park()的线程可以被interrupt()方法打断,park()进入时首先会判断当前线程是否有"许可",然后判断是否被打断。
如果线程没有被打断则正常判断是否有"许可"(_counter==1),过程与以上park()方法介绍一致;

如果线程中断状态=true被打断则先判断是否有"许可",若有"许可"则消费掉"许可"然后直接跳出阻塞,
若没有许可则直接跳出阻塞且不抛出InterruptedException并且也不会处理中断状态
这一点被用在ReentrantLock.parkAndCheckInterrupt方法

所以如果线程被打断,连续调用park()方法线程也不会被阻塞住(可以理解为park()方法失效)。若想重新生效park()方法需要重置中断状态为false;

JDK锁

CAS

CompareAndSet(origin,now):如果值是origin,那么设置成now,否则CAS失败
底层是lock cmpxhg指令(X86架构),在单核CPU和多核CPU下都能够保证【比较-交换】的原子性
多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把指令执行完毕再开启总线,这个过程不会被线程调度机制打断,保证多个线程对内存操作的准确性,是原子的

AQS(重要,整个加锁解锁流程,以ReentrantLock实现为例)

全称AbstractQueuedSynchronizer,阻塞式和相关的同步器工具框架
特点:
用state属性来表示资源的状态(独占模式和共享模式),子类继承后去定义和维护这个状态,控制如何获得锁和释放锁

  • getState:获取state属性
  • setState:设置state属性
  • compareAndSetState:cas机制设置state状态
  • 独占模式:只有一个线程能够访问资源。共享模式:允许多个线程访问资源
    提供了基于FIFO的等待队列,类似与Monitor的EntryList
    条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet

子类需要实现以下方法(默认抛出UnsupportedOperationException):
tryAcquire:获取锁
tryRelease:释放锁
tryAcquireShared:获取共享锁
tryReleaseShared:释放共享锁
isHeldExclusively:线程是否持有锁

加锁解锁流程

在这里插入图片描述
Thread-1执行过程:

// 采自 AbstractQueuedSynchronizer.acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. CAS尝试将state由0改为1,结果失败
  2. 进入tryAcquire逻辑,这时state已经是1,结果仍然失败
  3. 接下来进入addWaiter逻辑,构造Node队列
    • 途中黄色三角表示Node的waitStatus状态,其中0为默认正常状态
    • Node的创建时懒惰的
    • 其中第一个Node成为Dummy(亚元)或哨兵,用来占位,并不关联线程
    • 添加到队列中时,采用尾插法
      在这里插入图片描述
// 采自 AbstractQueuedSynchronizer.acquireQueued
// 处于队列中时尝试获取锁
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 当前节点的上一个节点是头节点,则去获得锁(队列中第一个有效线程节点优先获取锁)
            // 获得锁成功后,state 会
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 采自 AbstractQueuedSynchronizer.Node.release
// 这是acquire循环中主要的控制信号
// 前一个节点状态是-1时,表示当前线程需要阻塞
// 前一个节点状态时0 时,需要设置前一个节点状态为-1,并且当前线程需要继续尝试获得锁
// 前一个节点状态>0时,前一个节点获取到了锁
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  1. shouldParkAfterFailedAcquire执行完毕,前置节点的waitStatus置为-1,并返回false,回到acquireQueued,再次tryAcquire尝试获取锁,这时state仍为1,失败
  2. 当再次进入shouldParkAfterFailedAcquire时,这时因为其前驱node的waitStatus已经是-1,这次返回true
  3. 进入parkAndCheckInterrupt(),将当前线程使用LockSupport.park阻塞住
    在这里插入图片描述
  4. 多个线程经历加锁失败后
    在这里插入图片描述
  5. Thread-0释放锁
// 采自 AbstractQueuedSynchronizer.release
// 释放锁,唤醒等待队列中离头节点最近的那一个节点中的线程,让其参与tryAcquire 竞争锁
public final boolean release(int arg) {
	// 释放锁,exclusiveOwnerThread=null,且state 设置为 0 (以ReentrantLock为例)
 	if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// 采自 AbstractQueuedSynchronizer.Node.unparkSuccessor
// unpark 唤醒离头节点最近节点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
    	// 设置头节点状态为0
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 为什么这边要从后往前找?
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 唤醒头节点的下一个节点
    if (s != null)
        LockSupport.unpark(s.thread);
}

在这里插入图片描述

  1. 当队列部位null,并且head的waitStatus=-1时,进入unparkSuccessor流程,找到队列中离head最近的Node,unpark恢恢复其运行,本例中为Thread-1,如果加锁成功(没有竞争),会设置
    • exclusiveOwnerThread为Thread-1,state=1
    • head指向刚刚Thread-1所在的Node,该Node清空Thread
    • 原本的head从链表断开,没有指针指向它,因而可以被垃圾回收
      在这里插入图片描述
为什么唤醒节点要从后往前找?

节点插入到链表时,node.prev指向tail,tail指向node,node.prev.next指向node
如果从前往后找,最后一个节点的next还是null,还没来得及指向插入的节点
1)而此时又开始唤醒节点了,如果从前往后找,那么节点会丢失
2)

// AbstractQueuedSynchronizer.addWaiter 向等待队列中添加节点
private Node addWaiter(Node mode) {
 Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
公平与非公平

默认的ReentrantLock是非公平的实现方式,从加锁的实现方式可以看出tryAcquire(arg)

// 采自 ReentrantLock.NonfairSync.tryAcquire
// NonfairSync 继承 Sync
protected final boolean tryAcquire(int acquires) {
   return nonfairTryAcquire(acquires);
}
// 采自 ReentrantLock.Sync.nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// 首先就是加锁(不公平,线程获取锁首先就是加锁,无需进入等待队列)
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}


// 采自 ReentrantLock.FairSync.tryAcquire 方法,也继承Sync,但覆写了Sync的tryAcquire方法,使其公平
protected final boolean tryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// 公平,先判断队列是否有等待线程,如果有等待线程则加锁失败,会进入等待队列末尾去等待锁
        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;
}
可重入性

回到ReentrantLock.Sync.nonfairTryAcquire中,我们可以发现,current == getExclusiveOwnerThread() 时,会对state进行+acquires操作,而释放锁时,会对state进行-releases操作,只有当 减过后的 state == 0 的时候,free才会为true,也就是说state减到0了,才会彻底释放锁,去unparkSuccessor唤醒下一个线程

// 采自 ReentrantLock.tryRelease
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
可以打断的加锁

我们知道LockSupport.park()可以被unpark()和interrupt() 两个方法打断,如果锁被interrupt()打断,中断标志会置为true,而parkAndCheckInterrupt 被打断后会返回中断标识位=true,接着会抛出InterruptedException异常,不可打断的加锁实现:acquireQueued

// 采自 AbstractQueuedSynchronizer.doAcquireInterruptibly
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 可打断与不可打断的实现区别,这边打断后会直接抛出异常
                throw new InterruptedException();
                // 而不可打断的实现如下,只是改变了一个变量,并且会继续回到acquireQueued的循环中去
                // interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

JAVA线程创建方式

继承Thread类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。 启动线程的唯一方法就是通过 Thread 类的 start()实例方法。 start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法

public class MyThread extends Thread {
	public void run() {
		System.out.println("MyThread.run()");
	}
}
MyThread myThread1 = new MyThread();
myThread1.start();

实现Runnable方法

public class MyThread extends OtherClass implements Runnable {
	public void run() {
		System.out.println("MyThread.run()");
	}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();

实现Callable方法

//执行Callable方式,需要FutureTask实现类的支持,用于接受运算结果
FutureTask<Integer> run=new FutureTask<>(
        new Callable<Integer>() {
            /**
             * 相较于Runable接口,方法有返回值,并且可以抛出异常
             * @return
             * @throws Exception
             */
            @Override
            public Integer call() throws Exception {
                int sum=0;
                for (int i = 0; i < 100; i++) {
                    sum+=100;
                }
                return sum;
            }
        }
);

new Thread(run).start();
run.get();

线程池

构造方法

在这里插入图片描述

线程池运行过程
1)线程池刚开始没有线程,当一个任务提交给线程池后,会创建一个新线程来执行任务
2)当线程数达到corePoolSize,且线程没有空闲时,再加入任务会加入到workQueue阻塞队列,直到有空余线程
3)如果选择有界队列,且超过了任务队列的大小时,会创建maximumPoolSize-corePoolSize数目的线程来救急
4)如果线程达到maximumPoolSize仍有新任务时会执行拒绝策略,JDK有四种实现
- AbortPolicy:调用者抛出RejectedExecutionException异常
- CallerRunsPolicy:调用者直接运行任务
- DiscardPolicy:放弃本次任务
- DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之
- Dubbo的实现,抛出RejectedExecutionException异常之前会记录日志,并dump线程堆栈信息,方便定位问题
5)当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,会释放掉线程资源,这个时间由keepAliveTime来控制

为何用一个Int类型变量即表示线程池状态又表示线程数量(方便CAS)

在这里插入图片描述

submit和execute区别

execute和submit的区别
execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务
submit既能提交Runnable类型任务也能提交Callable类型任务。
异常:
execute会直接抛出任务执行时的异常,可以用try、catch来捕获,和普通线程的处理方式完全一致
submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
返回值:
execute()没有返回值
submit有返回值,所以需要返回值的时候必须使用submit

内存可见性

导致内存可见性的原因

JIT编译器对于频繁访问的主存变量,会缓存到线程的工作高速缓存中
使用volatile修饰变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

volatile原理

给volatile变量赋值后一行指令会加入写屏障,确保写屏障之前的共享变量直接赋值到主存,并且写屏障之前的指令不会重排序到写屏障后
读volatile变量的值前一行指令会加入读屏障,确保读屏障之后的共享变量直接从主存中读值,并且读屏障之后的指令不会重排序到读屏障前

单例模式最正确的实现

除了加锁前后变量的双重判断外,还需要给单例变量加上volatile!细品
在这里插入图片描述

  • 31
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值