2020大厂必问面试题(Java多线程)

2020大厂必问面试题(Java多线程)

1.Volatile相关

1.请谈谈你对 volatile 的理解
答:volatile 是 Java 虚拟机提供的轻量级的同步机制。

  • 保证可见性
  • 不能保证原子性
  • 禁止指令重排序

要完整地回答好这题,还需要理解Java内存模型(JMM)。
JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。
JMM 同步规定

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 加锁解锁是同一把锁

速率:CPU>内存>硬盘,CPU为了保证高效地工作,会将数据冲刷到缓冲区中。
由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量的储存在主内存,主内存是共享内存区域所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须都工作内存进行看。
首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
Java内存模型图:
Java内存模型图
JMM模型中的一个重要概念:可见性,即某个线程修改了某个共享变量的值,并把该共享变量写回主内存中,其他线程要知道该变量被修改过了。
(1)验证volatile的可见性

package Day34;

/**
 * @Author Zhongger
 * @Description 验证Volatile的可见性
 * @Date 2020.3.4
 */
public class VolatileDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
            myData.addOne();
            System.out.println(Thread.currentThread().getName()+"\t updated a="+myData.a);
        },"A").start();
        
        //第二个线程为main线程
        while (myData.a==0){
            //如果线程间的可见性不能保证,那么此循环回成为死循环
        }
        //如果执行到以下语句,证明volatile可以保证线程间的可见性
        System.out.println(Thread.currentThread().getName()+"\t come here");
    }
}
class MyData {
     //int a = 0;
    volatile int a = 0;
    void addOne() {
        this.a += 1;
    }
}

如果不加 volatile 关键字,则主线程会进入死循环,加 volatile 则主线程能够退出,说明加了 volatile 关键字变量,当有一个线程修改了值,会马上被另一个线程感知到,当前值作废,从新从主内存中获取值。对其他线程可见,这就叫可见性。
(2)测试volatile不能保证原子性

public class VolatileDemo {
    public static void main(String[] args) {
        test2();
    }
    public static void test2(){
        MyData data = new MyData();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.addOne();
                }
            }).start();
        }
        // 默认有 main 线程和 gc 线程
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(data.a);
    }
}
class MyData {
     //int a = 0;
    volatile int a = 0;
    void addOne() {
        this.a += 1;
    }
}

发现并不能输出 20000,因此这就没有保证原子性了。另外要注意,number++在多线程的情况下是线程不安全的,虽然可以使用synchronized给方法加锁,但最好不要杀鸡用牛刀,稍后会将解决方案。那么为什么volatile不能保证原子性呢?主要是因为写值丢失的情况。来看一下下面的代码:

class Test{
    volatile int n=0;
    public void add(){
        n++;
    }
}

编译成字节码文件是这样的:
在这里插入图片描述
要执行n++,需要进行三步,一是获得n的值,然后+1,然后写回主内存。如果没有加synchronized,所有的线程都有可能挣抢到n,并把n拷贝到自己的工作内存区,然后执行加1操作,然后多个线程把操作完的n写入回主内存中,这就容易导致写覆盖,即线程调度时某个写的线程被挂起了,等到它被唤醒之后又把这个值写进去,而没有对新的值进行修改。
那么怎么解决呢?使用JUC下的AtomicInteger

AtomicInteger atomicInteger=new AtomicInteger();//默认为0
    public void addAtomic(){
        atomicInteger.incrementAndGet();//相当于n++
    }

JMM规范中要求保证有序性,看看以下的解释:
在这里插入图片描述
volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:

保证特定操作的执行顺序
保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)
由于编译器个处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。

下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
在这里插入图片描述
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
在这里插入图片描述
线程安全性保证

  • 工作内存与主内存同步延迟现象导致可见性问题
    可以使用 synchronzied 或 volatile 关键字解决,它们可以使用一个线程修改后的变量立即对其他线程可见
  • 对于指令重排导致可见性问题和有序性问题,可以利用 volatile 关键字解决,因为 volatile 的另一个作用就是禁止指令重排序优化

2.你在哪些地方用到过 volatile?

单例模式(DCL,double check lock双端检锁机制)

//volatile是轻量级Synchronized,保证内存的可见性,防止指令重排序
class SingleTon {
    private static volatile SingleTon singleTon;

    private SingleTon() {
    }

    //解决线程安全问题,同时解决懒加载问题,也保证了效率
    public static synchronized SingleTon getSingleTon() {
        if (singleTon == null) {
            //同步代码效率较低
            synchronized (SingleTon.class) {
                if (singleTon == null) {
                    singleTon = new SingleTon();
                }
            }
        }
        return singleTon;
    }
}

DCL机制确不一定线程安全,原因是有指令重排序的存在,所以需要加入 volatile 可以禁止指令重排。当某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。指令重排只会保证单线程情况下语义执行的一致性,而不会保证多线程的情况。

instance = new Singleton()

可以分为以下三步完成(伪代码):

memory = allocate();  // 1.分配对象空间
instance(memory);     // 2.初始化对象
instance = memory;    // 3.设置instance指向刚分配的内存地址,此时instance != null

步骤 2 和步骤 3 不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种优化是允许的。指令可能会重排为以下情况:

memory = allocate();  // 1.分配对象空间
instance = memory;    // 3.设置instance指向刚分配的内存地址,此时instance != null,但对象还没有初始化完成
instance(memory);     // 2.初始化对象

所以不加 volatile 返回的实例不为空,但可能是未初始化的实例。

2.CAS相关

1.先看一个小Demo

package Day35;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author Zhongger
 * @Description CAS算法——比较和交换
 * @Date 2020.3.5
 */
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        //三个线程获取主内存中的值,并当主内存中的值为5时,替换为2020
        for (int i = 0; i < 3; i++) {
            System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t current data:"+atomicInteger.get());
        }

    }
}

运行结果:
在这里插入图片描述
下面用这幅图来辅助理解上述代码:
在这里插入图片描述
首先,主内存中的值为5,当前有三个线程,它们将5拷贝到自己的工作内存中,进行下一步的操作;然后,假设Thread1抢占到了资源,调用了atomicInteger.compareAndSet(5, 2020)方法,该方法的作用是,如果期望值是5的话,那么就使用2020去替换掉它,显然这个时候atomicInteger=5,可以把5替换成2020,然后把2020冲刷回主内存中,并通知其他线程可见,返回true;当Thread2和Thread3想要调用atomicInteger.compareAndSet(5, 2020)方法时,发现期望值已经不是5而是2020了,所以就无法再用2020进行替换了,返回false。
2.底层原理
自旋锁和Unsafe类
先来看看Unsafe类,AtomicInteger类中的getAndIncrement()方法:

/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

发现调用了unsafe类的方法。
这是AtomicInteger类的部分源码

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 {
            // 获取下面 value 的地址偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
	// ......
}

获取Unsafe类,此类是rt.jar包下的类

   private static final Unsafe unsafe = Unsafe.getUnsafe();
  • Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,而需要通过本地(native)方法来访问, Unsafe 类相当一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C 指针一样直接操作内存,因为 Java 中 CAS 操作执行依赖于 Unsafe 类。
  • 变量 vauleOffset,表示该变量值在内存中的偏移量,因为 Unsafe 就是根据内存偏移量来获取数据的。
  • 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性。

3.CAS是什么

  • CAS 的全称 Compare-And-Swap,它是一条 CPU 并发原语。
  • 它的功能是判断内存某一个位置的值是否为预期,如果是则更改这个值,这个过程就是原子的。
  • CAS 并发原语体现在 Java中就是 sun.misc.Unsafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖硬件的功能,通过它实现了原子操作。由于 CAS 是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成,用于完成某一个功能的过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说 CAS 是一条原子指令,不会造成所谓的数据不一致的问题。

分析一下 unsafe.getAndAddInt()方法来更好地理解CAS和自旋锁

obj为AtomicInteger对象本身
valueOffset为该对象的引用地址
expected为期望修改的值
val为修改的数值
public final int getAndAddInt(Object obj, long valueOffset, long expected, int val) {
    int temp;
    do {
        temp = this.getIntVolatile(obj, valueOffset);  // 获取当前对象在其地址上的快照值
    } while (!this.compareAndSwap(obj, valueOffset, temp, temp + val));  // 如果此时 temp 没有被修改,把其值修改成temp+val,就能退出循环;否则重新获取,这个循环的过程,就相当于自旋锁。
    return temp;
}

4.CAS 的缺点

  • 循环时间长开销很大。如果 CAS 失败,会一直尝试,如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销(比如线程数很多,每次比较都是失败,就会一直循环),所以希望是线程数比较小的场景。
  • 只能保证一个共享变量的原子操作。对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性。
  • 引出 ABA 问题。
3.ABA问题相关

1.ABA问题
CAS算法实现一个重要前提是需要提取内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化。比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存位置中取出A,并且线程2进行了一些操作将值变成了B,然后线程2有将值变成了A,这时候线程1进行CAS操作时发现内存中仍然是A,然后线程1操作成功。尽管线程1的CAS操作成功,但不代表这个过程就是没有问题的。
2.演示一下原子引用

package Day37;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author Zhongger
 * @Description 原子引用演示
 * @Date 2020.3.7
 */
public class AtomicReferenceDemo  {
    public static void main(String[] args) {
        AtomicReference<User> userAtomicReference = new AtomicReference<>();
        User user1 = new User("A",21);
        User user2 = new User("B", 26);
        userAtomicReference.set(user1);//将主物理内存中的值设置为A
        System.out.println(userAtomicReference.compareAndSet(user1,user2)+"\t"+userAtomicReference.get().toString());//CAS算法
        System.out.println(userAtomicReference.compareAndSet(user1,user2)+"\t"+userAtomicReference.get().toString());//经过上一行代码的CAS算法,主物理内存中的值是B而不是A,返回false
    }
}

class User{
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", age=" + age +
                '}';
    }
}

在这里插入图片描述
3.如何解决ABA问题?
时间戳原子引用。一种具备版本号机制的原子引用类,每修改一个值时,就将版本号更新。
先看一下产生ABA问题的代码:

package Day37;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author Zhongger
 * @Description ABA问题
 * @Date 2020.3.7
 */
public class ABAProblem {
    private static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);

    public static void main(String[] args) {
        new Thread(()->{
            atomicReference.compareAndSet(100,127);//由于Integer的范围,expect和update的值都应该在-128~127之间
            System.out.println("100->101:"+atomicReference.get());
            atomicReference.compareAndSet(127,100);//ABA操作,将100改成127,然后将127又改回100
            System.out.println("101->100:"+atomicReference.get());
        },"Thread1").start();

        new Thread(()->{
            try {//确保Thread1完成一次ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicReference.compareAndSet(100,2020);
            //读取到主存中值仍然为100,执行更新操作,其实中途主存的值发生了100->127->100的变化
            System.out.println("最终结果"+atomicReference.get());//返回2020
        },"Thread2").start();
    }
}

结果为:
在这里插入图片描述

引入AtomicStampedReference类来解决ABA问题,使得版本号不一致的CAS操作无法完成。

package Day37;

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Author Zhongger
 * @Description 使用AtomicStampedReference来解决ABA问题
 * @Date 2020.3.7
 */
public class ABASolution {
    private static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);
    //主内存中初始值为100,版本号为1
    public static void main(String[] args) {
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();//当前版本号
            System.out.println(Thread.currentThread().getName()+"\t 第1次的版本号"+stamp);
            try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }//等待Thread2也拿到相同的版本号
            atomicStampedReference.compareAndSet(100,127,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);//更新一次,版本号加1
            System.out.println(Thread.currentThread().getName()+"\t 第2次的版本号"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(127,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);//更新一次,版本号加1
            System.out.println(Thread.currentThread().getName()+"\t 第3次的版本号"+atomicStampedReference.getStamp());

        },"Thread1").start();

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();//当前版本号
            System.out.println(Thread.currentThread().getName()+"\t 第1次的版本号"+stamp);
            try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); }//等待Thread1完成一次ABA操作
            boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
            int newStamp = atomicStampedReference.getStamp();//最新版本号
            System.out.println(Thread.currentThread().getName()+"\t修改成功否"+result+"当前实际版本号"+newStamp);
            System.out.println(Thread.currentThread().getName()+"\t当前最新值"+atomicStampedReference.getReference());
        },"Thread2").start();
    }

}

运行结果如下:
在这里插入图片描述
可见,当Thread2与Thread1的版本号不一致时,CAS操作无法完成。

4.集合类线程不安全,请编写一个不安全的案例并给出解决方案。
public class ArrayListDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                list.add(random.nextInt(10));
                System.out.println(list);
            }).start();
        }
    }
}

发现报 java.util.ConcurrentModificationException 并发修改异常。
解决方案

  • new Vector();
  • Collections.synchronizedList(new ArrayList<>());
  • new CopyOnWriteArrayList<>();

优化建议
在读多写少的时候推荐使用 CopeOnWriteArrayList 这个类

另外,Set和Map的案例这里就不写了,可以看我之前写过的一篇博客。
https://blog.csdn.net/weixin_43395911/article/details/104586689

5.java中的锁

1.公平和非公平锁

  • 公平锁:是指多个线程按照申请的顺序来获取值
  • 非公平锁:是指多个线程获取值的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,可能会造成优先级翻转或者饥饿现象
  • 两者区别
    公平锁:在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否者就会加入到等待队列中,以后会按照 FIFO 的规则获取锁
    非公平锁:一上来就尝试占有锁,如果失败再进行排队。在JUC中ReentrantLock的创建可以知道构造函数的boolean类型来获得公平锁或非公平锁,默认是非公平锁;synchronized也是一种非公平锁。非公平锁的吞吐量比公平锁的大。

2.可重入锁和不可重入锁

  • 可重入锁:也叫递归锁,指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取到该锁,在同一个线程在外层方法获取锁的时候,在进入内层方法或会自动获取该锁。可重入锁的最大作用是避免死锁。
  • 不可重入锁: 所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
package Day38;

/**
 * @Author Zhongger
 * @Description 可重入锁演示
 * @Date 2020.3.8
 */
public class ReentrantLockDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSMS();
        },"A").start();

        new Thread(()->{
            phone.sendSMS();
        },"B").start();
    }
}
//资源类
class Phone{
    public synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName() + "\t 执行了sendSMS方法");
        sendEmail();
        //sendSMS()方法调用了加了锁的sendEmail方法,如果Thread.currentThread().getName()是一致的
        //说明synchronized是可重入锁
    }
    public synchronized void sendEmail(){
        System.out.println(Thread.currentThread().getName() + "\t 执行了sendEmail方法");
    }
}

运行结果如下:
在这里插入图片描述
由此可见,当一个线程拿到了synchronized锁时,可以执行所有的带有synchronized的方法,当然普通方法也是可以的。
同理,ReentrantLock同样也是可重入锁。
手写一个可重入锁:

public class ReentrantLock {
    boolean isLocked = false;
    Thread lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock() throws InterruptedException {
        Thread thread = Thread.currentThread();
        while (isLocked && lockedBy != thread) {
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    
    public synchronized void unlock() {
        if (Thread.currentThread() == lockedBy) {
            lockedCount--;
            if (lockedCount == 0) {
                isLocked = false;
                notify();
            }
        }
    }
}

测试:

public class Count {
    ReentrantLock lock = new ReentrantLock();
    public void print() throws InterruptedException{
        lock.lock();
        doAdd();
        lock.unlock();
    }

    private void doAdd() throws InterruptedException {
        lock.lock();
        // do something
        System.out.println("ReentrantLock");
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        count.print();
    }
}

发现可以输出 ReentrantLock,我们设计两个线程调用 print() 方法,第一个线程调用 print() 方法获取锁,进入 lock() 方法,由于初始 lockedBy 是 null,所以不会进入 while 而挂起当前线程,而是是增量 lockedCount 并记录 lockBy 为第一个线程。接着第一个线程进入 doAdd() 方法,由于同一进程,所以不会进入 while 而挂起,接着增量 lockedCount,当第二个线程尝试lock,由于 isLocked=true,所以他不会获取该锁,直到第一个线程调用两次 unlock() 将 lockCount 递减为0,才将标记为 isLocked 设置为 false。
手写一个不可重入锁

public class NotReentrantLock {
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}

测试:

public class Count {
    NotReentrantLock lock = new NotReentrantLock();
    public void print() throws InterruptedException{
        lock.lock();
        doAdd();
        lock.unlock();
    }

    private void doAdd() throws InterruptedException {
        lock.lock();
        // do something
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        count.print();
    }
}

当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。
3.自旋锁
是指定尝试获取锁的线程不会立即堵塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上线文切换的消耗,缺点就是循环会消耗 CPU。

手动实现自旋锁

public class SpinLock {
    private AtomicReference<Thread> atomicReference = new AtomicReference<>();
    private void lock () {
        System.out.println(Thread.currentThread() + " coming...");
        while (!atomicReference.compareAndSet(null, Thread.currentThread())) {
            // loop
        }
    }

    private void unlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(thread + " unlock...");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock = new SpinLock();
        new Thread(() -> {
            spinLock.lock();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hahaha");
            spinLock.unlock();

        }).start();

        Thread.sleep(1);

        new Thread(() -> {
            spinLock.lock();
            System.out.println("hehehe");
            spinLock.unlock();
        }).start();
    }
}

获取锁的时候,如果原子引用为空就获取锁,不为空表示其他线程获取了锁,就循环等待。
4.独占锁(写锁)/共享锁(读锁)
独占锁:指该锁一次只能被一个线程持有
共享锁:该锁可以被多个线程持有
对于 ReentrantLock 和 synchronized 都是独占锁;对与 ReentrantReadWriteLock 其读锁是共享锁而写锁是独占锁。读锁的共享可保证并发读是非常高效的,读写、写读和写写的过程是互斥的。
具体的例子见我之前写过的博客
https://blog.csdn.net/weixin_43395911/article/details/104604784

6.CountDownLatch/CyclicBarrier/Semaphore 使用过吗?

1.CountDownLatch
让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒。CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,调用线程会被堵塞,其他线程调用 countDown 方法会将计数减一(调用 countDown 方法的线程不会堵塞),当计数其值变为零时,因调用 await 方法被堵塞的线程会被唤醒,继续执行。
2.CyclicBarrier
我们假设有这么一个场景,每辆车只能坐个人,当车满了,就发车。
3.Semaphore
假设我们有 3 个停车位,6 辆车去抢

具体案例可以看我之前写的博客
https://blog.csdn.net/weixin_43395911/article/details/104604784

7.堵塞队列

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)对元素进行排序。
LinkedBlokcingQueue:是一个基于链表结构的阻塞队列,此队列按 FIFO(先进先出)对元素进行排序,吞吐量通常要高于 ArrayBlockingQueue。
SynchronousQueue:是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlokcingQueue。

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如图所示:
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
在这里插入图片描述
核心方法

方法\行为抛异常特定的值阻塞超时
插入方法add(o)offer(o)put(o)offer(o, timeout, timeunit)
移除方法poll()、remove(o)take()poll(timeout, timeunit)
检查方法element()peek()

行为解释:
抛异常:如果操作不能马上进行,则抛出异常
特定的值:如果操作不能马上进行,将会返回一个特殊的值,一般是 true 或者 false
阻塞:如果操作不能马上进行,操作会被阻塞
超时:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是 true 或者 false

插入方法:
add(E e):添加成功返回true,失败抛 IllegalStateException 异常
offer(E e):成功返回 true,如果此队列已满,则返回 false
put(E e):将元素插入此队列的尾部,如果该队列已满,则一直阻塞

删除方法:
remove(Object o) :移除指定元素,成功返回true,失败返回false
poll():获取并移除此队列的头元素,若队列为空,则返回 null
take():获取并移除此队列头元素,若没有元素则一直阻塞

检查方法:
element() :获取但不移除此队列的头元素,没有元素则抛异常
peek() :获取但不移除此队列的头;若队列为空,则返回 null

SynchronousQueue
SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。

public class SynchronousQueueDemo {

    public static void main(String[] args) {
        SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                synchronousQueue.put(1);
                Thread.sleep(3000);
                synchronousQueue.put(2);
                Thread.sleep(3000);
                synchronousQueue.put(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                Integer val = synchronousQueue.take();
                System.out.println(val);
                Integer val2 = synchronousQueue.take();
                System.out.println(val2);
                Integer val3 = synchronousQueue.take();
                System.out.println(val3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

使用场景
生产者消费者模式、线程池、消息中间件

8.synchronized 和 Lock 有什么区别?

原始结构
synchronized 是关键字属于 JVM 层面,反应在字节码上是 monitorenter 和 monitorexit,其底层是通过 monitor 对象来完成,其实 wait/notify 等方法也是依赖 monitor 对象只有在同步快或方法中才能调用 wait/notify 等方法。
Lock 是具体类(java.util.concurrent.locks.Lock)是 api 层面的锁。
使用方法
synchronized 不需要用户手动去释放锁,当 synchronized 代码执行完后系统会自动让线程释放对锁的占用。
ReentrantLock 则需要用户手动的释放锁,若没有主动释放锁,可能导致出现死锁的现象,lock() 和 unlock() 方法需要配合 try/finally 语句来完成。
等待是否可中断
synchronized 不可中断,除非抛出异常或者正常运行完成。
ReentrantLock 可中断,设置超时方法 tryLock(long timeout, TimeUnit unit),lockInterruptibly() 放代码块中,调用 interrupt() 方法可中断。
加锁是否公平
synchronized 非公平锁
ReentrantLock 默认非公平锁,构造方法中可以传入 boolean 值,true 为公平锁,false 为非公平锁。
锁可以绑定多个 Condition
synchronized 没有 Condition。
ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程。

9.线程池使用过吗?谈谈对 ThreadPoolExector 的理解?

1.为什使用线程池,线程池的优势?
线程池用于多线程处理中,它可以根据系统的情况,可以有效控制线程执行的数量,优化运行效果。线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,那么超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

主要特点为:

线程复用
控制最大并发数量
管理线程
主要优点

降低资源消耗,通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
提高相应速度,当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅仅会消耗系统资源,还会降低体统的稳定性,使用线程可以进行统一分配,调优和监控。
2.线程池如何使用?
架构说明
在这里插入图片描述
编码实现
Executors.newSingleThreadExecutor():只有一个线程的线程池,因此所有提交的任务是顺序执行
Executors.newCachedThreadPool():线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除
Executors.newFixedThreadPool():拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待
Executors.newScheduledThreadPool():用来调度即将执行的任务的线程池
Executors.newWorkStealingPool(): newWorkStealingPool适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中

ThreadPoolExecutor
ThreadPoolExecutor作为java.util.concurrent包对外提供基础实现,以内部线程池的形式对外提供管理任务执行,线程调度,线程池管理等等服务。
线程池的几个重要参数介绍和底层原理
参见我之前的博客
https://blog.csdn.net/weixin_43395911/article/details/104625100

线程池用过吗?生产上你如何设置合理参数?
线程池的拒绝策略你谈谈?

是什么
等待队列已经满了,再也塞不下新的任务,同时线程池中的线程数达到了最大线程数,无法继续为新任务服务。
拒绝策略
AbortPolicy:处理程序遭到拒绝将抛出运行时 RejectedExecutionException
CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
DiscardPolicy:不能执行的任务将被删除
DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
你在工作中单一的、固定数的和可变的三种创建线程池的方法,你用哪个多,超级大坑?
如果读者对Java中的阻塞队列有所了解的话,看到这里或许就能够明白原因了。

Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。

ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。

这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。

而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。
合理配置线程池你是如果考虑的?
CPU 密集型
CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。
CPU 密集型任务尽可能的少的线程数量,一般为 CPU 核数 + 1 个线程的线程池。
IO 密集型
由于 IO 密集型任务线程并不是一直在执行任务,可以多分配一点线程数,如 CPU * 2 。
也可以使用公式:CPU 核数 / (1 - 阻塞系数);其中阻塞系数在 0.8 ~ 0.9 之间。

10.死锁编码以及定位分析

产生死锁的原因

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种相互等待的现象,如果无外力的干涉那它们都将无法推进下去,如果系统的资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        DeadLockDemo deadLockDemo = new DeadLockDemo();
        Executor executor = Executors.newFixedThreadPool(2);
        executor.execute(() -> deadLockDemo.method(lockA, lockB));
        executor.execute(() -> deadLockDemo.method(lockB, lockA));

    }

    public void method(String lock1, String lock2) {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + "--获取到:" + lock1 + "; 尝试获取:" + lock2);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("获取到两把锁!");
            }
        }
    }
}

解决

jps -l 命令查定位进程号

28519 org.jetbrains.jps.cmdline.Launcher
32376 com.intellij.idea.Main
28521 com.cuzz.thread.DeadLockDemo
27836 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
28591 sun.tools.jps.Jps

jstack 28521 找到死锁查看

2019-05-07 00:04:15
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.191-b12 mixed mode):

"Attach Listener" #13 daemon prio=9 os_prio=0 tid=0x00007f7acc001000 nid=0x702a waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
// ...
Found one Java-level deadlock:
=============================
"pool-1-thread-2":
  waiting to lock monitor 0x00007f7ad4006478 (object 0x00000000d71f60b0, a java.lang.String),
  which is held by "pool-1-thread-1"
"pool-1-thread-1":
  waiting to lock monitor 0x00007f7ad4003be8 (object 0x00000000d71f60e8, a java.lang.String),
  which is held by "pool-1-thread-2"

Java stack information for the threads listed above:
===================================================
"pool-1-thread-2":
        at com.cuzz.thread.DeadLockDemo.method(DeadLockDemo.java:34)
        - waiting to lock <0x00000000d71f60b0> (a java.lang.String)
        - locked <0x00000000d71f60e8> (a java.lang.String)
        at com.cuzz.thread.DeadLockDemo.lambda$main$1(DeadLockDemo.java:21)
        at com.cuzz.thread.DeadLockDemo$$Lambda$2/2074407503.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
"pool-1-thread-1":
        at com.cuzz.thread.DeadLockDemo.method(DeadLockDemo.java:34)
        - waiting to lock <0x00000000d71f60e8> (a java.lang.String)
        - locked <0x00000000d71f60b0> (a java.lang.String)
        at com.cuzz.thread.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
        at com.cuzz.thread.DeadLockDemo$$Lambda$1/558638686.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

最后发现一个死锁。

本文是我在B站上学习面试题的总结,希望大家可以点赞收藏,这些面试题都是大厂必问的,祝愿各位都能找到心仪的工作!另外,我这里有深入理解Java虚拟机(第三版)的电子版资源,需要的话可以添加我的微信973593026.

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值