高频面试点-多线程及高并发

本文围绕Java多线程编程展开,介绍了volatile、CAS等概念。阐述了集合类不安全问题及解决方法,分析了公平锁、非公平锁等多种锁的特点。还讲解了CountDownLatch等工具、阻塞队列,对比了synchornized和lock,介绍线程实现方式、线程池原理及拒绝策略,最后提及死锁编码与定位。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

 volatile

CAS

集合类不安全

公平锁/非公平锁/可重入锁/递归锁/自旋锁

CountDownLatch/CyclicBarrier/Semaphore

 阻塞队列

synchornized和lock的区别

实现线程的三种方式

线程池

死锁编码及定位分析


 

 volatile

问题:谈谈对volatile的理解?

volatile是Java虚拟机轻量级的同步机制。基本遵守了JMM规范,保证可见性,不保证原子性,禁止指令重排。

三大特性:

  • 保证可见性

JMM(Java内存模型)

工作内存是高速缓存。比直接从内存读取数据快很多

在Java内存模型中,由于Java运行程序的实体是线程,每创建一个线程的时候,JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,单线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存中拷贝进自己的内存空间,然后对变量进行操作,操作完成后再将变量刷回主内存。不能直接操作内存中的变量,各个线程中的工作内存中存储着主内存中的变量拷贝副本,因此不同线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。 

什么是可见性?可见性指当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改。Java内存模型是通过变量修改后将新值同步回内存,在变量读取前从主内存刷新变量值来实现可见性的。三种实现可见性的方式:volatile、synchornized,final。

  • 不保证原子性 

什么是原子性?原子性是指不可分割、完整性,也即当某个线程在做某个具体任务的时候,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。

  • 禁止指令重排

什么是有序性?有序性是指,在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。指令重排会影响数据的依赖关系。 实现有序性除了volatile和synchornized之外,还规定了先行发生原则来保证有序性。(在Java内存模型中,允许编译器和处理器对指令进行重排,指令重排对单线程没有影响,但是在多线程中会影响并发执行的顺序性。

volatile 关键字通过添加内存屏障的方式来禁止指令重排,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障还可以保持变量的可见性(强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本)。

你在哪些地方用到过volatile?

单例模式;读写锁手写一个缓存;JUC包中底层大规模使用。

  使用双重校验单例模式基本能解决线程安全问题,但是由于指令重拍存在,也可能存在线程不安全。将唯一的单例使用volatile修饰,保证多线程间的语义一致性。

CAS

知识链:CAS->Unsafe->CAS底层思想->ABA->原子引用更新->如何规避ABA问题。

问题:讲一讲AtomicInteger,为什么要用CAS而不是synchornized?

CAS是什么?CompareAndSet比较并交换。比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较知道主内存中的值和工作内存中的值一致为止。CAS有三个操作数,内存知V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

它的功能是判断内存中某个位置的值是否为预期值,如果是则更改为新的这个值,这个过程是原子的。  CAS是一条CPU的原子指令,不会造成所谓的数据不一致。

CAS底层原理:unsafe类和CAS思想(自旋)

AtomicInteger原子类就是使用CAS实现的线程安全的类。可在多线程下保证原子性。 

AtomicInteger自增方法:使用CAS实现。

public final int getAndIncrement() {
        //this代表当前对象,valueOffset内存偏移量(内存地址)
        return unsafe.getAndAddInt(this, valueOffset, 1);
}

 

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //获取当前对象在指定地址上的值
            var5 = this.getIntVolatile(var1, var2);
          //当修改失败的时候一直循环
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
}
//获取对象var1在var2地址上的值
public native int getIntVolatile(Object var1, long var2);

 

//如果对象var1在地址var2上的值为var4则将其对象var1在var2上的地址值设置为var5,返回true。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

Unsafe类是什么?

 是CAS类的核心,由于Java方法中无法直接访问底层系统,

需要通过本地方法(native)来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存中的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源进行相应任务。

变量valueOffSet,表示该变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。 

变量value使用volatile修饰,保证了多线程之间的内存可见性。

CAS缺点?

  • 循环时间长,CPU开销大(比较不成功会一直循环,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大开销)
  • 只能保证一个共享变量的原子操作。(多个共享变量的原子性操作只能通过加锁来实现)。
  • ABA问题。

ABA问题?

问题:原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗?

ABA问题: 如果一个变量初次读取的是A值,它的值被改成了B,后来又被改回为A,CAS操作则会认为它从来没有被改变的。

原子引用?

把某个来包装为原子类型。

class User{
    String user;
    int age;

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


public class test {

    public static void main(String[] args) {
        User z3 = new User("z3",22);
        User li4 = new User("LI4",25);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3,li4)+"\t"+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3,li4)+"\t"+atomicReference.get().toString());

    }

}
true	User@4554617c
false	User@4554617c

ABA问题的解决?

AtomicStampedReference(带时间戳的原子引用),可以通过控制变量值的版本来保证CAS的正确性。

​
private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
}

private volatile Pair<V> pair;
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
}

集合类不安全

问题:我们知道ArrayList是线程不安全的,请编码写一个线程不安全的案例并给出解决方案?

ArrayList底层是由动态数组来实现的,默认的初始容量为10,扩容时扩大为原来的1.5倍

  • 并发修改异常
public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //java.util.ConcurrentModificationException
        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
}

故障现象:多线程对list进行修改的时候,会抛出 java.util.ConcurrentModificationException异常(并发修改异常)

Exception in thread "4" Exception in thread "5" Exception in thread "13" Exception in thread "26" java.util.ConcurrentModificationException

导致原因:并发争抢修改导致。当一个线程在使用list并且还没有完成相应操作的时候,其它线程拿到了list进行使用。

解决方法:

  1. Vector对数据操作加了锁,能解决线程安全问题,但并发行不好。(不使用);
  2. Collections.synchronizedList(new ArrayList<>());使用集合工具类,将ArrayList构建成一个同步的List
  3. CopyOnWriteArrayList。使用Java并发包下的CopyOnWriteArrayList。

优化建议:

  • 写时复制

CopyOnWriteArrayList

写时复制:读写分离

维护一个数组,用transient和volatile修饰。

private transient volatile Object[] array;

 add方法:

写操作在一个复制的数组上进行,读操作还是在原始的数组中进行。读写分离,互不影响。

写操作需要加锁(juc包中的ReentrantLock),放置并发写入时导致写入数据丢失。

写操作结束之后,将原始数组指向新的复制数组。

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
}

缺点:内存占用;数据不一致(读操作不能实时读取到最新的数据,写的数据可能还没同步) 

  • Set

HashSet线程不安全,也会导致 java.util.ConcurrentModificationException异常。

解决方法:

  1. 使用集合工具类包装成一个线程安全的Set 
    Collections.synchronizedSet(new HashSet<>());

     

  2. 使用CopyOnWriteArraySet
    Set<String> list = new CopyOnWriteArraySet<>();

     

底层转换成CopyOnWriteArrayList实现相应接口 

private final CopyOnWriteArrayList<E> al;

    /**
     * Creates an empty set.
     */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

HashSet的底层数据结构就是hashMap

private transient HashMap<E,Object> map;

 add方法:hashset的add的元素是底层hashmap的key,值为PRESENT,一个Object类型的常量

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object();

 

  • Map

HashMap多线程修改时也会抛java.util.ConcurrentModificationException异常(并发修改异常)

解决方法:

  1. 使用集合工具类COllections包装成线程安全的map
    ​
    Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
    
    ​

     

公平锁/非公平锁/可重入锁/递归锁/自旋锁

问题:公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁。

  • 公平锁和非公平锁

例如ReentrantLock

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

 默认为非公平锁,构造函数传入true为公平锁。

公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发情况下,有可能会造成优先级反转或饥饿现象(优先级较低的线程一直不能获取锁)。

ReentrantLock默认是非公平锁,可以通过构造函数传入true设置为公平锁,synchornized是非公平锁。

两者的区别:

公平锁:在并发环境中,每个线程获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就是加入等待队列种,以后会按照FIFO的规则从队列种取到自己。

非公平锁:非公平锁比较粗鲁,上来就直接占有锁,如果尝试失败,就再才有类似公平锁那种方式。

非公平锁的优点在于吞吐量比公平锁大。

 吞吐量:单位时间内成功传送数据的数量。

  • 可重入锁(递归锁)

指的是同一线程在外层函数获得锁之后,内层递归函数仍然能获得该锁的同步代码。在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。

Reentrantlock和synchornized都是可重入锁。

可重入锁的最大优点是避免死锁。

  • 自旋锁

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少上下文切换的消耗,缺点是循环会消耗cpu。CAS操作就是一直循环获取锁。

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //获取当前对象在指定地址上的值
            var5 = this.getIntVolatile(var1, var2);
          //当修改失败的时候一直循环
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
}

 好处:循环比较直到获取成功为止,没有类似wait的阻塞。

  • 独占锁(写锁)/共享锁(读锁)/互斥锁

独占锁:该锁一次只能被一个线程持有。对ReentrantLock和sychornized而言都是独占锁

共享锁:该锁可被多个线程持有。

ReentrantReadWriteLock:其读锁是共享,其写锁是独占。

CountDownLatch/CyclicBarrier/Semaphore

问题:CountDownLatch/CyclicBarrier/Semaphore使用过吗?

  • CountDownLatch:

让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。

CountDownLatch主要有两个方法:当一个线程调用await方法时,调用线程会被阻塞,其它线程调用countdown方法会将计数器减1(调用countdown方法线程不会阻塞,当计数器的值变为0的时候,因调用await方法被阻塞的线程会被唤醒,继续执行。(秦灭六国,一统天下)

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

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t上完自习离开教室");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t班长最后关门走人");
}
  • CyclicBarrier

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

public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{System.out.println(("召唤神龙"));});
        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

信号量:主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

public static void main(String[] args) throws InterruptedException {
         Semaphore semaphore = new Semaphore(3);//模拟3个停车位
        for (int i = 0; i <= 6; i++) {//模拟6部车
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t抢到车位");
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"\t停了3秒钟后离开");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
}

 阻塞队列

问题:阻塞队列知道吗?

队列:FIFO

阻塞队列:当阻塞队列是空的时候,从队列种获取元素的操作将会被阻塞。当阻塞队列是满的时候,往队列里添加元素的操作将会被阻塞。

为什么需要BlockingQueue:好处是我们不需要关心什么时候阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue给包办了。

JUC包种的BlockingQueue接口是Collection的子接口。

其实现类主要有以下7个:

  •  ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchornousBlockingQueue

阻塞队列的核心方法:

 

 阻塞队列应用场景:生产者消费者模式,线程池,消息中间件。

synchornized和lock的区别

问题:sychoenized和lock有什么区别?用新的lock有什么好处?你举例说说。

  • 原始构成

sychornized是关键字,属于JVM层面。monitorenter,monitorexit(底层是通过monitor对象来完成的,其实wait和notify方法也依赖于monitor对象只有在同步方法块种才能用wait和notify方法)。

Lock是具体类(java.util.concurrent.locks.lock)是api层面的

  • 使用方法

sychornized不需要用户去手动释放锁,当sychornized代码执行完后系统会自动让线程释放对锁的占用。

ReentrantLock则需要用户去手动释放锁,若没有主动释放,就有可能导致死锁现象。

  • 等待是否可中断

sychornized不可中断,除非抛出异常或者正常运行完成。

ReentrantLock可中断,1.设置超时方法trylock(long timeout,TimeUnit unit) 2.lockInterruptibly()放代码块种,调用interrupt()方法可中断。

  • 加锁是否公平

sychornized是非公平锁

ReentrantLock两者都可以,默认为非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁。

  • 锁绑定多个条件

sychornized没有

ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像sychornized要么随机唤醒一个线程要么唤醒全部线程。

Condition使用实例

/**
 * 题目:多线程下按顺序调用,实现A->B->C三个线程启动,要求如下:
 * AA打印5次,BB打印10次,CC打印15次
 * 上面的重复10轮
 */

class ShareResource{
    private int number = 1;//A:1 B:2 C:3
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5(){
        lock.lock();
        try{
            while (number!=1){
                condition1.await();
            }
            for (int i = 1; i <=5 ; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number = 2;
            condition2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void prin10(){
        lock.lock();
        try{
            while (number!=2){
                condition2.await();
            }
            for (int i = 1; i <=10 ; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number = 3;
            condition3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void prin15(){
        lock.lock();
        try{
            while (number!=3){
                condition3.await();
            }
            for (int i = 1; i <=15 ; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number = 1;
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

}
/**
 * 线程操纵资源类
 * 判断,等待,干活
 * 防止虚假唤醒
 */
public class test2 {

    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                shareResource.print5();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                shareResource.prin10();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                shareResource.prin15();
            }
        },"C").start();
    }
}

原始版生产者消费者实现

/**
 * 初始值为0的变量,两个线程对其交替进行操作,一个加1,一个减1,来5轮
 */
class ShareData{
    private int number = 0; //等于0时可以增加,等于1时可以减少
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws InterruptedException {

        lock.lock();
        try {
            //1 判断
            while (number!=0){
                //等待,不能生产
                condition.await();
            }

            number++;
            System.out.println(Thread.currentThread().getName()+"\t"+number);
            condition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void decrement(){
        lock.lock();
        try {
            //1 判断
            while (number!=1){
                //等待,不能生产
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"\t"+number);
            condition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}


public class 原始版生产者消费者模式 {

    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        for (int i = 1; i <= 5; i++) {
            new Thread(()->{
                try {
                    shareData.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }

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

阻塞队列版生产者消费者实现

class ShareData1{

    //true表示开始生产+消费
    private volatile boolean flag = true;
    //生产消费过程是原子操作
    private AtomicInteger atomicInteger = new AtomicInteger();

    BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(5);

    public void myProd() throws InterruptedException {
        String data = null;
        boolean returnVal;
        while (flag){
            data = atomicInteger.incrementAndGet()+"";
            //将数据放入阻塞队列中
            returnVal = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
            if(returnVal){
                System.out.println(Thread.currentThread().getName()+"\t插入队列"+data+"成功");
            }else{
                System.out.println(Thread.currentThread().getName()+"\t插入队列"+data+"失败");
            }
            TimeUnit.SECONDS.sleep(1L);
        }
        //flag为false停止生产
        System.out.println(Thread.currentThread().getName()+"\t大老板叫停");
    }

    public void myConsumer() throws InterruptedException {
        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超过2秒钟没有取到蛋糕,消费退出");
                System.out.println();
                System.out.println();
                return;
            }

            System.out.println(Thread.currentThread().getName()+"\t消费队列"+result+"成功");
        }
        System.out.println(Thread.currentThread().getName()+"\t大老板叫停");
    }


    public void stop(){
        this.flag = false;
    }

}

public class 生产者消费者模式阻塞队列版 {

    public static void main(String[] args) throws InterruptedException {
        ShareData1 shareData1 = new ShareData1();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t生产线程启动");
            try {
                shareData1.myProd();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Prod").start();

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

        TimeUnit.SECONDS.sleep(5L);
        shareData1.stop();
    }
}

实现线程的三种方式

  • 第一种:继承Thread实现run方法
  • 第二种:实现Runable接口,并将实现类的实例传给Thread的构造方法。还是要通过thread的start方法来调用。
  • 第三种:实现Callable接口,实现Call方法,并且有返回值,call方法会抛异常
class MyThread implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        return 1024;
    }
}

public class callable接口 {
        
        MyThread myThread = new MyThread();
        FutureTask<Integer> futureTask = new FutureTask<>(myThread);
        Thread t1 = new Thread(futureTask);
}

线程池

问题:线程池用过吗?ThreadPoolExecutor谈谈你的理解?

问题:线程池用过吗?生产上你如何设置合理参数?

线程池的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

主要特点为:线程复用;控制最大并发数;管理线程。

第一:降低资源消耗。通过重复利用已经创建的线程降低线程创建和销毁造成的消耗

第二:提高响应的速度。当任务到达时,任务可以不需要等到线程创建就能立即执行

第三:提高线程的客观理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一分配,调优和监控。

线程池的架构:

 

 Executors是Excutor的辅助工具

 
  • 线程池的种类-5种

(了解)

Executors.newScheduledThreadPool()

Executors.newWorkStealingPool(int)-java8新出

重点

Executors.newFixedThreadPool()

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

主要特点:

  • 创建一个定长线程池,可控制最大并发数量,超出的线程会在队列中等待 
  • newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue

执行长期的任务,性能好很多

Executors.newSingleThreadExecutor()

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

主要特点:

  • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
  • newSingleThreadExecutor创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的LinkedBlockingQueue

一个任务一个任务执行的场景

Executors.newCachedThreadPool()

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
 }

 主要特点:

  • 创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  • newCachedThreadPoolr创建的线程池corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,使用SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

执行很多短期异步的小程序或者负载较轻的服务

public class test {
    public static void main(String[] args) throws InterruptedException {
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();
//        ExecutorService threadPool = Executors.newCachedThreadPool();
        ExecutorService threadPool = new ThreadPoolExecutor(2,5,1L,
                TimeUnit.SECONDS,new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try{
            //模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
            for (int i = 1; i <= 8; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

线程池的7个参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
}
  • corePoolSize:线程池中的常驻核心线程数
  • maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  • keepAliveTime:多余的空闲线程的存活时间。当线程数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
  • unit:keepAliveTime的单位
  • workQueue:任务队列,被提交但尚未被执行的任务
  • threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认即可
  • handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数maximumPoolSize时如何来拒绝请求执行的runable的策略。

线程池的底层工作原理

线程池的拒绝策略

等待队列也已经满了,不能接收新的任务了,同时线程池中的max线程也达到了,无法继续为新任务服务,需要拒绝策略机制合理的处理这个问题。

jdk内置的4种拒绝策略

  • AbortPolicy(默认):直接抛出RejectExecutionException异常组织系统正常运行
  • CallerRunsPolicy:“调用者运行“一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务退回到调用者,从而减低新任务的流量。
  • DiscardOldestPolicy:抛弃等待队列种等待最久的任务,然后把当前任务加入队列种尝试再次提交当前任务
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了RejectExecutionHandler接口

线程池选择

问题:你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用哪个最多?

一个都不用,生产上只能使用自定义的。为什么不用?

请求堆积(任务队列使用的是无界的阻塞队列(LinkedBlockingQueue(有界为Integer.MAX_VALUE相当于有界))和创建大量的线程的问题

问题:你在工作中是如何使用线程池的,是否自定义过线程池使用?

自定义线程池

问题:合理配置线程池你是如何考虑的?

先看服务器是几核的

  • CPU密集型

  • IO密集型

死锁编码及定位分析

  • 什么是死锁

  • 产生死锁的主要原因

  • 解决

jps命令定位进程号

jstack找到死锁查看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值