100%有你不会的Thread必考面试题

author:编程界的小学生

date:2021/05/06

flag:不写垃圾没有营养的文章!

1、什么是进程,什么是线程?

  • 程序是以进程为单位来执行的,所以进程是操作系统对资源分配的基本单位。

比如一个QQ.exe,我打开两个qq程序,这就是两个进程,但是程序只有一个QQ.exe,两个进程的资源彼此隔离,由操作系统来管控。

  • 进程是以线程为单位来执行的,所以线程是进程调度执行的基本单位。多个线程共享同一个进程的资源。

比如一个QQ.exe进程,内部包含N个线程,N个线程共享同一份资源,所有有了多线程安全问题。进程执行其实就是线程被CPU分配到了时间片进行了调度。

2、什么是线程的切换?

一图胜千言

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3OXXh1k1-1620286894538)(images/线程是如何切换的.png)]

3、单核CPU设定为多线程有意义吗?

某些场景下有意义。比如说某个线程是用来等待网络输入或者IO输入的,那么如果没有请求的话他将会一直阻塞在那里等待,那么会白白浪费CPU,所以这时候多线程是有意义的,可以让CPU去执行其他线程,你等待着我先干别的,你有请求后我再回来。

4、线程数是不是越大越好?怎么合理设置?

肯定不是。正常应该通过压测来得到最适合业务场景的数值,但是国外大神的一本书《Java并发编程实战》里给出了计算线程数的公式:

在这里插入图片描述

举个例子:比如我们是2核CPU,想让CPU利用率到100%,且等待时间和计算时间各占50%,也就是1比1。那么如下:

2 * 100% * (1 + 1/1)= 2 * 1 * 2 = 4,所以得出结论设置4个最为合适。

4.1、追问:我怎么知道等待时间和计算时间是多少?

Profiler这种专业的工具分析或者自己记录log,方法前后各打时间,然后进行压测分析w(等待时间)/c(计算时间)的平均值。

4.2、补充

  • 正常情况下不建议CPU利用率设置为100%,最好留出10%-20%的CPU利用率用于处理意外情况。
  • 网上很多其他公式其实都是我上面这个公式的变种。

5、创建线程的方法

  • 继承Thread类
  • 实现Runnable接口
  • 使用Lambda表达式
  • 使用ThreadPool线程池
  • 使用Callable
  • 采取FutureTask

6、线程有哪几种状态?

一图胜千言

在这里插入图片描述

也就是说只有synchronized才会进入blocked阻塞状态。

7、聊聊线程的打断interrupt

并没办法真正的打断一个线程,interrupt只是设置标志位,并不是中断这个线程,需要配合catch(InterruptedException e) {}来处理线程,要么抛出异常中断,要么打log或者干其他事不中断。可以作为停止一个线程的最佳方案!

如下三个api :

  • interrupt

打断某个线程(并非真正打断,而是设置一个标志位,来代表是打断状态了

  • isInterrupted

查询某线程是否被打断过(查询标志位)

  • static interrupted

查询当前线程是否被打断过,并重置打断标志位

8、如何停止一个线程?

比如你上传一个大文件,半天没反应,用户点了取消上传,那么这个取消动作怎么将上传线程给停止掉呢?

  • stop

java.lang.Thread#stop()

不建议使用,因为它太暴力了,不管你当前线程是什么状态,直接就给杀死了,没有善后的工作,比如线程需要设置两个变量,第一个设置成功了,第二个还没来得及设置就给stop了,很容易造成数据一致性问题。

  • suspend/resume

java.lang.Thread#suspend/java.lang.Thread#resume

暂停/恢复。看起来很巧妙,能暂停后干一些事,干完后在resume恢复线程。其实不然。

不建议使用,因为suspend暂停是不会释放锁的,如果你忘记resume,或者resume有问题的话,那这把锁就成为死锁了。

  • volatile

不推荐使用,局限性太强。原理就是利用volatile的内存可见性设置状态位来完成线程的生命周期。

public class TestVolatile {
    private static volatile boolean running = true;
    
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (running) {
                // process something ...
            }
        });
        
        t.start();
    }
    
    public static final void stopThread() {
        running = false;
    }
}
  • interrupt

推荐使用,停止一个线程的最佳方案。

比如如下(或者不手动检查interrupted,而是catch住InterruptedException来停止):

public class TestInterrupt {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (! Thread.interrupted()) {
                // process something ...
            }
            System.out.println("thread t end !");
        });
        
        t.start();
        
        // try .catch
        Thread.sleep(1000);
        
        t.interrupt();
    }
}

9、线程之间怎么进行通讯?

不逐个写代码举例,浪费篇幅,只带入解决方案,具体不知道怎么回事的自行Google。

  • synchronized

多个线程锁同一个对象,先得到锁的先执行,后面的排队等待。也就是同步机制。

有的人说不算通讯,就是同步而已,我看来算是通讯的一种。本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁,谁就可以执行。

  • volatile

利用内存可见性来完成通讯,类似共享内存的方式。

  • wait/notify/notifyAll

等待通知,彼此互相协作来完成线程通信

需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。

  • join

它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。

  • CountDownLatch

CountDownLatch 可以实现 join 相同的功能,但是更加的灵活。

  • CyclicBarrier

它可以等待 N 个线程都达到某个状态后继续运行的效果。

  • 信号量机制Semaphore

多个线程(超过2个)需要相互合作,我们用简单的等待通知机制就不那么方便了。这个时候就可以用到信号量。

  • 管道通信

使用管道多半与I/O流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。

管道是基于“管道流”的通信方式。JDK提供了PipedWriterPipedReaderPipedOutputStreamPipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。

10、聊聊线程可见性?

首先粗糙的来讲就是每个线程都有独立的工作内存,所以线程彼此之间的工作内存是隔离的,所以都从主存上读取变量到工作内存然后各自更改在刷到主存的话是会出现数据不一致的问题的,这就是可见性问题。说的有点粗糙,下面细聊一下。

首先看下CPU的多级缓存:

在这里插入图片描述

一般机箱里面会有很多颗CPU,比如说我们有两颗CPU。每颗CPU里面又分别有两个核。CPU是有多级(三级)缓存的,L1/L2/L3,其中L1和L2属于核,L3属于CPU。操作的时候会逐级去找,先从L1找,L1没有去L2找,L2没有去L3找。从主存读的时候会先存到L3一份,然后存到L2一份,然后存到L1一份。所以我们说的内存可见性其实是CPU这个多级缓存的可见性,每颗CPU都是隔离的,每个核也是隔离的。当程序进来交给CPU处理,CPU交给核处理,核上的L1/L2彼此独立隔离。

10.1、缓存行

想一个问题:比如要从主存读取x变量到多级缓存中,那么他只会读取x这一个变量吗?

不是的,他会按照行去读取,这个行就称为缓存行,行的大小设定为64个字节(byte),每次读取64byte进行缓存到L3/L2/L1。

比如:

我从主存读取long x(long占8byte),不满足64byte,所以他会附带着把变量y也给读取进来,那么多个线程同时读取了x和附带的y到缓存中,其中一个CPU对y进行更改后还需要采取手段通知另一个CPU的缓存,让他也同时跟着更改,这个就是缓存一致性协议。效率低下。

通俗理解成:我线程1对x需要更改,但是不足一行,而x和y正好一行,就把x和y一起缓存到了CPU1,另一个线程需要对y更改,但是y也不足一行,而x和y正好是一行,所以就把x和y一起缓存到了CPU2,那么线程对y更改(CPU2上)的时候需要通过一种手段通知CPU1,CPU1上的缓存行的y的值也需要一起修改,这就效率比较低了。

解决方案:

很简单粗暴,你不是一行64byte吗?那我就给你前后都追加7个long类型的byte,让你不管是从前还是从中间还是从后面数都是64字节,这样就仅仅缓存我这一个变量(100%占满一行),这样就间接提升了效率。比如如下写法:

public class TestCacheLine {
    private long p1, p2, p3, p4, p5, p6, p7;
    public long x = 0L;
    private long p9, p10, p11, p12, p13, p14, p15;
}

真的有人这么写吗?我个人不建议这么写,没什么必要。除非开发一款对性能极其敏感的开源框架,比如大名鼎鼎的Disruptor框架就是采取的此种写法,这也是它号称单机情况下最快的MQ的原因之一。

扩展:

JDK1.8出了个注解@Contended来自动填充缓存行,标记到变量上面就行。这个注解可以防止将来缓存行从64byte变成了128或者其他后你的程序都要一同修改的问题, jdk到时候会一起随同更改的。但是这个注解默认是关闭的,想要打开的话需要添加jvm参数:-XX:RestrictContended

10.2、补充

  • volatile可以保证内存可见性。

  • volatile修饰的引用类型(对象或者数组)只能保证引用的可见性,不能保证引用所指对象内的变量的可见性,也就是对象内变量进行更改的话依然是彼此隔离的,不可见的。

11、聊聊线程有序性?

说简单点就是:

int x = 0;
int y = 1;

这段程序在执行的时候真的会先初始化x在初始化y吗?未必,他有可能先执行int y = 1这段代码,后执行int x = 0这段。

乱序存在的条件,也就是什么时候会乱序?

不影响单线程的最终一致性的情况就可能发生乱序。JVM会自己看换顺序执行的话对结果是否有影响,如果没影响的话可能会调换顺序执行来提升性能。

会造成什么影响?

比如双重检查锁的单例(这个解决方案需要JDK5或更高版本):

public class SafeDoubleCheckedLocking {
    // volatile
    private volatile static Instance instance;

    public static Instance getInstance() {
        // 1
        if (null == instance) {
            // 2
            synchronized (SafeDoubleCheckedLocking.class) {
                //  3
                if (null == instance) {
                    // 4
                    instance = new Instance();
                }
            }
        }
        return instance;
    }
}

若不加volatile的话这就是一个线程不安全的单例写法。在线程执行到第1处,代码读取到instance不为null时,instance引用的对象有可能发生了指令重排给分配了内存空间,但是还没有完成初始化!所以业务系统获取到的instance实例可能是null。

问题的根源如下:

前面的双重检查锁定实例代码的第4处instance = new Instance();创建了一个对象。这一行代码可以分解为如下的3行伪代码。

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

上面3行伪代码中的2和3之间可能会被重排序,2和3之间重排序之后的执行时序如下:

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

更多好文请关注微信公众号:【Java码农社区】

在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【原】编程界的小学生

没有打赏我依然会坚持。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值