14.JUC面试题

JUC面试题

  • 多线程基础
    • 1.进程和线程的区别。启动线程的方式有几种?如何保证线程安全的?
    • 2.线程生命周期
    • 3.yield(),join(),sleep()方法各自有什么作用
    • 4.notify()和notifyAll()有什么区别?
    • 5.并行、并发、串行有什么区别?
    • 6.说说自己是怎么使用 synchronized 关键字,在项目中用到了吗?synchronized关键字最主要的使用方式
    • 7.synchronized和Lock的区别?使用Lock有什么好处
    • 8.为什么有了Runnable接口还要出现Callable接口?
    • 9.手写生产者和消费者
  • Volatile
    • 1.谈谈你对JMM(java内存模型java Memory Model)的理解
    • 2.谈谈你对Volatile的理解。它跟Synchronized有什么区别?
    • 3.DCL单例为什么要加Volatile
    • 4.写一个volatile保证可见性的例子
    • 5.验证volatile不保证原子性的例子。如何解决?
  • CAS
    • 1.CAS是什么?
    • 2.CAS的底层原理知道吗?如果知道,谈谈你对UnSafe的理解
    • 3.AtomicInteger保证原子性的底层原理?
    • 4.CAS的优缺点
  • 集合不安全
    • 1.集合不安全举例
    • 2.CopyOnWriteArrayList容器的原理。
  • 各种锁
    • 1.什么是乐观锁?什么是悲观锁?
    • 2.公平锁与非公平锁
    • 3.共享锁和独占锁
    • 4.ReadWriteLock读写锁
    • 5.重量级锁(Mutex Lock),轻量级锁,偏向锁
    • 6.可重入锁(递归锁)
    • 7.自旋锁
    • 8.CountDownLatch、CycliBarrier、Semaphore使用过吗?
    • 9.写一个死锁的例子,如何定位死锁?
    • 10.写一个ForkJoin分支合并框架的例子
  • 阻塞队列
    • 1.阻塞队列是什么?为什么需要阻塞队列?
    • 2.阻塞队列的核心方法
    • 3.阻塞队列的架构和具体的实现类有哪些?
    • 4.阻塞队列用在哪里?
  • 线程池
    • 1.创建线程池有哪些参数?
    • 2.线程池底层工作原理
    • 3.常用的四种线程池有哪些?工作中用哪个?
    • 4.工作中如何使用自定义的线程池?如何合理配置线程池?
    • 5.为什么要使用线程池?使用线程池的好处有哪些?
  • ThreadLocal
    • 1.JAVA中的引用类型有哪几种?每种引用类型的特点是什么?每种引用类型的应用场景是什么?
    • 2.ThreadLocal的作用和应用场景
    • 3.ThreadLocal原理
    • 4.ThreadLocal的内存泄漏你了解吗?
  • LockSupport
    • 谈谈你堆LockSupport的了解
    • 为什么可以先唤醒线程后阻塞线程?
    • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
  • AQS
    • 说下你对AQS框架的理解
    • 大致说下AQS的流程
    • 我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?
    • 如果AB两个线程进来了以后,请问这个总共有多少个Node节点?

多线程基础

1.进程和线程的区别。启动线程的方式有几种?如何保证线程安全的?

线程和进程的区别:进程是操作系统进行资源分配的最小单元,线程是操作系统进行任务分配的最小单元,线程隶属于进程。
开启线程的方式:
1.继承Thread类,重写run方法
2.创建Runnable接口实现类,实现run方法
3.创建Callable接口实现类,实现call方法,通过FutureTask创建一个线程,可以获取到线程执行后的返回值。
14.JUC面试题_第1张图片
4.通过线程池来开启线程
14.JUC面试题_第2张图片
保证线程安全需要加锁,1.JVM提供的锁,也是就synchronized关键字 2.JDK提供的各种Lock锁

2.线程生命周期

线程创建并启动以后,不会直接进入执行状态,也不会一直处于执行状态。它要经过新建,就绪,执行,阻塞,死亡 五种状态。由于一个线程不会一直霸占着CPU,CPU需要在多条线程之间切换,于是线程状态也会在多次在运行,阻塞状态之间切换。
1.新建状态(new)
当程序new关键字创建线程后,线程就处于新建状态。jvm就会给这个线程对象分配内存,初始化其成员变量的值。
2.就绪状态(RUNNABLE)
当线程对象调用了start()方法之后就处于就绪状态。等待CPU调度执行。
3.运行状态(RUNNING)
处于就绪状态的线程获得了CPU,开始执行run()方法中的逻辑代码,此时线程就处于执行状态。
4.阻塞状态(BLOCKED)
阻塞状态是指该线程由于某种原因放弃了CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会再次被CPU调度执行进入运行状态。
阻塞情况分三种:
等待阻塞(wait->等待队列):运行的线程执行wait()方法,JVM会把该进程进入等待队列。
同步阻塞(lock->锁池): 运行的线程在获取对象的同步锁时,如果被其它线程占用,JVM会把该线程放入锁池中
其它阻塞(sleep/join):运行的线程执行join()方法或者Thread.sleep()方法,或者发出IO请求时,JVM会把线程置为阻塞状态。
5.死亡状态(DEAD)
线程会以下面是那种方式结束。
1.run()或者call()方法执行完成,线程正常结束。
2.执行过程中出现了异常
3.直接调用线程的stop()方法来结束线程,这个方法不安全不建议使用。
不安全原因:
因为stop()方法会释放该线程所持有的所有锁,而且即可停止run()剩余未处理的工作,导致数据没有得到同步处理,被保护的数据可能会出现数据不一致的状态。别的线程在使用这些破坏的数据就可能会导致各种程序错误,因为不推荐使用stop()方法

3.yield(),join(),sleep()方法各自有什么作用

sleep: 导致当前线程休眠,与wait不同的是不会释放锁。

yield:会使当前线程让出CPU的执行时间片,与其它线程一起重新竞争CPU时间片。一般情况下优先级高的线程更容易成功竞争到,但也不是绝对的,有些操作系统对优先级不敏感。

join: join()方法只会将调用线程挂起,直到被调用的对象完成它的执行。
例如:t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。

4.notify()和notifyAll()有什么区别?

notifyAll()可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify()只能唤醒一个
notify()可能会导致死锁,notifyAll()不会
原因是notify()只能唤醒1个线程,假设它是消费者,消费后缓冲区为0,唤醒了消费者2,消费者2发现缓冲区为0则进入等待状态。此时生产者因为没被唤醒也在等待,就死锁了。

5.并行、并发、串行有什么区别?

串行在时间上不可能发生重叠,一个任务没有执行完,下一个任务就只能等着。
并行在时间上是重叠的,两个任务同时执行,互不干扰。
并发跟串行有点相似,同一时刻也是只有一个任务执行,但是可以交替执行。

6.说说自己是怎么使用 synchronized 关键字,在项目中用到了吗?synchronized关键字最主要的使用方式

1.修饰实例方法:给当前对象实例加锁,进入同步方法要获得当前对象的锁。
2.修饰静态方法:给当前类加锁,作用于该类的所有对象实例,因为静态成员不属于任何一个实例,是类成员。
3.修饰代码块:给指定的对象加锁,要想进入同步代码块必须先获得指定对象的锁。
因访问静态的同步方法占用的是当前类的锁,访问实例的同步方法给当前对象加锁。所以线程A调用静态同步方法时,线程B也可以调用非静态同步方法,两者不互斥。

7.synchronized和Lock的区别?使用Lock有什么好处

(1)原始构成:synchronized是关键字属于JVM层面,Lock是具体的类是api层面的锁
(2)使用方法:synchronized不需要用户手动释放锁,当synchronized代码执行完成后系统会自动让线程释放锁的占用
ReentrantLock则需要用户去手动释放锁,若没有主动释放锁就有可能导致出现死锁现象
(3)等待是否可中断
synchronized不能中断,除非是抛出了异常或者是正常执行完成
ReentrantLock可中断,1.设置超时方法 tryLock(long timeout,TimeUnit unit) 2.lockInterruptibly()放代码块中,调用interrupt()方法可中断

@Test
public void testInterrupt(){
    ReentrantLock lock = new ReentrantLock();
    new Thread(()->{
        lock.lock();
        try {
            System.out.println("A获取到锁");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }).start();

    Thread t = new Thread(() -> {
        lock.lock();
        try {
            System.out.println("B获取到锁");
        } finally {
            lock.unlock();
        }
    });
    t.interrupt();

    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

控制台没有打印B获取到锁,因为已经被中断了

A获取到锁

(4)synchronized是非公平锁 ReentrantLock默认是非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
(5)synchronized不支持精确唤醒,只能随机唤醒或者是唤醒全部线程;Lock支持精确唤醒

8.为什么有了Runnable接口还要出现Callable接口?

因为Callable接口中的call方法有返回值,还可以抛出异常。Runable接口的run方法没有返回值而且不能抛出异常。
如果多个任务执行当中,其中一个任务完成耗时时间较长。可以将这个任务异步执行。等待这个耗时比较长任务结束后获取它的返回值一起进行总的计算。

public class MyThread {
    public static int result1(){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 10;
    }

    public static int result2(){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 100;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        long startTime=System.currentTimeMillis();

        //让任务1异步执行
        FutureTask<Integer> futureTask=new FutureTask<>(()->{
            System.out.println("任务1异步执行中");
            return result1();
        });
        new Thread(futureTask).start();
        //new Thread(futureTask).start();如果执行两遍futureTask,第二次会直接返回结果不会再执行方法中的业务逻辑
        //主线程执行任务2
        int result2 = result2();

        //这是个阻塞式等待,会等到futureTask执行完成获取返回结果
        Integer result1 = futureTask.get();


        long endTime=System.currentTimeMillis();
        //任务1让主线程执行一共花费4001毫秒,让任务1异步执行一共花费2040毫秒
        System.out.println("最后结果result1+result2="+(result1+result2)+"\t一共花费"+(endTime-startTime)+"毫秒");

    }
}

9.手写生产者和消费者

synchronized版:

public class ShareData01 {
    private int number=0;
    public synchronized void increment() {
        //为什么判断信号灯的时候不用if要用while,防止虚假唤醒,因为一旦把阻塞状态的线程唤醒,
        //如果是if条件就不会再次判断等待状态,直接执行后面的流程。
        while(number!=0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"\t number="+number);
        notifyAll();
    }
    public synchronized void decrement(){
        while(number==0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"\t number="+number);
        notifyAll();
    }

    public static void main(String[] args) {
        ShareData01 shareData = new ShareData01();
    }
}

ReentrantLock版:

public class ShareData02 {
    private int number=0;
    private Lock lock=new ReentrantLock();
    private Condition condition=lock.newCondition();
    public void increment() {
        lock.lock();
        try {
            while(number!=0){
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"\t number="+number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void decrement() {
        lock.lock();
        try {
            while(number==0){
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"\t number="+number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ShareData02 shareData = new ShareData02();
        for (int i = 1; i <=5 ; i++) {
            new Thread(()->{
                shareData.increment();
            },"AA").start();
        }

        for (int i = 1; i <=5 ; i++) {
            new Thread(()->{
                shareData.decrement();
            },"BB").start();
        }
    }
}

ReentrantLock精准唤醒版

public class ShareData03 {
    private int number=0;
    private Lock lock=new ReentrantLock();
    Condition conditionA=lock.newCondition();
    Condition conditionB=lock.newCondition();
    Condition conditionC=lock.newCondition();
    Condition conditionD=lock.newCondition();
    public void increment() {
        lock.lock();
        try {
            while(number!=0){
                conditionA.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"\t number="+number);
            conditionB.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void decrement() {
        lock.lock();
        try {
            while(number==0){
                conditionB.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"\t number="+number);
            conditionC.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void increment2() {
        lock.lock();
        try {
            while(number!=0){
                conditionC.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"\t number="+number);
            conditionD.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement2() {
        lock.lock();
        try {
            while(number==0){
                conditionD.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"\t number="+number);
            conditionA.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ShareData03 shareData = new ShareData03();
        new Thread(()->{
            for (int i = 1; i <=5 ; i++) {
                shareData.increment();
            }
        },"AA").start();
        new Thread(()->{
            for (int i = 1; i <=5 ; i++) {
                shareData.decrement();
            }
        },"BB").start();
        //这里卡了1小时,不是开启5个线程,是创建一个线程生产5次
        new Thread(()->{
            for (int i = 1; i <=5 ; i++) {
                shareData.increment2();
            }
        },"CC").start();
		//还要保证第二个消费者"DD"在BB第一个消费者后面。因为如果A唤醒B的时候因为等待队列中还没有B
		//所以如果此时D先消费了,那最后就会导致死锁
        new Thread(()->{
            for (int i = 1; i <=5 ; i++) {
                shareData.decrement2();
            }
        },"DD").start();
    }
}

这是把D线程放到B线程前面,每个线程各执行6次任务的业务逻辑。

判断A是否需要进入等待队列->A没有进入等待队列->AA         number=1->AA线程唤醒BB线程
(此时等待队列中还没有BB线程,所以就会随机让一个消费者消费,如果DD消费者在前面先消费就导致死锁)
判断A是否需要进入等待队列
A进入了等待队列
判断D是否需要进入等待队列(虽然唤醒B,但是等待队列还没B,假如把DD消费者线程开启在B前面,就极大概率D先消费到)

D没有进入等待队列
DD         number=0
DD线程唤醒AA线程
判断D是否需要进入等待队列
D进入了等待队列
判断B是否需要进入等待队列
B进入了等待队列

A没有进入等待队列
AA         number=1
AA线程唤醒BB线程
判断A是否需要进入等待队列
A进入了等待队列
判断C是否需要进入等待队列
C进入了等待队列

B没有进入等待队列
BB         number=0
BB线程唤醒CC线程
判断B是否需要进入等待队列
B进入了等待队列

C没有进入等待队列
CC         number=1
CC线程唤醒DD线程
判断C是否需要进入等待队列
C进入了等待队列

D没有进入等待队列
DD         number=0
DD线程唤醒AA线程
判断D是否需要进入等待队列
D进入了等待队列

A没有进入等待队列
AA         number=1
AA线程唤醒BB线程
判断A是否需要进入等待队列
A进入了等待队列

B没有进入等待队列
BB         number=0
BB线程唤醒CC线程
判断B是否需要进入等待队列
B进入了等待队列

C没有进入等待队列
CC         number=1
CC线程唤醒DD线程
判断C是否需要进入等待队列
C进入了等待队列

D没有进入等待队列
DD         number=0
DD线程唤醒AA线程
判断D是否需要进入等待队列
D进入了等待队列

A没有进入等待队列
AA         number=1
AA线程唤醒BB线程
判断A是否需要进入等待队列
A进入了等待队列

B没有进入等待队列
BB         number=0
BB线程唤醒CC线程
判断B是否需要进入等待队列
B进入了等待队列

C没有进入等待队列
CC         number=1
CC线程唤醒DD线程
判断C是否需要进入等待队列
C进入了等待队列

D没有进入等待队列
DD         number=0
DD线程唤醒AA线程
判断D是否需要进入等待队列
D进入了等待队列

A没有进入等待队列
AA         number=1
AA线程唤醒BB线程
判断A是否需要进入等待队列
A进入了等待队列

B没有进入等待队列
BB         number=0
BB线程唤醒CC线程
判断B是否需要进入等待队列
B进入了等待队列

C没有进入等待队列
CC         number=1
CC线程唤醒DD线程
判断C是否需要进入等待队列
C进入了等待队列

D没有进入等待队列
DD         number=0
DD线程唤醒AA线程
判断D是否需要进入等待队列
D进入了等待队列

A没有进入等待队列
AA         number=1
AA线程唤醒BB线程

B没有进入等待队列
BB         number=0
BB线程唤醒CC线程
判断B是否需要进入等待队列
B进入了等待队列(最后一次B任务进入了等待队列)

C没有进入等待队列
CC         number=1
CC线程唤醒DD线程
判断C是否需要进入等待队列
C进入了等待队列(最后一次C任务进入等待队列)

D没有进入等待队列
DD         number=0
DD线程唤醒AA线程
最后的业务逻辑是AD ABCD ABCD ABCD ABCD ABCD ABCD 少了一个BC
(原因是因为A线程已经任务执行完了,已经没有AA线程可唤醒了,随机让一个生产者生产,但是最后一次CC任务已经进入等待队列了,就必须等着唤醒才能执行任务,但是没有人可以唤醒了就发生死锁了。)

阻塞队列版

public class MyResource
{
    private volatile boolean FLAG = true; //默认开启,进行生产+消费
    private AtomicInteger atomicInteger = new AtomicInteger();

    BlockingQueue<String> blockingQueue = null;
	
	//构造注入的思想。阻塞队列有7个实现类,具体想用哪个创建的时候传入对应的实现类即可
    public MyResource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }

    //生产者
    public void MyProd() throws Exception{
        String data = null;
        boolean retValue ; //默认是false

        while (FLAG)
        {
            //往阻塞队列填充数据
            data = atomicInteger.incrementAndGet()+"";//等于++i的意思
            retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
            if (retValue){ //如果是true,那么代表当前这个线程插入数据成功
                System.out.println(Thread.currentThread().getName()+"\t插入队列"+data+"成功");
            }else {  //那么就是插入失败
                System.out.println(Thread.currentThread().getName()+"\t插入队列"+data+"失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        //如果FLAG是false了,马上打印
        System.out.println(Thread.currentThread().getName()+"\t大老板叫停了,表示FLAG=false,生产结束");
    }

    //消费者
    public void MyConsumer() throws Exception
    {
        String result = null;
        while (FLAG) { //开始消费
            //两秒钟等不到生产者生产出来的数据就不取了
            result = blockingQueue.poll(2L,TimeUnit.SECONDS);
            if (null == result || result.equalsIgnoreCase("")){ //如果取不到数据了
                FLAG = false;
                System.out.println(Thread.currentThread().getName()+"\t 超过两秒钟没有取到数据,消费退出");
                System.out.println();
                System.out.println();
                return;//退出
            }
            System.out.println(Thread.currentThread().getName()+"\t消费队列数据"+result+"成功");
        }
    }

    //叫停方法
    public void stop() throws Exception{
        this.FLAG = false;
    }

}

class ProdConsumer_BlockQueueDemo {
    public static void main(String[] args)  throws Exception{
        MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t 生产线程启动");
            try {
                myResource.MyProd();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Prod").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t 消费线程启动");
            System.out.println();
            System.out.println();
            try {
                myResource.MyConsumer();
                System.out.println();
                System.out.println();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Consumer").start();

        try { TimeUnit.SECONDS.sleep(5); }catch (Exception e) {e.printStackTrace();}
        System.out.println();
        System.out.println();
        System.out.println();
        System.out.println("5秒钟时间到,大bossMain主线程叫停,活动结束");
        myResource.stop();
    }
}

Volatile

1.谈谈你对JMM(java内存模型java Memory Model)的理解

JMM内存模型规定所有变量都存储在主内存,但是线程不能直接操作主内存的变量,需要将变量从主内存拷贝到自己的工作内存,在工作内存中对变量操作完成后再写回主内存,线程之间的通讯必须通过主内存来完成。

如果有一个线程修改了某个变量的值,并将修改好的值写回主内存,但是其他线程并不知道这个变量的值已经修改了。所以此时就必须有一种机制,只要一个线程修改完自己工作内存的值,并写回给主内存以后要及时通知其他线程,这种即时通知的这种情况就是JMM内存模型里面一个重要特性:可见性
14.JUC面试题_第3张图片

2.谈谈你对Volatile的理解。它跟Synchronized有什么区别?

volatile是java虚拟机提供的轻量级的同步机制,是基本上遵守了JMM的规范,主要是保证可见性和禁止指令重排,但是它并不保证原子性
(根据JMM,内存模型规定,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作完工作内存中的变量并写回主内存后,如果这个变量用volatile修饰,就会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。)
总线嗅探机制

  • 对于工作内存和主内存同步延迟现象导致的可见性问题,可以使用synchronized和volatile关键字来解决,它们都可以保证变量对所有线程的可见性
  • 对于指令重排导致的可见性问题和有序性问题,可以使用volatile关键字解决,因为volatile可以禁止指令重排
  • synchronized可以保证线程安全,volatile并不能保证原子性所以不能保证线程安全

3.DCL单例为什么要加Volatile

DCL机制不一定线程安全,原因是因为创建对象的时候 第一步是先给对象分配内存空间 第二步初始化对象 第三步将对象引用指向刚分配的内存地址,此时对象就不为null了。由于第二步和第三步不存在数据依赖关系。所以如果先指向分配的内存地址 再初始化对象这种重排优化是允许的。所以当别的线程判断instance!=null获取到instance可能没有完成初始化,这也就造成了线程安全问题。
加上volatile就可以禁止指令重排,防止指令重排造成的线程安全问题。

//懒汉式双重检查
public class Singleton06 {
    private Singleton06(){
        System.out.println("调用了构造方法");
    }
    private static volatile Singleton06 instance;
    public static Singleton06 getInstance(){
        if (instance == null) {
            synchronized (Singleton06.class){
                if (instance == null) {
                    instance=new Singleton06();
                }
            }
        }
        return instance;
    }
}

加了volatile修饰的共享变量,是通过内存屏障解决了多线程的有序性问题。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。
    14.JUC面试题_第4张图片
    14.JUC面试题_第5张图片

4.写一个volatile保证可见性的例子

public class VolatileDemo {
    private volatile int num=0;

    public int getNum() {
        return num;
    }

    public void addTo(){
        num=60;
    }
}
class Test{
    public static void main(String[] args) {
        VolatileDemo data = new VolatileDemo();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\tcome in 修改num的值");
            //睡眠1毫秒是为了这个线程修改num之前,main线程已经把这个变量拷贝到自己的工作内存了。
            //如果不睡眠可能main线程拷贝的是修改num之后的值
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.addTo();
            System.out.println(Thread.currentThread().getName()+"\t updated number value:"+data.getNum());
        }).start();

        while(data.getNum()==0){

        }
        System.out.println("感知到别的线程修改了num的值");
    }
}

14.JUC面试题_第6张图片

5.验证volatile不保证原子性的例子。如何解决?

造成原因?
因为++操作会分成多条指令执行,会先获取getField,然后add,然后putField。如果putField的时候线程挂起了就会造成线程安全问题
如何解决?
使用synchronized关键字修饰这个方法,这样虽然解决了问题但是并发性不高
我们可以使用java.util.concurrent.atomic下的AtomicInteger来解决不保证原子性问题。
发现的问题
我们发现AtomicInteger也没添加volatile,虽然getAndIncrement()方法是个原子性操作,但是如何保证修改完之后保证这个变量在所有线程的可见性的?
因为AtomicInteger内部的value属性已经使用了volatile修饰了。

public class VolatileDemo02 {
    private volatile int number=0;

    public int getNum() {
        return number;
    }

    public void addPlus(){
        number++;
    }
}
class Test02{
    public static void main(String[] args) {
        VolatileDemo02 demo = new VolatileDemo02();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    demo.addPlus();
                }
            },String.valueOf(i)).start();
        }

        //只有当剩下main线程和gc线程才跳出此循环,否则就让出main线程CPU时间片不停自旋
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally number value: "+demo.getNum());
    }
}
main	 finally number value: 18650

理论上num最后返回应该为20000,因为num++在多线程下是非线程安全的。
因为有很多值在putfield这步写回去的时候可能线程的调度被挂起了,刚好也没有收到最新值的通知,有这么一个纳秒级别的时间差,一写就出现了写覆盖,就把人家的值给覆盖掉了。
14.JUC面试题_第7张图片
解决方案:
1.可以加synchronized来解决不保证原子性问题,但是不推荐使用,并发性得不到保证。
2.可以使用java.util.concurrent.atomic包下的AtomicInteger(带原子包装的整型类)来解决不保证原子性问题

    AtomicInteger atomicInteger=new AtomicInteger();
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
}
class Test02{
    public static void main(String[] args) {
        VolatileDemo02 demo = new VolatileDemo02();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    demo.addAtomic();
                }
            },String.valueOf(i)).start();
        }

        //只有当剩下main线程和gc线程才跳出此循环,否则就让出main线程CPU时间片不停自旋
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally number value: "+demo.atomicInteger);
    }
}
main	 finally number value: 20000

这里就会引发一个问题了。AtomicInteger虽然操作是原子性的,但是我们没用volatile修饰,你修改完之后写回主内存后,别的线程可能工作内存中还是旧数据。
原因是因为AtomicInteger底层已经加了volatile修饰了。
14.JUC面试题_第8张图片

CAS

1.CAS是什么?

CAS就是比较并交换,是一条原子指令。它会判断内存某个位置的值跟预期值是否一致,如果是就更新为新的值。

2.CAS的底层原理知道吗?如果知道,谈谈你对UnSafe的理解

CAS是一条系统原语,原语属于操作系统范畴,由若干条指令组成,而且原语执行必须是连续的不能中断,所以CAS是一条原子指令。
UnSafe是CAS的核心类,他让Java拥有了直接操作内存空间的能力。CAS并发原语就体现在unsafe类的各个本地方法中。

(1)UnSafe是CAS的核心类,由于java方法没有办法直接访问底层,Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力
(2)CAS是一种系统原语,原语属于操作系统范畴,由若干条指令组成用于完成某个功能的一个过程,并且原语的执行必须是连续的不能中断,所以说CAS是一条原子指令,不会造成数据不一致问题。
(3)CAS并发原语体现在UnSafe类中的各个本地方法,当我们调用这些方法JVM会帮我们实现CAS汇编指令,这是一种完全依赖于硬件功能实现的原子性操作。

public class CASDemo01 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger=new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2021)); //true
        System.out.println(atomicInteger.compareAndSet(5, 2020)); //false
    }
}

在这里插入图片描述
14.JUC面试题_第9张图片

3.AtomicInteger保证原子性的底层原理?

AtomicInteger的getAndIncrement方法实际调用的是Unsafe类中的GetAndAddInt方法,里面有个while循环调用compareAndSwapInt方法不断比较内存的值跟我们的预期值是否一致,如果不一致继续获取比较,直到一致。

在多线程下++操作存在着线程安全问题,我们使用AtomicInteger中的getAndIncrement方法解决了原子性问题,那么它保证原子性的底层原理是什么?
AtomicInteger的getAndIncrement方法会调用UnSafe类中的getAndAddInt方法。
14.JUC面试题_第10张图片

(1)假设线程A和线程B同时执行getAndAddInt操作,如果AtomicInteger里面的value原始值为3,根据JMM模型,线程A和线程B的工作内存中各持有一份值为3的value的副本
(2)线程A执行getIntVolatile()方法拿到内存中value值为3,如果此时线程A被挂起
(3)线程B也拿到内存中value值为3,继续执行compareAndSwapInt方法比较此时内存中的值确实为3,就修改工作内存的值为4并写回主内存。因为value是volatile修饰的,写回主内存后就会通知其它线程该变量副本已经失效,需要重新从主内存中获取。
(4)当线程A恢复继续执行compareAndSwapInt方法比较时,因为此时已经从主内存中拷贝最新的值到自己的工作内存了,此时工作内存中value值为4,跟之前获得的值不一致,说明该值被其它线程抢先一步修改了,那么A线程修改失败只能重新来一遍
(5)线程A重新获取工作内存中value值为4,再次执行compareAndSwapInt方法比较,如果一致替换成功。否则继续重新比较,直到成功。

4.CAS的优缺点

优点:synchronized需要加锁保证一致性,并发性能下降。CAS没有加锁,反复的通过CAS进行比较,直到比较成功为止。既保证一致性又提高了并发性
缺点:
(1)如果CAS长时间一直不成功,就会一直尝试可能会给CPU带来很大的开销
(2)只能保证一个共享变量的原子性
14.JUC面试题_第11张图片
(3)会引发ABA问题
ABA问题的产生。如何解决ABA问题?
比如两个线程都从内存中某个位置获取到A,如果其中一个线程进行某些操作将值变为B,然后又将这个数据变成了A,这时候另一个线程进行CAS操作发现内存中仍然是A,CAS操作就会成功但是不代表这个过程就是没有问题的。
解决方案:使用AtomicStampReference
新增一种机制,那就是增加版本号,当版本号跟要对比的版本号不一致的话,就说明这个数据中间被修改过

@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
    private Integer id;
    private String name;

}
public class ABADemo {
    private static AtomicReference<User> atomicReference=new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User(100,"zccheng");
        User user2 = new User(200,"zccheng");

        atomicReference.set(user1);

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+":"+atomicReference.compareAndSet(user1, user2)
                    +"\t"+atomicReference.get());
            System.out.println(Thread.currentThread().getName()+":"+atomicReference.compareAndSet(user2, user1)
                    +"\t"+atomicReference.get());
        },"AA").start();

        new Thread(()->{
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+atomicReference.compareAndSet(user1, user2)
                    +"\t"+atomicReference.get());
        },"BB").start();

        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("以下是ABA问题的解决");
        AtomicStampedReference<User> stampedReference=new AtomicStampedReference<>(user1,1);

        new Thread(()->{
            //获取版本号
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
            //暂停1秒钟t3线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            stampedReference.compareAndSet(user1,user2,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第2次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
            stampedReference.compareAndSet(user2,user1,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第3次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
        },"CC").start();

        new Thread(()->{
            //获取版本号
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
            //保证线程3完成1次ABA
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = stampedReference.compareAndSet(user1, user2, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t 修改成功否"+result+"\t最新版本号"+stampedReference.getStamp());
            System.out.println("最新的值\t"+stampedReference.getReference());
        },"DD").start();
    }
}

集合不安全

1.集合不安全举例

1.多线程下同时add()元素会出现数组下标越界异常。
原因是因为一开始大量线程同时判断不需要扩容,然后都在size++处设置数组元素就会出现,因为elmentData数组没有扩容,所以下标越界

public static void main(String[] args) {
	List<String> list= new ArrayList<>();
	for (int i = 1; i <=100; i++) {
	    new Thread(()->{
	        try {
	            Thread.sleep(1);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        list.add("a");
	    },String.valueOf(i)).start();
	}
}

在这里插入图片描述
14.JUC面试题_第12张图片
14.JUC面试题_第13张图片
2.如果多个线程都在修改元素,当遍历集合的时候会出现ConcurrentModificationException
调用list的toString方法,会首先获取这个集合的迭代器实例。然后使用hasNext()和next()方法遍历这个集合

//ArrayList的父类AbstractCollection的toString()方法源码
public String toString() {
     Iterator<E> it = iterator();
     if (! it.hasNext())
         return "[]";

     StringBuilder sb = new StringBuilder();
     sb.append('[');
     for (;;) {
         E e = it.next(); //出现异常都在这个方法中
         sb.append(e == this ? "(this Collection)" : e);
         if (! it.hasNext())
             return sb.append(']').toString();
         sb.append(',').append(' ');
     }
}

next()方法中我们要确保遍历过程中没有其他线程修改这个遍历的集合。

//ArrayList的内部类Itr是Iterator实现类,里面的next()方法源码
@SuppressWarnings("unchecked")
public E next() {
     checkForComodification(); //这里会检查别的线程有没有修改这个集合
     int i = cursor;
     if (i >= size)
         throw new NoSuchElementException();
     Object[] elementData = ArrayList.this.elementData;
     if (i >= elementData.length)
         throw new ConcurrentModificationException();
     cursor = i + 1;
     return (E) elementData[lastRet = i];
 }

如果集合的修改次数跟我们迭代器的期望的修改次数不一致就会报ConcurrentModificationException

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

而我们迭代器创建时
14.JUC面试题_第14张图片
所以如果我们用迭代器遍历集合的时候别的线程对这个集合元素进行了修改就会因为modCount加了1跟我们期望的expectedModCount不一致,而报异常

3.HashMap的put方法中如果有两个线程他们计算的存储下标一致,而且都判断数组当前位置没有元素,就会造成线程安全问题
在这里插入图片描述

2.CopyOnWriteArrayList容器的原理。

读锁的作用?为什么写时复制的容器就不需要加读锁?

我觉得这个问题不能考虑得过于深入,感觉特别容易陷进去。多线程的运行是复杂的,要考虑到每一种事故也挺困难的。不过好处是遵守一定的规则可以避免一些问题。就说读写锁吧,互斥可以分为读读、读写、写写、写读。而读写锁,也仅仅只是打开了读读,读写可没有打开,在读取数据的时候不能写入,这应该是底层需要遵守的基本规则,否则可能发生数据争用,造成未知情况。写入时复制之所以能放开读写,那是因为它在每一次写入时都会拷贝一份来操作,并不会在原数据上操作,即使同时读写,那也是读一份,写一份,而写完后赋值可以看作是具有原子性的,所以避免的数据争用,但对高频写,这个类就不那么友好。

CopyOnWrite是写时复制的容器。往这个容器中添加元素的时候,使用ReentrantLock保证写操作的原子性。它不会在原来的容器中添加元素,而是会创建一个原来长度+1的新的容器,然后将原来容器中的元素拷贝到新的容器中,新添加的元素放到最后的下标位置。最后在把原容器的引用指向新的容器,这样做的好处是并发读的时候不需要加锁。因为当前容器是不会添加任何元素的。
但是问题来了,我们是如何保证并发读每次读取到的都是最新的容器呢?
因为底层的Objetct数组array属性用了volatile修饰,只要这个数组的引用指向了新的数组就会立刻通知其它线程这个array属性已经失效,需要重新从主内存中获取新的数据。

不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy.复制出一个新的容器Object[] newElements,然后新的容器
Object[] newElements里添加元素,添加完元素之后再将原容器的引用指向新的容器setArray(newElements);这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想。

14.JUC面试题_第15张图片
上面问题来了。我们每次修改实际是将原容器引用指向新容器。那么如何保证并发读每次读到的都是最新容器里的数据呢?因为底层array数组用了volatile修饰,修改时每次将array变量指向新的数组都会立刻通知其它线程旧的array数据已经失效,要从主内存中拷贝最新的值。
14.JUC面试题_第16张图片
CopyOnWrite中的array使用volatile修饰。如果元素发生变化会立刻感知到。如果使用ArrayList就会感知不到一直卡在那里

public static void main(String[] args) {
    ContainerNoSafeDemo2 demo = new ContainerNoSafeDemo2();
    //List list = new ArrayList();
    List list=new CopyOnWriteArrayList();
    new Thread(()->{
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        list.add("a");
    },"A").start();

    while(list.isEmpty()){}
    
    System.out.println("感知到不为空");
}

各种锁

1.什么是乐观锁?什么是悲观锁?

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去读数据的时候都认为别人不会修改,所以不会上锁,但是更新数据的时候会判断一下在此期间别人有没有去更新这个数据,也就是先读出当前版本号,然后加锁比较上一次版本号,如果一样就更新,失败就重复读-比较-写的操作。
JAVA中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新原子操作,比较当前值跟传入值是否一样,一样则更新,否则失效。

悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高,每次拿数据的时候都会认为别人会修改,所以每次读写操作都会上锁,这样别人读写这个数据就会block直到拿到锁,java中悲观锁就是Synchronized; AQS框架中的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转换为悲观锁,如RetreenLock

2.公平锁与非公平锁

公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队的优先获得锁,先来先得
非公平锁(Nonfair)
加锁时不考虑排队问题,直接尝试获取锁,如果尝试失败就再采用类似公平锁的那种方式
1.非公平锁性能比公平锁高5~10倍,因为公平锁需要在多个线程情况下维护一个队列
2.java中的synchronized是非公平锁,ReentrantLock 的lock()方法默认也是非公平锁。如果使用fair=true创建ReentrantLock就是公平锁
在这里插入图片描述

3.共享锁和独占锁

从锁的类别上分,有共享锁和排他锁。
排他锁也叫独占锁,该锁一次只能被一个线程所持有,如果一个线程对数据加上排他锁后,其它线程便不能给这个数据加任何锁(排他锁跟其它排他锁和共享锁互斥),
获得排他锁的线程既能读数据又能修改数据。JDK中的Synchronized和JUC中的Lock实现类就是互斥锁
共享锁指该锁可以被多个线程所持有。如果一个线程对数据加上共享锁后,其它线程只能对该数据加共享锁,不能加排他锁。获得共享锁的线程只能读数据,不能更新数据。

4.ReadWriteLock读写锁

java提供读写锁,在读的地方使用读锁,写的地方使用写锁。
java中读写锁有个具体接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWrite。
读锁的共享锁可保证并发读非常高效,写锁跟其它的读锁,写锁互斥,也能保证数据安全性。所以ReentranReadWrite并发性相比一般的互斥锁有了很大的提升。

public class ReadWriteLockDemo {
    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();

    public void write(){
        Lock wlock = readWriteLock.writeLock();
        wlock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t写数据");
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            wlock.unlock();
        }
    }

    public void read(){
        Lock rlock = readWriteLock.readLock();
        rlock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t读数据");
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            rlock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
        //写数据时,其它读写线程都要等待
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                readWriteLockDemo.write();
            },"write"+i).start();
        }
        //读数据时,其它线程也可以读数据,但是不能写数据
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                readWriteLockDemo.read();
            },"read"+i).start();
        }
    }
}

5.重量级锁(Mutex Lock),轻量级锁,偏向锁

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,但是监视器锁本质是依赖于操作系统的Mutex Lock来实现的。
操作系统实现线程之间切换需要从用户态转换到核心态,成本非常高。状态之间转换需要相对较长时间,这就是为什么Synchronized效率低的原因
因为,这种依赖于操作系统Mutex Lock所实现的锁我们称之为重量级锁,JDK中对synchronized做的种种优化,核心都是减少这种重量锁的使用。

JDK1.6以后,为了减少获得锁和释放锁的性能消耗,提高性能,引入了"轻量级锁"和"偏向锁"
锁的状态总共有四种:无锁状态,偏向锁,轻量级锁,重量级锁
锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁升级是单向的,只能由低到高,不能降级)
"轻量级"是相对于操作系统中的传统互斥锁而言的,但是它并不是为了代替重量级锁,它主要是在没有多线程竞争的前提下,减少传统重量级锁使用产生的性能消耗
轻量级锁适应场景是线程交替执行同步代码块的情况,如果存在同一时间获得同一把锁的情况,就会导致轻量级锁升级成重量级锁。

研究发现大多数情况下锁不仅不存在多线程竞争,而且总是存在同一线程多次获得锁的情况。偏向锁的目的是某个线程获得锁之后,减少这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要轻量锁的执行路径,轻量锁的获取和释放依赖多次CAS原子指令,而偏向锁只需要置换ThreadID的时候依赖一次CAS原子指令。由于一旦出现多线程竞争情况必须撤销偏向锁,所以偏向锁的撤销操作性能消耗必须要小于节省下来CAS原子指令的性能消耗。
上面说过,轻量级锁是为了线程交替执行同步代码块时提高性能,偏向锁只有一个线程执行同步代码块时进一步提高性能。

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

6.可重入锁(递归锁)

可重入锁指的是同一线程外层函数获得锁之后,内层函数仍然可以获取该锁而不会发生死锁。可重入锁加几次锁就要解几次锁。
Synchronized和ReentrantLock都是可重入锁。

public class Phone implements Runnable {
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }

    private void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\tget");
            set();
        } finally {
            lock.unlock();
        }
    }

    private void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\tset");
        } finally {
            lock.unlock();
        }
    }
}

7.自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去不断尝试获取锁,这样做的好处是减少线程上下文切换的消耗,缺点是如果长时间没获取到锁,不停的循环会消耗CPU。
自旋锁适合获取锁的线程持有锁的时间比较短的情况,这种情况下自旋锁的效率要远高于互斥锁。

自旋锁不适合单核单线程的CPU
自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。因此操作系统的实现在很多地方往往用自旋锁。Windows操作系统提供的轻型读写锁(SRW Lock)内部就用了自旋锁。显然,单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行状态,只能等到操作系统分给A的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。(红字部分是我给wiki编辑的词条,单核CPU不适合自旋锁,这个也只是针对单核单线程的情况,现在的技术基本单核都是支持多线程的)

public class SpinLockDemo {
    AtomicReference<Thread> atomicReference=new AtomicReference<>();
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"\t come in");
        while(!atomicReference.compareAndSet(null,thread)){

        }
    }

    public void unlock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"\tunlock success");
        atomicReference.compareAndSet(thread,null);
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(()->{
            try {
                spinLockDemo.myLock();
                TimeUnit.SECONDS.sleep(3);
                spinLockDemo.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AA").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            spinLockDemo.myLock();
            spinLockDemo.unlock();
        },"BB").start();
    }
}

14.JUC面试题_第17张图片

8.CountDownLatch、CycliBarrier、Semaphore使用过吗?

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞.其他线程调用countDown方法计数器减1(调用countDown方法时线程不会阻塞),当计数器的值变为0,因调用await方法被阻塞的线程会被唤醒,继续执行。主要用于让一些线程阻塞直到另外一些完成后才被唤醒

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);

        //秦灭6国实现统一
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"国已被攻略");
                countDownLatch.countDown();
            },CountryEnum.forEach(i).getName()).start();
        }

        countDownLatch.await();
        System.out.println("秦国实现统一");
    }
}

工作中枚举使用就相当于一个数据库,CountryEnum相当于一个表table.里面的ONE,TWO,THREE相当于表中的记录。code,name属性相当于表中的字段。如果哪些数据不想从数据库中返回了可以考虑使用枚举。

public enum  CountryEnum {
    ONE(1,"齐"),
    TWO(2,"楚"),
    THREE(3, "燕"),
    FOUR(4, "赵"),
    FIVE(5, "魏"),
    SIX(6, "韩");


    @Getter
    private Integer code;
    @Getter
    private String name;

    CountryEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    public static CountryEnum forEach(int i){
        CountryEnum[] values = CountryEnum.values();
        for (CountryEnum countryEnum : values) {
            if(i==countryEnum.getCode()){
                return countryEnum;
            }
        }
        return null;
    }

}

CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法.

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("收集到7颗龙珠召唤神龙");
        });
        for (int i = 1; i <=7; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t 收集到第"+ temp +"颗龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

Semaphore信号量的主要用户两个目的,一个是用于多个线程抢用多个共享资源的情况,另一个用于并发资源数的控制.
14.JUC面试题_第18张图片
acquire是阻塞式等待,我们可以使用tryAcquire(long timeout,TimeUnit unit)

public class SemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                try {
                    //会尝试去抢4秒,抢不到就放弃返回false.因为3s释放所以能抢到。如果只尝试2秒就抢不到;
                    //如果不使用带参数的tryAcquire()直接返回true或者false
                    if (semaphore.tryAcquire(4, TimeUnit.SECONDS)) {
                        System.out.println(Thread.currentThread().getName()+"\t获取到停车位");
                        TimeUnit.SECONDS.sleep(3);
                        System.out.println(Thread.currentThread().getName()+"\t3秒后离开停车位");
                        semaphore.release();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

9.写一个死锁的例子,如何定位死锁?

public class DeadLockDemo {
    private String lockA;
    private String lockB;

    public DeadLockDemo(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    public void methodA(){
        synchronized (lockA){
            try {
                TimeUnit.MILLISECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){

            }
        }
    }

    public void methodB(){
        synchronized (lockB){
            synchronized (lockA){

            }
        }
    }

    public static void main(String[] args) {
        DeadLockDemo deadLockDemo = new DeadLockDemo("lockA", "lockB");
        new Thread(()->{
            deadLockDemo.methodA();
        }).start();

        new Thread(()->{
            deadLockDemo.methodB();
        }).start();
    }
}

如何定位死锁
使用jps命令定位进程编号

D:\0000project\00mygulimall\study_setup\testInterview\src\main\java\com\zcc\juc>jps
11648 DeadLockDemo
20080 Jps
20020 RemoteMavenServer36
18908 Launcher
20908

使用jstack找到死锁查看:观察jvm中当前所有线程的运行情况和线程当前状态。

jstack 11648

"Thread-1":
        at com.zcc.juc.DeadLockDemo.methodB(DeadLockDemo.java:31)
        - waiting to lock <0x0000000715e860d8> (a java.lang.String)
        - locked <0x0000000715e86110> (a java.lang.String)
        at com.zcc.juc.DeadLockDemo.lambda$main$1(DeadLockDemo.java:42)
        at com.zcc.juc.DeadLockDemo$$Lambda$2/1595428806.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.zcc.juc.DeadLockDemo.methodA(DeadLockDemo.java:23)
        - waiting to lock <0x0000000715e86110> (a java.lang.String)
        - locked <0x0000000715e860d8> (a java.lang.String)
        at com.zcc.juc.DeadLockDemo.lambda$main$0(DeadLockDemo.java:38)
        at com.zcc.juc.DeadLockDemo$$Lambda$1/2065951873.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

10.写一个ForkJoin分支合并框架的例子

class MyTask extends RecursiveTask<Integer> {
    private int start;
    private int end;
    private final int VALUE=10;
    private int result=0;

    public MyTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if(end-start<= VALUE){
            for (int i = start; i <=end ; i++) {
                result+=i;
            }
            return result;
        }else{
            int middle=start+(end-start)/2;
            MyTask myTask1 = new MyTask(start, middle);
            MyTask myTask2 = new MyTask(middle + 1, end);
            myTask1.fork();
            myTask2.fork();
            return myTask1.join()+myTask2.join();
        }
    }
}
public class ForkJoinDemo{
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> submit = forkJoinPool.submit(new MyTask(1, 100));
        try {
            Integer result = submit.get();
            System.out.println("result = " + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            forkJoinPool.shutdown();
        }
    }
}

阻塞队列

1.阻塞队列是什么?为什么需要阻塞队列?

阻塞队列首先它是一个队列。当阻塞队列是空时,从队列中获取元素的操作就会被阻塞。当阻塞队列满时,往队列中添加元素的操作会被阻塞
14.JUC面试题_第19张图片
在多线程情况下,某些线程在一些情况下会被阻塞,一旦一些条件满足这些阻塞的线程又会被唤醒。
阻塞队列的好处就是我们不需要关心什么时候阻塞线程,什么时候需要唤醒线程,因为BlockingQueue都一手给你包办好了

2.阻塞队列的核心方法

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用

offer(e,time,unit)和poll(time,unit)会阻塞一段时间,超过时间后还没插入或者获取成功就返回false。

3.阻塞队列的架构和具体的实现类有哪些?

14.JUC面试题_第20张图片

4.阻塞队列用在哪里?

生产者消费者模式
线程池
消息中间件

线程池

1.创建线程池有哪些参数?

corePoolSize:核心线程数,线程中一直保持的线程的数量
maximumPoolSize:线程池允许的最大线程数
keepAliveTime:空闲线程存活时间
unit:时间的单位
workQueue:阻塞队列(类似银行中的候客区)
threadFactory:线程工厂

handler:拒绝策略

  • AbortPolicy 丢弃抛出异常
  • DiscardOldestPolicy 丢出队列中最老的任务 新的任务加入队列中 不抛出异常
  • DiscardPolicy 丢弃不抛出异常
  • CallerRunsPolicy 交给上个线程(主线程)处理执行
/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters.
1.池中一直保持的线程的数量,即使线程空闲。除非设置了allowCoreThreadTimeOut
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 
2.池中允许的最大的线程数
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
 
3.当线程数大于核心线程数的时候,线程在最大多长时间没有接到新任务就会终止释放,最终线程池维持在corePoolSize 大小
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 
4.时间的单位
 * @param unit the time unit for the {@code keepAliveTime} argument
 
5.阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了corePoolSize大小,就会放在这里等待空闲线程执行。
 * @param workQueue the queue to use for holding tasks before they are
 *        executed.  This queue will hold only the {@code Runnable}
 *        tasks submitted by the {@code execute} method.
 
6.创建线程的工厂,比如指定线程名等,一般使用默认的线程工厂 Executors.defaultThreadFactory()
 * @param threadFactory the factory to use when the executor
 *        creates a new thread
 
7.拒绝策略,如果线程满了,线程池就会使用拒绝策略。
 * @param handler the handler to use when execution is blocked
 *        because the thread bounds and queue capacities are reached
 * @throws IllegalArgumentException if one of the following holds:<br>
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code threadFactory} or {@code handler} is null
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}

四大拒绝策略:

  • new ThreadPoolExecutor.AbortPolicy() //银行满了(最大线程数5+等待区3=8),再有人进来,不处理这个人的,抛出异常
  • new ThreadPoolExecutor.CallerRunsPolicy() //哪来的回哪处理,由于是main线程来的,返回main线程处理
  • new ThreadPoolExecutor.DiscardPolicy() //银行满了,丢掉任务不吭声,不抛出异常
  • new ThreadPoolExecutor.DiscardOldestPolicy() //丢弃队列最前面的任务,最新任务入列

2.线程池底层工作原理

14.JUC面试题_第21张图片
1.创建线程池后,调用execute()方法添加一个请求任务时,线程池会做如下判断:

  • 如果正在运行的线程数量小于核心线程数,那么马上分配线程执行这个任务;
  • 如果正在运行的线程数量大于或者等于核心线程数,那么就把这个任务放入阻塞队列中;
  • 如果这时候阻塞队列也满了,那就继续判断正在运行的线程数是否小于最大线程数,如果小于那么创建非核心线程立刻运行这个任务
  • 如果已经达到最大线程数,那线程池会启动拒绝策略来执行

2.当一个线程完成任务时,它会从队列中取下一个任务来执行
3.如果一个线程处于空闲状态超过一定的时间(keepAliveTime),且当前运行的线程数大于核心线程数,那么这个线程就会被回收掉

3.常用的四种线程池有哪些?工作中用哪个?

newSingleThreadExecutor();单线程的线程池
newFixedThreadPool();创建固定长度的线程池
newCachedThreadPool() 创建可缓存的线程池
newScheduledThreadPool() 也是固定长度的线程池,但是支持周期性和定时任务的执行
工作中都不用。
因为newSingleThreadExecutor()newFixedThreadPool()的阻塞队列使用的是LinkedBlockingQueue,队列长度是Integer.MAX_VALUE,就会导致堆积大量的请求,导致OOM
newCachedThreadPool()newScheduledThreadPool()最大线程数是Integer.MAX_VALUE,可能会创建大量的线程,导致OOM

newCacheThreadPool
创建一个可缓存的线程池,如果线程池长度超过需要,可灵活回收空闲线程,若无可回收,则新建线程
14.JUC面试题_第22张图片
newFixedThreadPool
创建一个指定长度的线程池,可控制线程最大并发数,超出的线程会再队列中等待
14.JUC面试题_第23张图片
newScheduleThreadPool
创建一个定长线程池,支持定时及周期性任务执行
newSingleThreadExecutor
创建一个单线程化的线程池,她只会用唯一的工作线程来执行任务,保证所有任务按照队列中的顺序挨个执行
14.JUC面试题_第24张图片
工作中我们只用自定义ThreadPoolExecutor的方式创建线程池,不用Executors去创建。原因如下
(1)FixedThreadPoolSingleThreadPool允许的请求队列长度为Integer.MAX_VALUE(LinkedBlockingQueue如果是无参创建就是无界的),可能会堆积大量的请求,从而导致OOM
(2)CachedThreadPoolScheduledThreadPool允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
14.JUC面试题_第25张图片

4.工作中如何使用自定义的线程池?如何合理配置线程池?

public class MyThreadPoolExecutorDemo {
    public static void main(String[] args){
        ExecutorService executor = new ThreadPoolExecutor(2,5,2L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        try {
            for (int i = 1; i <=12; i++) {
                final int temp=i;
                executor.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"执行任务"+temp);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        	//不要忘记关闭线程
            executor.shutdown();
        }
    }
}

看公司业务是CPU密集型还是IO密集型,根据业务的类型决定线程池最佳最大线程数
(1)我们可以调用Runtime.getRuntime().avaliableProcessors()这个方法来查看CPU核数

(2)如果业务是CPU密集型表明该业务没有阻塞,CPU一直全速运行。应尽量配置尽可能少的线程数量。一般公式:(CPU核数+1)个线程数;
在这里插入图片描述
(3)如果业务是IO密集型时表明该业务需要大量的IO,会造成大部分线程被阻塞,所以需要多配置线程数。参考公式:CPU核数*2,
如果IO任务非常多 跟项目经理沟通确认最后最大线程数=CPU核数/(1-阻塞系数) 阻塞系统在0.8~0.9之间。
(要求会背:在单线程上运行IO密集型任务会导致浪费大量的CPU运行能力,都浪费在等待了。所以IO密集型任务使用多线程可以大大加速程序的运行,即使在单核CPU上,这种加速主要是利用了被浪费掉的阻塞时间)
14.JUC面试题_第26张图片

5.为什么要使用线程池?使用线程池的好处有哪些?

如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)合理 利用线程池能够带来三个好处。
第一:降低资源消耗。可以重复利用线程池中创建好的线程,不会因为频繁创建线程和销毁线程带来的性能损耗。
第二:提高响应速度。线程池中创建的线程处理等于分配任务的状态,当任务来时无需创建线程立即执行任务
第三:提高线程的可管理性。线程如果无限制创建,不仅消耗资源还会降低系统稳定性。使用线程池可以对池内的线程进行统一的分配,调优和监控。

ThreadLocal

1.JAVA中的引用类型有哪几种?每种引用类型的特点是什么?每种引用类型的应用场景是什么?

强引用:只要有引用指向就不会被回收,宁肯OOM也不会被回收。
软引用(SoftReference):内存不足时会被回收(缓存)
弱引用(WeakReference):只要GC就会垃圾回收(防止map,threadlocal的内存泄漏)
虚引用: 主要用来跟踪对象被垃圾回收器回收的活动

2.ThreadLocal的作用和应用场景

ThreadLocal的作用是提供线程内的局部变量,在多线程环境下各个线程的ThreadLocal变量各自独立。也就是说每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。
使用场景:
(1)多线程环境下对非线程安全对象的并发访问,由于我们不想加锁影响效率,如果该对象不需要在线程间共享,这时候我们可以使用ThreadLocal来使每个线程都持有该对象的副本。
(2)同一个线程前面执行的方法使用了ThreadLocal保存了信息后,后续方法就可以通过ThreadLocal 直接获取到,避免了传参。
举例
最常见的ThreadLocal使用场景为用来解决数据库连接、Session会话管理等。
1.数据库连接:
Spring的@Transactional源码用到了ThreadLocal,因为要保证事务准备阶段,事务处理阶段,事务提交阶段使用的是同一个connection,aop帮我们处理了事务准备阶段和事务提交阶段,我们只需要关注事务处理阶段

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  

2.Session会话

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  

3.ThreadLocal原理

往ThreadLocal设置数据时候,其实是获取到当前线程的ThreadLocalMap成员变量,以这个threadlocal对象为key,数据为value生成一个新的Entry对象保存到ThreadLocalMap中。获取数据的时候也是先获取当前线程的ThreadLocalMap成员变量,然后以这个threadlocal为key获取数据。

//ThreadLocal的set方法源码
public void set(T value) {
	//获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap成员变量
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//以这个threadlocal实例为key添加到map中
        map.set(this, value);
    else
        createMap(t, value);
}
public T get() {
		//获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的threadLocalMap成员变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//以threadlocal为key获取键值对对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

测试

for (int i = 0; i < 3; i++) {
    int temp = i;
    new Thread(() -> {
        //一个线程可以创建多个threadlocal对象
        ThreadLocal tl1 = new ThreadLocal();
        ThreadLocal tl2 = new ThreadLocal();
        tl1.set("aaa" + temp);
        tl2.set("bbb" + temp);
        System.out.println(tl1.get());
        System.out.println(tl2.get());
    }).start();
}
aaa0
bbb0
aaa1
bbb1
aaa2
bbb2

4.ThreadLocal的内存泄漏你了解吗?

14.JUC面试题_第27张图片
原因:

  • 如果ThreadLocal的引用指向了null,由于ThreadLocalMap中的key只持有ThreadLocal的弱引用,所以threadlocal就能顺利被GC回收。此时Entry中的key=null。对应的value永远不会访问到。
  • 如果我们没有手动删除这个Entry,而且当前线程依然运行的前提下。存在着强引用链threadRef->currentThread->threadLocalMap->entry->value,value就不会被回收,而且这个value还不会被访问到,导致value内存泄漏

解决方案:
调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal后,要调用remove()方法。

JDK的设计已经考虑到了这个问题,所以在set()、remove()、resize()、getEntry()方法中会扫描到key为null的Entry,并且把对应的value设置为null,这样value对象就可以被回收。
14.JUC面试题_第28张图片

LockSupport

谈谈你堆LockSupport的了解

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语;
  • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。

为什么可以先唤醒线程后阻塞线程?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够,不能放行。

AQS

说下你对AQS框架的理解

AQS字面意思是抽象的队列同步器,是用来构建锁或者其它同步器组件的基石。
通过内置的FIFO(先进先出)队列来完成线程获取锁的排队工作,通过一个state的int变量表示持有锁的状态。

大致说下AQS的流程

1.假设A,B,C三个线程 依次去抢一个同步锁,首先A先抢到锁把state置为1,将自己设置为线程持有者ExclusiveOwnerThread

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

2.B线程进来后先尝试获取锁tryAcquire(),获取不到添加到队列中addWaiter(),如果队列为空,先创建傀儡节点,再把B添加到傀儡节点后边。

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;
}

private Node enq(final Node node) {
   for (;;) {
       Node t = tail;
       if (t == null) { // Must initialize
           if (compareAndSetHead(new Node())) //创建傀儡节点
               tail = head;
       } else {
           node.prev = t;
           if (compareAndSetTail(t, node)) {
               t.next = node;
               return t;
           }
       }
   }
}

3.B线程调用acquireQueued(),将傀儡节点waitStatus设置为-1,最后调用LockSupport.part(this)方法将自己阻塞

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            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);
    }
}
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;
}

4.C线程进来后也是先尝试获取不到锁,然后添加到B节点后,将B节点的waitStatus设置为-1,最后调用LockSupport.part(this)方法将自己阻塞
5.当A线程解锁后,将state置为0,将线程持有者置为null。最后将队列头节点waitStatus置为0,调用LockSupport的unpark方法,唤醒head节点下一个节点也就是B节点

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;
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

6.B线程唤醒后,接着执行acquireQueued()方法,尝试获取锁成功,然后自己成为头节点,原来头结点指向null方便GC

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            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);
    }
}

7.B线程解锁后,C线程同上

我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?

3个状态:没占用是0,占用了是1,大于1是可重入锁

如果AB两个线程进来了以后,请问这个总共有多少个Node节点?

3个,。当第一个线程进来持有同步锁的时候,A线程进来后先初始化创建一个占位节点,之后再创建线程A的节点。 当B线程进来再创建线程B的节点。

你可能感兴趣的:(interview,面试,java)