两万字!多线程50问!

RTX 50系显卡运行DeepPrep的CUDA GPU支持全链路修复指南 CUDA是NVIDIA GPU通用并行计算平台的核心技术栈,其正常工作依赖驱动、运行时、容器工具与应用镜像四层严格对齐。当新一代Blackwell架构显卡(如RTX 5090)遭遇旧版CUDA生态时,常因PCIe设备ID缺失、Compute Capability不识别、nvidia-container-toolkit架构白名单限制及镜像CUDA版本锁死等断层,导致‘nvidia-smi可见但CUDA初始化失败’。这类问题本质是硬件演进与软件兼容性之间的系统性错配,广泛影响神经影像预处理、医学AI训练等GPU 阅读详情

前言

大家好,我是捡田螺的小男孩。金九银十快要来了,整理了50道多线程并发面试题,大家可以点赞、收藏起来,慢慢品!~

  • github地址,麻烦给个star鼓励一下,感谢感谢
  • 公众号:捡田螺的小男孩(欢迎关注,干货多多)

1、为什么要使用多线程

选择多线程的原因,就是因为快。举个例子:

如果要把1000块砖搬到楼顶,假设到楼顶有几个电梯,你觉得用一个电梯搬运快,还是同时用几个电梯同时搬运快呢?这个电梯就可以理解为线程。

所以,我们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。

当然,有利就有弊,多线程场景下,我们要保证线程安全,就需要考虑加锁。加锁如果不恰当,就很很耗性能。

2. 创建线程有几种方式?

Java中创建线程主要有以下这几种方式:

  • 定义Thread类的子类,并重写该类的run方法
  • 定义Runnable接口的实现类,并重写该接口的run()方法
  • 定义Callable接口的实现类,并重写该接口的call()方法,一般配合Future使用
  • 线程池的方式

2.1 定义Thread类的子类,并重写该类的run方法

public class ThreadTest {

    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("关注公众号:捡田螺的小男孩");
    }
}
复制代码

2.2 定义Runnable接口的实现类,并重写该接口的run()方法

public class ThreadTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("关注公众号:捡田螺的小男孩");
    }
}
//运行结果:
关注公众号:捡田螺的小男孩

复制代码

2.3 定义Callable接口的实现类,并重写该接口的call()方法

如果想要执行的线程有返回,可以使用Callable。

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThreadCallable mc = new MyThreadCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

class MyThreadCallable implements Callable {
    @Override
    public String call()throws Exception {
        return "关注公众号:捡田螺的小男孩";
    }
}
//运行结果:
关注公众号:捡田螺的小男孩
复制代码

2.4 线程池的方式

日常开发中,我们一般都是用线程池的方式执行异步任务。

public class ThreadTest {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,
                TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20), new CustomizableThreadFactory("Tianluo-Thread-pool"));
        executorOne.execute(() -> {
            System.out.println("关注公众号:捡田螺的小男孩");
        });

        //关闭线程池
        executorOne.shutdown();
    }
}
复制代码

3. start()方法和run()方法的区别

其实start和run的主要区别如下:

  • start方法可以启动一个新线程,run方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程。
  • start方法实现了多线程,而run方法没有实现多线程。
  • start不能被重复调用,而run方法可以。
  • start方法中的run代码可以不执行完,就继续执行下面的代码,也就是说进行了线程切换。然而,如果直接调用run方法,就必须等待其代码全部执行完才能继续执行下面的代码。

大家可以结合代码例子来看看哈~

public class ThreadTest {

    public static void main(String[] args){
        Thread t=new Thread(){
            public void run(){
                pong();
            }
        };
        t.start();
        t.run();
        t.run();
        System.out.println("好的,马上去关注:捡田螺的小男孩"+ Thread.currentThread().getName());
    }

    static void pong(){
        System.out.println("关注公众号:捡田螺的小男孩"+ Thread.currentThread().getName());
    }
}

//输出
关注公众号:捡田螺的小男孩main
关注公众号:捡田螺的小男孩main
好的,马上去关注:捡田螺的小男孩main
关注公众号:捡田螺的小男孩Thread-0
复制代码

4. 线程和进程的区别

  • 进程是运行中的应用程序,线程是进程的内部的一个执行序列
  • 进程是资源分配的最小单位,线程是CPU调度的最小单位。
  • 一个进程可以有多个线程。线程又叫做轻量级进程,多个线程共享进程的资源
  • 进程间切换代价大,线程间切换代价小
  • 进程拥有资源多,线程拥有资源少地址
  • 进程是存在地址空间的,而线程本身无地址空间,线程的地址空间是包含在进程中的

举个例子:

你打开QQ,开了一个进程;打开了迅雷,也开了一个进程。

在QQ的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。

所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成QQ的运行,那么这“多个工作”分别有一个线程。

所以一个进程管着多个线程。

通俗的讲:“进程是爹妈,管着众多的线程儿子”...

5. 说一下 Runnable和 Callable有什么区别?

  • Runnable接口中的run()方法没有返回值,是void类型,它做的事情只是纯粹地去执行run()方法中的代码而已;
  • Callable接口中的call()方法是有返回值的,是一个泛型。它一般配合Future、FutureTask一起使用,用来获取异步执行的结果。
  • Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常;

大家可以看下它俩的API:

@FunctionalInterfacepublic interface Callable<V> {
    /**
     * 支持泛型V,有返回值,允许抛出异常
     */
    V call() throws Exception;
}

@FunctionalInterface
public interface Runnable {
    /**
     *  没有返回值,不能继续上抛异常
     */
    public abstract void run();
}
复制代码

为了方便大家理解,写了一个demo,小伙伴们可以看看哈:

/*
 *  @Author 关注公众号:捡田螺的小男孩
 *  @date 2022-07-11
 */
public class CallableRunnableTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        Callable<String> callable =new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "你好,callable,关注公众号:捡田螺的小男孩";
            }
        };

        //支持泛型
        Future<String> futureCallable = executorService.submit(callable);

        try {
            System.out.println("获取callable的返回结果:"+futureCallable.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("你好呀,runnable,关注公众号:捡田螺的小男孩");
            }
        };

        Future<?> futureRunnable = executorService.submit(runnable);
        try {
            System.out.println("获取runnable的返回结果:"+futureRunnable.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        executorService.shutdown();

    }
}
//运行结果
获取callable的返回结果:你好,callable,关注公众号:捡田螺的小男孩
你好呀,runnable,关注公众号:捡田螺的小男孩
获取runnable的返回结果:null
复制代码

6. 聊聊volatile作用,原理

volatile关键字是Java虚拟机提供的的最轻量级的同步机制。它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性。

我们先来一起回忆下java内存模型(jmm):

  • Java虚拟机规范试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。
  • Java内存模型规定所有的变量都是存在主内存当中,每个线程都有自己的工作内存。这里的变量包括实例变量和静态变量,但是不包括局部变量,因为局部变量是线程私有的。
  • 线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存。并且每个线程不能访问其他线程的工作内存。

volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。

volatile保证可见性和禁止指令重排,都跟内存屏障有关。我们来看一段volatile使用的demo代码:

/**
 * 关注公众号:捡田螺的小男孩
 **/
public class Singleton {  
   private volatile static Singleton instance;  
   private Singleton (){}  
   public static Singleton getInstance() {  
   if (instance == null) {  
       synchronized (Singleton.class) {  
       if (instance == null) {  
           instance = new Singleton();  
       }  
       }  
   }  
   return instance;  
   }  
}  
复制代码

编译后,对比有volatile关键字和没有volatile关键字时所生成的汇编代码,发现有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令,lock指令相当于一个内存屏障

lock指令相当于一个内存屏障,它保证以下这几点:

  1. 重排序时不能把后面的指令重排序到内存屏障之前的位置
  2. 将本处理器的缓存写入内存
  3. 如果是写入动作,会导致其他处理器中对应的缓存无效。

第2点和第3点就是保证volatile保证可见性的体现嘛,第1点就是禁止指令重排的体现。

内存屏障四大分类:(Load 代表读取指令,Store代表写入指令)

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

有些小伙伴,可能对这个还是有点疑惑,内存屏障这玩意太抽象了。我们照着代码看下吧:

内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性,哈哈有关于volatile的底层实现,我们就讨论到这哈

7. 说说并发与并行的区别?

并发和并行最开始都是操作系统中的概念,表示的是CPU执行多个任务的方式。

  • 顺序:上一个开始执行的任务完成后,当前任务才能开始执行
  • 并发:无论上一个开始执行的任务是否完成,当前任务都可以开始执行

(即 A B 顺序执行的话,A 一定会比 B 先完成,而并发执行则不一定。)

  • 串行:有一个任务执行单元,从物理上就只能一个任务、一个任务地执行
  • 并行:有多个任务执行单元,从物理上就可以多个任务一起执行

(即在任意时间点上,串行执行时必然只有一个任务在执行,而并行则不一定。)

知乎有个很有意思的回答,大家可以看下:

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是同时。

来源:知乎

8.synchronized 的实现原理以及锁优化?

synchronized是Java中的关键字,是一种同步锁。synchronized关键字可以作用于方法或者代码块。

一般面试时。可以这么回答:

8.1 monitorenter、monitorexit、ACC_SYNCHRONIZED

如果synchronized作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit两个指令实现同步;如果作用synchronized作用于方法,反编译可以看到ACCSYNCHRONIZED标记,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能。

  • 同步代码块是通过monitorenter和monitorexit来实现,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。
  • 同步方法是通过中设置ACCSYNCHRONIZED标志来实现,当线程执行有ACCSYNCHRONI标志的方法,需要获得monitor锁。每个对象都与一个monitor相关联,线程可以占有或者释放monitor。

8.2 monitor监视器

monitor是什么呢?操作系统的管程(monitors)是概念原理,ObjectMonitor是它的原理实现。

在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:

ObjectMonitor() {_header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

复制代码

ObjectMonitor中几个关键字段的含义如图所示:

8.3 Java Monitor 的工作机理

  • 想要获取monitor的线程,首先会进入_EntryList队列。
  • 当某个线程获取到对象的monitor后,进入Owner区域,设置为当前线程,同时计数器count加1。
  • 如果线程调用了wait()方法,则会进入WaitSet队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入WaitSet队列阻塞等待。
  • 如果其他线程调用 notify() / notifyAll() ,会唤醒WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入Owner区域。
  • 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。

8.4 对象与monitor关联

  • 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对象填充(Padding)。
  • 对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。

Mark Word 是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。

重量级锁,指向互斥量的指针。其实synchronized是重量级锁,也就是说Synchronized的对象锁,Mark Word锁标识位为10,其中指针指向的是Monitor对象的起始地址。

9. 线程有哪些状态?

线程有6个状态,分别是:New, Runnable, Blocked, Waiting, Timed_Waiting, Terminated。

转换关系图如下:

  • New:线程对象创建之后、但还没有调用start()方法,就是这个状态。
/**
 * 关注公众号:捡田螺的小男孩
 */
public class ThreadTest {

    public static void main(String[] args) {
        Thread thread = new Thread();
        System.out.println(thread.getState());
    }
}

//运行结果:
NEW
复制代码
  • Runnable:它包括就绪(ready)和运行中(running)两种状态。如果调用start方法,线程就会进入Runnable状态。它表示我这个线程可以被执行啦(此时相当于ready状态),如果这个线程被调度器分配了CPU时间,那么就可以被执行(此时处于running状态)。
public class ThreadTest {

    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();
        System.out.println(thread.getState());
    }
}
//运行结果:
RUNNABLE
复制代码
  • Blocked: 阻塞的(被同步锁或者IO锁阻塞)。表示线程阻塞于锁,线程阻塞在进入synchronized关键字修饰的方法或代码块(等待获取锁)时的状态。比如前面有一个临界区的代码需要执行,那么线程就需要等待,它就会进入这个状态。它一般是从RUNNABLE状态转化过来的。如果线程获取到锁,它将变成RUNNABLE状态
Thread t =new Thread(new Runnable {
    void run() {
        synchronized (lock) { // 阻塞于这里,变为Blocked状态
            // dothings
        } 
    }
});
t.getState(); //新建之前,还没开始调用start方法,处于New状态

t.start(); //调用start方法,就会进入Runnable状态
复制代码
  • WAITING : 永久等待状态,进入该状态的线程需要等待其他线程做出一些特定动作(比如通知)。处于该状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。一般Object.wait。
Thread t =new Thread(new Runnable {
    void run() {
        synchronized (lock) { // Blocked
            // dothings
            while (!condition) {
                lock.wait(); // into Waiting
            }
        } 
    }
});
t.getState(); // New

t.start(); // Runnable
复制代码
  • TIMED_WATING: 等待指定的时间重新被唤醒的状态。有一个计时器在里面计算的,最常见就是使用Thread.sleep方法触发,触发后,线程就进入了Timed_waiting状态,随后会由计时器触发,再进入Runnable状态。
Thread t =new Thread(new Runnable {
    void run() {
        Thread.sleep(1000); // Timed_waiting
    }
});
t.getState(); // New
t.start(); // Runnable
复制代码
  • 终止(TERMINATED):表示该线程已经执行完成。

再来看个代码demo吧:

/**
 * 关注公众号:捡田螺的小男孩
 */
public class ThreadTest {

    private static Object object = new Object();

    public static void main(String[] args) throws Exception {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for(int i = 0; i< 1000; i++){
                        System.out.print("");
                    }
                    Thread.sleep(500);
                    synchronized (object){
                        object.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (object){
                        Thread.sleep(1000);
                    }
                    Thread.sleep(1000);
                    synchronized (object){
                        object.notify();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        System.out.println("1"+thread.getState());
        thread.start();
        thread1.start();
        System.out.println("2"+thread.getState());
        while (thread.isAlive()){
            System.out.println("---"+thread.getState());
            Thread.sleep(100);
        }
        System.out.println("3"+thread.getState());
    }
}

运行结果:
1NEW
2RUNNABLE
---RUNNABLE
---TIMED_WAITING
---TIMED_WAITING
---TIMED_WAITING
---TIMED_WAITING
---BLOCKED
---BLOCKED
---BLOCKED
---BLOCKED
---BLOCKED
---WAITING
---WAITING
---WAITING
---WAITING
---WAITING
---WAITING
---WAITING
---WAITING
---WAITING

复制代码

10. synchronized和ReentrantLock的区别?

  • Synchronized是依赖于JVM实现的,而ReenTrantLock是API实现的。
  • 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者性能就差不多了。
  • Synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放。而ReenTrantLock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。
  • ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。
  • ReentrantLock可响应中断、可轮回,而Synchronized是不可以响应中断的

11. wait(),notify()和suspend(),resume()之间的区别

  • wait()方法使得线程进入阻塞等待状态,并且释放锁
  • notify()唤醒一个处于等待状态的线程,它一般跟wait()方法配套使用。
  • suspend()使得线程进入阻塞状态,并且不会自动恢复,必须对应的resume()被调用,才能使得线程重新进入可执行状态。suspend()方法很容易引起死锁问题。
  • resume()方法跟suspend()方法配套使用。

suspend()不建议使用,因为suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

12. CAS?CAS 有什么缺陷,如何解决?

CAS,全称是Compare and Swap,翻译过来就是比较并交换;

CAS涉及3个操作数,内存地址值V,预期原值A,新值B;如果内存位置的值V与预期原A值相匹配,就更新为新值B,否则不更新

CAS有什么缺陷?

  • ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

可以通过AtomicStampedReference 解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。

  • 循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

  • 只能保证一个变量的原子操作。

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。可以通过这两个方式解决这个问题:1. 使用互斥锁来保证原子性; 2.将多个变量封装成对象,通过AtomicReference来保证原子性。

有兴趣的朋友可以看看我之前的这篇实战文章哈~CAS乐观锁解决并发问题的一次实践

13. 说说CountDownLatch与CyclicBarrier 区别

CountDownLatch和CyclicBarrier都用于让线程等待,达到一定条件时再运行。主要区别是:

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

举个例子吧:

  • CountDownLatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。
  • CyclicBarrier:多名短跑运动员要开始田径比赛,只有等所有运动员准备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。

14. 什么是多线程环境下的伪共享

14.1 什么是伪共享?

CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享

现代计算机计算模型:

  • CPU执行速度比内存速度快好几个数量级,为了提高执行效率,现代计算机模型演变出CPU、缓存(L1,L2,L3),内存的模型。
  • CPU执行运算时,如先从L1缓存查询数据,找不到再去L2缓存找,依次类推,直到在内存获取到数据。
  • 为了避免频繁从内存获取数据,聪明的科学家设计出缓存行,缓存行大小为64字节。

也正是因为缓存行的存在,就导致了伪共享问题,如图所示:

假设数据a、b被加载到同一个缓存行。

  • 当线程1修改了a的值,这时候CPU1就会通知其他CPU核,当前缓存行(Cache line)已经失效。
  • 这时候,如果线程2发起修改b,因为缓存行已经失效了,所以「core2 这时会重新从主内存中读取该 Cache line 数据」。读完后,因为它要修改b的值,那么CPU2就通知其他CPU核,当前缓存行(Cache line)又已经失效。
  • 酱紫,如果同一个Cache line的内容被多个线程读写,就很容易产生相互竞争,频繁回写主内存,会大大降低性能。

14.2 如何解决伪共享问题

既然伪共享是因为相互独立的变量存储到相同的Cache line导致的,一个缓存行大小是64字节。那么,我们就可以使用空间换时间的方法,即数据填充的方式,把独立的变量分散到不同的Cache line~

来看个例子:

/**
 * 更多干货内容,关注公众号:捡田螺的小男孩
 */
public class FalseShareTest  {

    public static void main(String[] args) throws InterruptedException {
        Rectangle rectangle = new Rectangle();
        long beginTime = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                rectangle.a = rectangle.a + 1;
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                rectangle.b = rectangle.b + 1;
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("执行时间" + (System.currentTimeMillis() - beginTime));
    }

}

class Rectangle {
    volatile long a;
    volatile long b;
}

//运行结果:
执行时间2815
复制代码

一个long类型是8字节,我们在变量a和b之间不上7个long类型变量呢,输出结果是啥呢?如下:

class Rectangle {
    volatile long a;
    long a1,a2,a3,a4,a5,a6,a7;
    volatile long b;
}
//运行结果
执行时间1113
复制代码

可以发现利用填充数据的方式,让读写的变量分割到不同缓存行,可以很好挺高性能~

15. Fork/Join框架的理解

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join框架需要理解两个点,「分而治之」和「工作窃取算法」。

分而治之

以上Fork/Join框架的定义,就是分而治之思想的体现啦

工作窃取算法

把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~

工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

16. 聊聊ThreadLocal原理?

ThreadLocal的内存结构图

为了对ThreadLocal有个宏观的认识,我们先来看下ThreadLocal的内存结构图

从内存结构图,我们可以看到:

  • Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
  • ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。

关键源码分析

对照着关键源码来看,更容易理解一点哈~

首先看下Thread类的源码,可以看到成员变量ThreadLocalMap的初始值是为null

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码

成员变量ThreadLocalMap的关键源码如下:

staticclass ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //Entry数组
    private Entry[] table;
    
    // ThreadLocalMap的构造器,ThreadLocal作为key
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}
复制代码

ThreadLocal类中的关键set()方法:

public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)  //如果获取的ThreadLocalMap对象不为空
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }
    
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
    }

    void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
        t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal
    }
    
复制代码

ThreadLocal类中的关键get()方法

public Tget() {
        Thread t = Thread.currentThread();//获取当前线程t
        ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
        if (map != null) { //如果获取的ThreadLocalMap对象不为空
            //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue(); //初始化threadLocals成员变量的值
    }
    
     private T setInitialValue() {
        T value = initialValue(); //初始化value的值
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
        if (map != null)
            map.set(this, value);  //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //实例化threadLocals成员变量
        return value;
    }
复制代码

所以怎么回答ThreadLocal的实现原理?如下,最好是能结合以上结构图一起说明哈~

  • Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
  • 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。

大家可以看下我之前这篇文章哈: ThreadLocal的八个关键知识点

17. TreadLocal为什么会导致内存泄漏呢?

  • 弱引用导致的内存泄漏呢?
  • key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?
  • ThreadLocal内存泄漏的demo

17.1 弱引用导致的内存泄漏呢?

我们先来看看TreadLocal的引用示意图哈:

关于ThreadLocal内存泄漏,网上比较流行的说法是这样的:

ThreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。

当ThreadLocal变量被手动设置为null后的引用链图:

实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有key为null的value。

源代码中,是有体现的,如ThreadLocalMap的set方法:

private void set(ThreadLocal<?> key, Object value) {

      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);

      for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
          ThreadLocal<?> k = e.get();

          if (k == key) {
              e.value = value;
              return;
          }

           //如果k等于null,则说明该索引位之前放的key(threadLocal对象)被回收了,这通常是因为外部将threadLocal变量置为null,
           //又因为entry对threadLocal持有的是弱引用,一轮GC过后,对象被回收。
            //这种情况下,既然用户代码都已经将threadLocal置为null,那么也就没打算再通过该对象作为key去取到之前放入threadLocalMap的value, 因此ThreadLocalMap中会直接替换调这种不新鲜的entry。
          if (k == null) {
              replaceStaleEntry(key, value, i);
              return;
          }
        }

        tab[i] = new Entry(key, value);
        int sz = ++size;
        //触发一次Log2(N)复杂度的扫描,目的是清除过期Entry  
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
    }
复制代码

如ThreadLocal的get方法:

public T get() {
    Threadt = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //去ThreadLocalMap获取Entry,方法里面有key==null的清除逻辑
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
             return e;
        else
          //里面有key==null的清除逻辑
          return getEntryAfterMiss(key, i, e);
    }
        
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            // Entry的key为null,则表明没有外部引用,且被GC回收,是一个过期Entry
            if (k == null)
                expungeStaleEntry(i); //删除过期的Entry
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
复制代码

17.2 key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?

有些小伙伴可能有疑问,ThreadLocal的key既然是弱引用.会不会GC贸然把key回收掉,进而影响ThreadLocal的正常使用?

  • 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)

其实不会的,因为有ThreadLocal变量引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null,我们可以跑个demo来验证一下:

public class WeakReferenceTest {
    public static void main(String[] args) {
        Object object = new Object();
        WeakReference<Object> testWeakReference = new WeakReference<>(object);
        System.out.println("GC回收之前,弱引用:"+testWeakReference.get());
        //触发系统垃圾回收
        System.gc();
        System.out.println("GC回收之后,弱引用:"+testWeakReference.get());
        //手动设置为object对象为null
        object=null;
        System.gc();
        System.out.println("对象object设置为null,GC回收之后,弱引用:"+testWeakReference.get());
    }
}
运行结果:
GC回收之前,弱引用:java.lang.Object@7b23ec81
GC回收之后,弱引用:java.lang.Object@7b23ec81
对象object设置为null,GC回收之后,弱引用:null
复制代码

结论就是,小伙伴放下这个疑惑了,哈哈~

17.3 ThreadLocal内存泄漏的demo

给大家来看下一个内存泄漏的例子,其实就是用线程池,一直往里面放对象

public class ThreadLocalTestDemo {

    private static ThreadLocal<TianLuoClass> tianLuoThreadLocal = new ThreadLocal<>();


    public static void main(String[] args) throws InterruptedException {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());

        for (int i = 0; i < 10; ++i) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("创建对象:");
                    TianLuoClass tianLuoClass = new TianLuoClass();
                    tianLuoThreadLocal.set(tianLuoClass);
                    tianLuoClass = null; //将对象设置为 null,表示此对象不在使用了
                   // tianLuoThreadLocal.remove();
                }
            });
            Thread.sleep(1000);
        }
    }

    static class TianLuoClass {
        // 100M
        private byte[] bytes = new byte[100 * 1024 * 1024];
    }
}


创建对象:
创建对象:
创建对象:
创建对象:
Exception in thread "pool-1-thread-4" java.lang.OutOfMemoryError: Java heap space
	at com.example.dto.ThreadLocalTestDemo$TianLuoClass.<init>(ThreadLocalTestDemo.java:33)
	at com.example.dto.ThreadLocalTestDemo$1.run(ThreadLocalTestDemo.java:21)
	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)
复制代码

运行结果出现了OOM,tianLuoThreadLocal.remove();加上后,则不会OOM。

创建对象:
创建对象:
创建对象:
创建对象:
创建对象:
创建对象:
创建对象:
创建对象:
......复制代码

我们这里没有手动设置tianLuoThreadLocal变量为null,但是还是会内存泄漏。因为我们使用了线程池,线程池有很长的生命周期,因此线程池会一直持有tianLuoClass对象的value值,即使设置tianLuoClass = null;引用还是存在的。这就好像,你把一个个对象object放到一个list列表里,然后再单独把object设置为null的道理是一样的,列表的对象还是存在的。

public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        Object object = new Object();
        list.add(object);
        object = null;
        System.out.println(list.size());
    }
    //运行结果
    1
复制代码

所以内存泄漏就这样发生啦,最后内存是有限的,就抛出了OOM了。如果我们加上threadLocal.remove();,则不会内存泄漏。为什么呢?因为threadLocal.remove();会清除Entry,源码如下:

private void remove(ThreadLocal<?> key) {
      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);
      for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
          if (e.get() == key) {
              //清除entry
              e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
复制代码

18 为什么ThreadLocalMap 的 key是弱引用,设计理念是?

通过阅读ThreadLocal的源码,我们是可以看到Entry的Key是设计为弱引用的(ThreadLocalMap使用ThreadLocal的弱引用作为Key的)。为什么要设计为弱引用呢?

我们先来回忆一下四种引用:

  • 强引用:我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

我们先来看看官方文档,为什么要设计为弱引用:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
复制代码

我再把ThreadLocal的引用示意图搬过来:

下面我们分情况讨论:

  • 如果Key使用强引用:当ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏问题。
  • 如果Key使用弱引用:当ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此可以发现,使用弱引用作为Entry的Key,可以多一层保障:弱引用ThreadLocal不会轻易内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:

  • 一种就是,使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除
  • 另外一种方式就是:ThreadLocalMap的自动清除机制去清除过期Entry.(ThreadLocalMap的get(),set()时都会触发对过期Entry的清除)

19. 如何保证父子线程间的共享ThreadLocal数据

我们知道ThreadLocal是线程隔离的,如果我们希望父子线程共享数据,如何做到呢?可以使用InheritableThreadLocal。先来看看demo:

public class InheritableThreadLocalTest {

   public static void main(String[] args) {
       ThreadLocal<String> threadLocal = new ThreadLocal<>();
       InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

       threadLocal.set("关注公众号:捡田螺的小男孩");
       inheritableThreadLocal.set("关注公众号:程序员田螺");

       Thread thread = new Thread(()->{
           System.out.println("ThreadLocal value " + threadLocal.get());
           System.out.println("InheritableThreadLocal value " + inheritableThreadLocal.get());
       });
       thread.start();
       
   }
}
//运行结果
ThreadLocal value null
InheritableThreadLocal value 关注公众号:程序员田螺
复制代码

可以发现,在子线程中,是可以获取到父线程的 InheritableThreadLocal 类型变量的值,但是不能获取到 ThreadLocal 类型变量的值。

获取不到ThreadLocal 类型的值,我们可以好理解,因为它是线程隔离的嘛。InheritableThreadLocal 是如何做到的呢?原理是什么呢?

在Thread类中,除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。它们两类型是一样的:

public class Thread implements Runnable {
   ThreadLocalMap threadLocals = null;
   ThreadLocalMap inheritableThreadLocals = null;
 }
复制代码

Thread类的init方法中,有一段初始化设置:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
      
        ......
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
复制代码

可以发现,当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,但是数据从父线程那里来的。有兴趣的小伙伴们可以在去研究研究源码~

20. 如何保证多线程下 i++ 结果正确?

  • 使用循环CAS,实现i++原子操作
  • 使用锁机制,实现i++原子操作
  • 使用synchronized,实现i++原子操作

举个简单的例子,如下:

/**
 *  关注公众号:捡田螺的小男孩 
 *  非常多干货
 */
public class AtomicIntegerTest {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        testIAdd();
    }

    private static void testIAdd() throws InterruptedException {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 2; j++) {
                    //自增并返回当前值
                    int andIncrement = atomicInteger.incrementAndGet();
                    System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement);
                }
            });
        }
        executorService.shutdown();
        Thread.sleep(100);
        System.out.println("最终结果是 :" + atomicInteger.get());
    }
    
}
复制代码

运行结果:

...
线程:pool-1-thread-1count=1997
线程:pool-1-thread-1 count=1998
线程:pool-1-thread-1 count=1999
线程:pool-1-thread-2 count=315
线程:pool-1-thread-2 count=2000
最终结果是 :2000
复制代码

21. 如何检测死锁?怎么预防死锁?死锁四个必要条件

死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。如图感受一下:

死锁的四个必要条件:

  • 互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。
  • 占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
  • 非抢占:不能强行抢占进程中已占有的资源。
  • 循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。

如何预防死锁?

  • 加锁顺序(线程按顺序办事)
  • 加锁时限 (线程请求所加上权限,超时就放弃,同时释放自己占有的锁)
  • 死锁检测

22. 如果线程过多,会怎样?

使用多线程可以提升程序性能。但是如果使用过多的线程,则适得其反。

过多的线程会影响程序的系统。

  • 一方面,线程的启动和销毁,都是需要开销的。
  • 其次,过多的并发线程也会导致共享有限资源的开销增大。过多的线程,还会导致内存泄漏,笔者在以前公司,看到一个生产问题:一个第三方的包是使用new Thread来实现的,使用完没有恰当回收销毁,最后引发内存泄漏问题。

因此,我们平时尽量使用线程池来管理线程。同时还需要设置恰当的线程数。

23. 聊聊happens-before原则

在Java语言中,有一个先行发生原则(happens-before)。它包括八大规则,如下:

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
  • 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

24. 如何实现两个线程间共享数据

  1. 可以通过类变量直接将数据放到主存中
  2. 通过并发的数据结构来存储数据
  3. 使用volatile变量或者锁
  4. 调用atomic类(如AtomicInteger)

25. LockSupport作用是?

LockSupport是一个工具类。它的主要作用是挂起和唤醒线程。该工具类是创建锁和其他同步类的基础。它的主要方法是

public static void park(Object blocker); // 暂停指定线程
public static void unpark(Thread thread); // 恢复指定的线程
public static void park(); // 无期限暂停当前线程
复制代码

看个代码的例子:

public class LockSupportTest {

    private static Object object = new Object();
    static MyThread thread = new MyThread("线程田螺");

    public static class MyThread extends Thread {

        public MyThread(String name) {
            super(name);
        }

        @Override public void run() {
            synchronized (object) {
                System.out.println("线程名字: " + Thread.currentThread());
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park();
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程被中断了");
                }
                System.out.println("继续执行");
            }
        }
    }

    public static void main(String[] args) {
        thread.start();
        LockSupport.unpark(thread);
        System.out.println("恢复线程调用");
    }
}
//output
恢复线程调用
线程名字: Thread[线程田螺,5,main]
继续执行
复制代码

因为thread线程内部有休眠2秒的操作,所以unpark方法的操作肯定先于park方法的调用。为什么thread线程最终仍然可以结束,是因为park和unpark会对每个线程维持一个许可证(布尔值)

26 线程池如何调优,如何确认最佳线程数?

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目复制代码

我们的服务器CPU核数为8核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 8 = 40。也就是设置 40个线程数最佳。

有兴趣的小伙伴,也可以看这篇文章哈: 线程池到底设置多少线程比较合适?

27. 为什么要用线程池?

线程池:一个管理线程的池子。线程池可以:

  • 管理线程,避免增加创建线程和销毁线程的资源损耗。
  • 提高响应速度。
  • 重复利用线程。

大家可以看看我之前这篇文章,很经典, 面试必备:Java线程池解析

28. Java的线程池执行原理

线程池的执行原理如下:

为了形象描述线程池执行,打个比喻:

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交任务比作提需求

29. 聊聊线程池的核心参数

我们先来看看ThreadPoolExecutor的构造函数

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
   long keepAliveTime,
   TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler) 
复制代码
  • corePoolSize:线程池核心线程数最大值
  • maximumPoolSize:线程池最大线程数大小
  • keepAliveTime:线程池中非核心线程空闲的存活时间大小
  • unit:线程空闲存活时间单位
  • workQueue:存放任务的阻塞队列
  • threadFactory:用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
  • handler:线城池的饱和策略事件,主要有四种类型拒绝策略。

四种拒绝策略

  • AbortPolicy(抛出一个异常,默认的)
  • DiscardPolicy(直接丢弃任务)
  • DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
  • CallerRunsPolicy(交给线程池调用所在的线程进行处理)

几种工作阻塞队列

  • ArrayBlockingQueue(用数组实现的有界阻塞队列,按FIFO排序量)
  • LinkedBlockingQueue(基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列)
  • DelayQueue(一个任务定时周期的延迟执行的队列)
  • PriorityBlockingQueue(具有优先级的无界阻塞队列)
  • SynchronousQueue(一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态)

30.当提交新任务时,异常如何处理?

我们先来看一段代码:

ExecutorServicethreadPool = Executors.newFixedThreadPool(5);
  for (int i = 0; i < 5; i++) {
      threadPool.submit(() -> {
          System.out.println("current thread name" + Thread.currentThread().getName());
          Object object = null;
          System.out.print("result## "+object.toString());
      });
  }
复制代码

显然,这段代码会有异常,我们再来看看执行结果

虽然没有结果输出,但是没有抛出异常,所以我们无法感知任务出现了异常,所以需要添加try/catch。 如下图:

OK,线程的异常处理,我们可以直接try...catch捕获。

最近写了一篇线程池坑相关的,大家可以去看看哈: 细数线程池的10个坑

31. AQS组件,实现原理

AQS,即AbstractQueuedSynchronizer,是构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。可以回答以下这几个关键点哈:

  • state 状态的维护。
  • CLH队列
  • ConditionObject通知
  • 模板方法设计模式
  • 独占与共享模式。
  • 自定义同步器。
  • AQS全家桶的一些延伸,如:ReentrantLock等。

31.1 state 状态的维护

  • state,int变量,锁的状态,用volatile修饰,保证多线程中的可见性。
  • getState()和setState()方法采用final修饰,限制AQS的子类重写它们两。
  • compareAndSetState()方法采用乐观锁思想的CAS算法操作确保线程安全,保证状态 设置的原子性。

31.2 CLH队列

CLH 同步队列,全英文Craig, Landin, and Hagersten locks。是一个FIFO双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。AQS依赖它来完成同步状态state的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

31.3 ConditionObject通知

我们都知道,synchronized控制同步的时候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以实现等待/通知模式。而Lock呢?它提供了条件Condition接口,配合await(),signal(),signalAll() 等方法也可以实现等待/通知机制。ConditionObject实现了Condition接口,给AQS提供条件变量的支持

ConditionObject队列与CLH队列的爱恨情仇:

  • 调用了await()方法的线程,会被加入到conditionObject等待队列中,并且唤醒CLH队列中head节点的下一个节点。
  • 线程在某个ConditionObject对象上调用了singnal()方法后,等待队列中的firstWaiter会被加入到AQS的CLH队列中,等待被唤醒。
  • 当线程调用unLock()方法释放锁时,CLH队列中的head节点的下一个节点(在本例中是firtWaiter),会被唤醒。

31.4 模板方法设计模式

模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

AQS的典型设计模式就是模板方法设计模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生实现,就体现出这个设计模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,给子类实现自定义的同步器。

31.5 独占与共享模式。

  • 独占式: 同一时刻仅有一个线程持有同步状态,如ReentrantLock。又可分为公平锁和非公平锁。
  • 共享模式:多个线程可同时执行,如Semaphore/CountDownLatch等都是共享式的产物。

31.6 自定义同步器

你要实现自定义锁的话,首先需要确定你要实现的是独占锁还是共享锁,定义原子变量state的含义,再定义一个内部类去继承AQS,重写对应的模板方法即可啦

32 Semaphore原理

Semaphore,我们也把它叫做信号量。可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

我们可以把它简单的理解成我们停车场入口立着的那个显示屏,每当有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。

32.1 Semaphore使用demo

我们就以停车场的例子,来实现demo。

假设停车场最多可以停20辆车,现在有100辆要进入停车场。

我们很容易写出以下代码;

public class SemaphoreTest {

    private  static Semaphore semaphore=new Semaphore(20);

    public static void main(String[] args) {
         
        ExecutorService executorService= Executors.newFixedThreadPool(200);

        //模拟100辆车要来
        for (int i = 0; i < 100; i++) {
            executorService.execute(()->{
                System.out.println("===="+Thread.currentThread().getName()+"准备进入停车场==");
                //车位判断
                if (semaphore.availablePermits() == 0) {
                    System.out.println("车辆不足,请耐心等待");
                }

                try {
                    //获取令牌尝试进入停车场
                    semaphore.acquire();
                    System.out.println("====" + Thread.currentThread().getName() + "成功进入停车场");
                    //模拟车辆在停车场停留的时间
                    Thread.sleep(new Random().nextInt(20000));
                    System.out.println("====" + Thread.currentThread().getName() + "驶出停车场");
                    //释放令牌,腾出停车场车位
                    semaphore.release();
                 } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });
            //线程池关闭          
            executorService.shutdown();
        }
    }
}
复制代码

32.2 Semaphore原理

我们来看下实现的原理是怎样的。

  • Semaphore构造函数
  • 可用令牌数
  • 获取令牌
  • 释放令牌
  1. Semaphore构造函数
Semaphoresemaphore=new Semaphore(20);
复制代码

它会创建一个非公平的锁的同步阻塞队列,并且把初始令牌数量(20)赋值给同步队列的state,这个state就是AQS的哈。

//构造函数,创建一个非公平的锁的同步阻塞队列
 public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
    
NonfairSync(int permits) {
    super(permits);
}

//把令牌数量赋值给同步队列的state
Sync(int permits) {
    setState(permits);
}
复制代码

2.可用令牌数

这个availablePermits,获取的就是state值。刚开始为20,所以肯定不会为0嘛。

semaphore.availablePermits();public int availablePermits() {
  return sync.getPermits();
}

final int getPermits() {
  return getState();
}
复制代码
  1. 获取令牌

接着我们再看下获取令牌的API

semaphore.acquire();
复制代码

获取1个令牌

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
  }
  
    public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
      //尝试获取令牌,arg为获取令牌个数
    if (tryAcquireShared(arg) < 0)
        //
        doAcquireSharedInterruptibly(arg);
  }
复制代码

尝试获取令牌,使用了CAS算法。

final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
复制代码

可获取令牌的话,就创建节点,加入阻塞队列;重双向链表的head,tail节点关系,清空无效节点;挂起当前节点线程

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建节点加入阻塞队列
        final Nodenode = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    //返回锁的state
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //重组双向链表,清空无效节点,挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
复制代码
  1. 释放令牌
semaphore.release();
 
  /**
     * 释放令牌
     */
public void release() {
    sync.releaseShared(1);
}

  public final boolean releaseShared(int arg) {
         //释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒所有共享节点线程
            doReleaseShared();
            return true;
        }
        return false;
    }
复制代码

33 synchronized做了哪些优化?什么是偏向锁?什么是自旋锁?锁租化?

在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了synchronized的性能。

  • 偏向锁:在无竞争的情况下,只是在Mark Word里存储当前线程指针,CAS操作都不做。
  • 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。
  • 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
  • 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

34 什么是上下文切换?

什么是CPU上下文?

CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文。

什么是CPU上下文切换?

它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

一般我们说的上下文切换,就是指内核(操作系统的核心)在CPU上对进程或者线程进行切换。进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换。

所以大家有时候会听到这种说法,线程的上下文切换。 它指,CPU资源的分配采用了时间片轮转,即给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是线程的上下文切换。看个图,可能会更容易理解一点

35.为什么wait(),notify(),notifyAll()在对象中,而不在Thread类中

锁只是个一个标记,存在对象头里面。

下面从面向对象和观察者模式角度来分析。

  • 面向对象的角度:我们可以把wait和notify直接理解为get和set方法。wait和notify方法都是对对象的锁进行操作,那么自然这些方法应该属于对象。举例来说,门对象上有锁属性,开锁和关锁的方法应该属于门对象,而不应该属于人对象。
  • 从观察者模式的角度:对象是被观察者,线程是观察者。被观察者的状态如果发生变化,理应有被观察者去轮询通知观察者,否则的话,观察者怎么知道notify方法应该在哪个时刻调用?n个观察者的notify又如何做到同时调用?
  • 来源:知乎 www.zhihu.com/question/32…

36. 线程池中 submit()和 execute()方法有什么区别?

  • execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
  • execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
  • execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。

37 AtomicInteger 的原理?

AtomicInteger的底层,是基于CAS实现的。我们可以看下AtomicInteger的添加方法。如下

public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    通过Unsafe类的实例来进行添加操作
    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));//使用了CAS算法实现

        return var5;
    }
复制代码

注意:compareAndSwapInt是一个native方法哈,它是基于CAS来操作int类型的变量。并且,其它的原子操作类基本也大同小异。

38 Java中用到的线程调度算法是什么?

我们知道有两种调度模型:分时调度和抢占式调度。

  • 分时调度模型:让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的 CPU 的时间片。
  • 抢占式调度:优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

Java默认的线程调度算法是抢占式。即线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

39. shutdown() 和 shutdownNow()的区别

  • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
  • shutdown()只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。

40 说说几种常见的线程池及使用场景?

几种常用线程池:

  • newFixedThreadPool (固定数目线程的线程池)
  • newCachedThreadPool(可缓存线程的线程池)
  • newSingleThreadExecutor(单线程的线程池)
  • newScheduledThreadPool(定时及周期执行的线程池)

40.1 newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
复制代码
  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即keepAliveTime为0
  • 阻塞队列为无界队列LinkedBlockingQueue

使用场景

FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

40.2 newCachedThreadPool

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
复制代码
  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

使用场景

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

40.3 newSingleThreadExecutor 单线程的线程池

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
复制代码
  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是LinkedBlockingQueue
  • keepAliveTime为0

使用场景

适用于串行执行任务的场景,一个任务一个任务地执行。

40.4 newScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
复制代码
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

使用场景

周期性执行任务的场景,需要限制线程数量的场景

41 什么是FutureTask

FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,可以把它理解为是可以返回结果的Runnable。

使用FutureTask的优点:

  • 可以获取线程执行后的返回结果;
  • 提供了超时控制功能。

它实现了Runnable接口和Future接口,底层基于生产者消费者模式实现。

FutureTask用于在异步操作场景中,FutureTask作为生产者(执行FutureTask的线程)和消费者(获取FutureTask结果的线程)的桥梁,如果生产者先生产出了数据,那么消费者get时能会直接拿到结果;如果生产者还未产生数据,那么get时会一直阻塞或者超时阻塞,一直到生产者产生数据唤醒阻塞的消费者为止。

42 java中interrupt(),interrupted()和isInterrupted()的区别

  • interrupt 它是真正触发中断的方法。
  • interrupted是Thread中的一个类方法,它也调用了isInterrupted(true)方法,不过它传递的参数是true,表示将会清除中断标志位。
  • isInterrupted是Thread类中的一个实例方法,可以判断实例线程是否被中断。。
public voidinterrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
    
   public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

    public boolean isInterrupted() {
        return isInterrupted(false);
    }
复制代码

43 有三个线程T1,T2,T3,怎么确保它们按顺序执行

可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是:A等待B线程执行完毕后(释放CPU执行权),在继续执行。

代码如下:

public class ThreadTest {

    public static void main(String[] args) {

        Thread spring = new Thread(new SeasonThreadTask("春天"));
        Thread summer = new Thread(new SeasonThreadTask("夏天"));
        Thread autumn = new Thread(new SeasonThreadTask("秋天"));

        try
        {
            //春天线程先启动
            spring.start();
            //主线程等待线程spring执行完,再往下执行
            spring.join();
            //夏天线程再启动
            summer.start();
            //主线程等待线程summer执行完,再往下执行
            summer.join();
            //秋天线程最后启动
            autumn.start();
            //主线程等待线程autumn执行完,再往下执行
            autumn.join();
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

class SeasonThreadTask implements Runnable{

    private String name;

    public SeasonThreadTask(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 1; i <4; i++) {
            System.out.println(this.name + "来了: " + i + "次");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

春天来了: 1次
春天来了: 2次
春天来了: 3次
夏天来了: 1次
夏天来了: 2次
夏天来了: 3次
秋天来了: 1次
秋天来了: 2次
秋天来了: 3次
复制代码

44 有哪些阻塞队列

  • ArrayBlockingQueue 一个由数组构成的有界阻塞队列
  • LinkedBlockingQueue 一个由链表构成的有界阻塞队列
  • PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列
  • DelayQueue 一个使用优先队列实现的无界阻塞队列。
  • SynchroniouQueue 一个不储存元素的阻塞队列
  • LinkedTransferQueue 一个由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列

45 Java中ConcurrentHashMap的并发度是什么?

并发度就是segment的个数,通常是2的N次方。默认是16

46 Java线程有哪些常用的调度方法?

46.1 线程休眠

Thread.sleep(long)方法,使线程转到超时等待阻塞(TIMED_WAITING) 状态。long参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为就绪(Runnable)状态。

46.2 线程中断

interrupt()表示中断线程。需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。可以用isInterrupted()来获取状态。

46.3 线程等待

Object类中的wait()方法,会导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()唤醒方法。

46.4 线程让步

Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

46.5 线程通知

Object的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。

notifyAll(),则是唤醒在此对象监视器上等待的所有线程。

47. ReentrantLock的加锁原理

ReentrantLock,是可重入锁,是JDK5中添加在并发包下的一个高性能的工具。它支持同一个线程在未释放锁的情况下重复获取锁。

47.1 ReentrantLock使用的模板

我们先来看下是ReentrantLock使用的模板:

//实例化对象
    ReentrantLock lock = new ReentrantLock();
    //获取锁操作
    lock.lock();
    try {
        // 执行业务代码逻辑
    } catch (Exception ex) {
        //异常处理
    } finally {
        // 解锁操作
        lock.unlock();
    }
复制代码

47.2 什么是非公平锁,什么是公平锁?

ReentrantLock无参构造函数,默认创建的是非公平锁,如下:

public ReentrantLock() {
    sync = new NonfairSync();
}
复制代码

而通过fair参数指定使用公平锁(FairSync)还是非公平锁(NonfairSync)

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

什么是公平锁?

  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

什么是非公平锁?

  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

47.3 lock()加锁流程

大家可以结合AQS + 公平锁/非公平锁 + CAS去讲ReentrantLock的原理哈。

48. 线程间的通讯方式

48.1 volatile和synchronized关键字

  • volatile关键字用来修饰共享变量,保证了共享变量的可见性,任何线程需要读取时都要到内存中读取(确保获得最新值)。
  • synchronized关键字确保只能同时有一个线程访问方法或者变量,保证了线程访问的可见性和排他性。

48.2 等待/通知机制

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。

48.3 管道输入/输出流

  • 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要 用于线程之间的数据传输,而传输的媒介为内存。
  • 管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。

48.4 join()方法

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时 时间里没有终止,那么将会从该超时方法中返回。

48.5 ThreadLocal

ThreadLocal,即线程本地变量(每个线程都有自己唯一的一个哦),是一个以ThreadLocal对象为键、任意对象为值的存储结构。底层是一个ThreadLocalMap来存储信息,key是弱引用,value是强引用,所以使用完毕后要及时清理(尤其使用线程池时)。

大家可以看下我之前这篇文章,ThreadLocal的八个关键知识点

49 写出3条你遵循的多线程最佳实践

  • 多用同步类,少用wait,notify
  • 少用锁,应当缩小同步范围
  • 给线程一个自己的名字
  • 多用并发集合少用同步集合

50. 为什么阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建?

这是因为,JDK开发者提供了线程池的实现类都是有坑的,如newFixedThreadPool和newCachedThreadPool都有内存泄漏的坑。大家可以看下我之前这篇文章哈

细数线程池的10个坑

  • github地址,麻烦给个star鼓励一下,感谢感谢
  • 公众号:捡田螺的小男孩(欢迎关注,干货多多)

参考与感谢

来源:https://juejin.cn/post/7134480865089814565
并发编程面试八股文背诵版 大家好,我是the—shy ,今天为大家分享一篇高质量的并发编程高频面试题,大概八十道题,两万多字,每道题都是亲自整理,绝非网上随意复制粘贴,应该是目前网上质量比较高的并发编程面试题目合集。如果觉得有用,记得点赞收藏 然后转发给身边的小伙伴鸭 推荐阅读: (1条消息) MySQL高频面试题(收藏版)_the shy-优快云博客 (1条消息) Java多线程连环50问(八股文背诵版)_the shy-优快云博客 互联网大厂最爱问的智力题总结(带详细答案)_the shy-优快云博客 文章目录. 阅读详情

相关推荐

手把手教你用示波器搞定PCIe 5.0 REFCLK时钟信号测量(附完整参数解读)

本文详细介绍了如何使用示波器进行PCIe 5.0 REFCLK时钟信号测量,包括示波器与探头的选择、关键参数的测量方法及解读,以及实战中的常见问题与解决方案。通过手把手教学和完整参数解读,帮助硬件工程师高效完成时钟信号测量,确保系统稳定性。

weixin_28704565的博客 359

【建议收藏】对线面试官:多线程硬核50问

选择多线程的原因,就是因为快。举个例子: > 如果要把1000块砖搬到楼顶,假设到楼顶有几个电梯,你觉得用一个电梯搬运快,还是同时用几个电梯同时搬运快呢?这个电梯就可以理解为线程。

weixin_57907028的博客 972

用MATLAB从零实现六足机器人步态仿真:交替三角与波动步态保姆级教程

本文详细介绍了如何使用MATLAB从零实现六足机器人的两种经典步态仿真:交替三角支撑步态和波动步态。通过参数定义、相位设置、运动轨迹生成和三维可视化等步骤,帮助读者掌握六足机器人步态规划的核心技术。教程包含完整代码和详细解析,适合机器人爱好者和相关专业学生学习实践。

weixin_26765363的博客 226

JAVA高并发多线程必须懂的50个问题

http://www.importnew.com/12773.html ImportNew 首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源 Java线程面试题 Top 50 2014/08/21 | 分类: 基础技术 | 27 条评论 | 标签: 多线程, 面试题 分享到: 692 本文由 ImportNew - 李 广 翻译自 javarevisited。欢迎加入翻译小

u011163372的博客 5万+

FPGA实战:手把手教你实现VESA DSC编码(附Verilog代码解析)

本文详细介绍了如何使用FPGA实现VESA DSC编码器,包括核心架构设计、Verilog代码实现与优化技巧。通过流水线设计、色彩空间转换和预测编码等关键技术,帮助开发者掌握工业级DSC编码器的开发流程,并提供实用的调试与性能优化方法。

weixin_29165725的博客 403

MATLAB 设置legend加标题

x1 = linspace(0,5); y1 = sin(x1/2); plot(x1,y1) hold on x2 = [0 1 2 3 4 5]; y2 = [0.2 0.3 0.6 1 0.7 0.6]; scatter(x2,y2,'filled') hold off legend({'sin(x/2)','2016'},'Location','northwest','Orientation','horizontal')%水平 legend添加标题 lgd = legend; lgd.Fon

早起CaiCai的学习空间 4713

Java并发多线程面试题 Top 50

不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,所以线程相关的问题在面试中经常会被提到。 在典型的Java面试中, 面试官会从线程的基本概念问起, 如:为什么你需要使用线程, 如何创建线程,用

Thancks 2万+

5-EP4CE10F17C8-引脚配置

EP4CE10F17C8-引脚配置

weixin_44479192的博客 719

两万字!多线程硬核50问!

}}@OverrideSystem.out.println(":捡田螺的小男孩");}}}}@OverrideSystem.out.println("捡田螺的小男孩");}}//运行结果:捡田螺的小男孩如果想要执行的线程有返回,可以使用Callable。}}@Overridereturn "捡田螺的小男孩";}}//运行结果:捡田螺的小男孩。

java_lujj的博客 272

零基础吃透!Python爬虫实战:超详细爬取小红书图文,解析封面+标题+文案全流程

本文为零基础开发者提供小红书图文爬取全流程指南,针对动态加载、严格反爬等核心痛点,从环境配置到数据解析详细讲解。主要内容包括: 核心特性分析:动态加载机制需抓包获取真实接口,严格反爬依赖Cookie验证和请求头伪装 环境准备:Python 3.8-3.10环境,使用requests/beautifulsoup4等库,通过浏览器开发者工具获取有效Cookie 实战步骤: 定位关键API接口(note/list) 配置请求头(含Cookie/User-Agent/Referer) 实现分页爬取和异常处理 解析封

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 729

FPGA 学习笔记:Vitis IDE launch failed 的解决方法

FPGA 学习笔记:Vitis IDE launch failed 的解决方法

坚韧与专注 1万+

两万字!多线程硬核50问!详解!面试必过

在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。

weixin_47428270的博客 272

泛微OA提交流程到下一个节点

泛微OA后端接口强制流程归档

CXYCMY的博客 2245

两万字!多线程硬核50问!备战金九银十

}@OverrideSystem.out.println("关注公众号:捡田螺的小男孩");}}}}@OverrideSystem.out.println("关注公众号:捡田螺的小男孩");}}//运行结果:关注公众号:捡田螺的小男孩如果想要执行的线程有返回,可以使用Callable。}}@Overridereturn "关注公众号:捡田螺的小男孩";}}//运行结果:关注公众号:捡田螺的小男孩。

weixin_70730532的博客 213

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

肝爆两万字!多线程硬核50问!看看你都能答上来吗

}}@OverrideSystem.out.println("关注公众号:捡田螺的小男孩");}}}}@OverrideSystem.out.println("关注公众号:捡田螺的小男孩");}}//运行结果:关注公众号:捡田螺的小男孩如果想要执行的线程有返回,可以使用Callable。}}@Overridereturn "关注公众号:捡田螺的小男孩";}}//运行结果:关注公众号:捡田螺的小男孩。

msbz7的博客 253

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

两万字 多线程50问 看你能抵住多少?

们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。 当然,有利就有弊,多线程场景下,我们要保证线程安全,就需要考虑加锁。加锁如果不恰当,就很很耗性能。

Trouvailless的博客 208

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

多线程面试题

选择多线程的原因,就是因为快。举个例子:所以,我们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。当然,有利就有弊,多线程场景下,我们要保证线程安全,就需要考虑加锁。加锁如果不恰当,就很很耗性能。Java中创建线程主要有以下这几种方式: 2.2 定义Runnable接口的实现类,并重写该接口的run()方法 2.3 定义Callable接口的实现类,并重写该接口的call()方法 如果想要执行的线程有

爱是与世界平行 865

Java多线程百万数据快速入库实战

出自: 腾讯课堂 700多分钟干货实战Java多线程高并发高性能实战全集 , 我学习完了之后,我做了个笔记. 背景 某应用程序(单台服务器,非分布式的多台服务器),这单台服务器就是你的笔记本电脑了, ​ 并发产生100万条数据,这100w条数据是你自己产生的,假设你是架构师,如何运用多线程等基础知识将这100万条数据,快速同步(4分钟以内)到MySQL数据库? ​ 分析百万数据快速入库的特点 1.百万数据快速入库的特点: 数据量比较大(高并发),时间很短(性能), 100万条数据如果一条一条的插入到

张俊杰 的博客 1万+

2025版【Java并发编程面试题】全收录(两万字含答案解析)

多线程会共同使用一组计算机上的 CPU,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU。不同的线程切换使用 CPU发生的切换数据等就是上下文切换。ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。为什么不推荐使用?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。

mxt51220的博客 1096

97.5%准确率的深度学习中文分词(字嵌入+Bi-LSTM+CRF)

http://www.17bigdata.com/97-5%E5%87%86%E7%A1%AE%E7%8E%87%E7%9A%84%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D%EF%BC%88%E5%AD%97%E5%B5%8C%E5%85%A5bi-lstmcrf%EF%BC%89.html 摘要...

weixin_34203426的博客 778

避开这些坑!用Pandas处理2万条基金数据时的3个性能优化技巧

本文针对使用Pandas处理上万条基金数据时遇到的性能瓶颈,分享了三个核心优化技巧:通过优化数据类型显著降低内存占用,利用向量化计算与批量请求提升处理效率,以及构建健壮的API调用与缓存机制确保程序稳定运行。这些方法尤其适用于LOF基金套利等需要高效数据处理的金融分析场景,能将处理时间从十几秒压缩至一两秒。

ooo22的博客 147

项目是如何死掉的?太过真实!

点击上方“码农突围”,马上关注,每天上午8:50准时推送真爱,请置顶或星标来源:PM圈子作者:正牌老李飞刀做项目多年,见过成功的项目,也见过太多失败的项目;成功或失败,都...

欢迎关注公众号:【码农突围】,公号后台回复9999,可以获取一份500页的LeetCode刷题笔记。 639

Java 百万数据秒级导出到Excel中

出自: 腾讯课堂 700多分钟干货实战Java多线程高并发高性能实战全集 , 我学习完了之后,我做了个笔记. 1.案例背景 某个MySQL服务器的user表有100万条数据,请运用多线程并发编程等相关基础知识,将这100万条数据秒级(8秒内)导出到若干Excel文件. 实际需求如果产品经理说要弄到一个Excel文件的话,你可以跟产品经理要提建议的,因为你100万条数据往一个Excel里面保存的话那么你的文件会很大,而且看数据也不方便,也会很卡. 如果你把一百万条数据导出到多个Excel里面,一个Excel里

张俊杰 的博客 8734

重磅开源!吴恩达新书《Machine Learning Yearning》完整中文版

吴恩达新书《Machine Learning Yearning》完整中文版(开源)《Machine Learning Yearning》是吴恩达历时两年,根据自己多年实践...

欢迎关注公众号:【码农突围】,公号后台回复9999,可以获取一份500页的LeetCode刷题笔记。 2203

每日一面冲击BAT--Java 面试题3

一一面试宇宙计划: MyBatis面试题 每天一个面试技巧–MyBatis面试题1 每天一个面试技巧–MyBatis面试题2 每天一个面试技巧–MyBatis面试题终极 ZooKeeper 面试题 每日一面冲击BAT–ZooKeeper 面试题1 每日一面冲击BAT–ZooKeeper 面试题2 每日一面冲击BAT–ZooKeeper 面试题终极 Dubbo 面试题 每日一面冲击BAT–Dubbo 面试题1 每日一面冲击BAT–Dubbo 面试题终极 Elasticsearch 面试题 每日一面冲击BAT–

代码从一开始的博客 379

程序员段子:有哪些话一听就知道对方很水!

上一篇:多次浏览色情网站会被抓起来吗?01之前和前公司某个技术总监讨论方案。我:这个东西我们可以用(http)get来获取。总监:不行啊,这样不安全。我:为什么不安全?总监:别人可以用set来修改服务器的数据啊。我:???我:没事,咱给set加上private02一开口就说自己做了几千万用户量的产品,一说技术全是各种热门词语。一谈项目细节却说完全都忘记了;一问事故责任没有思考就说和我这边没关系。一...

Java和Android架构 143
上一篇: 三万字+八十图,详解Redis五十二问!太全面了
下一篇: 【微服务】:何为微服务、网关、服务发现/注册?
码趣资源网
博客等级 码龄4年 268粉丝 518原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值