线程基础

1问:线程的状态是怎样的?


(相关资料图)

1答

2问:多线程一定比单线程快吗?

2答:不一定,因为线程切换是有开销的,需要消耗性能。若CPU是单核,那你开多个线程可能会使程序变慢,而单线程则会很快。

3问:多线程的优缺点有哪些?

3答

优点

资源利用率更好

比如:下载文件。我们的流程是这样的:

将下载任务放到队列。

从队列里取出下载链接去下载。

若是单线程的话,那费老劲了,一个一个的下载,CPU大部分时间是空闲的,若是多线程呢?同时下载一批任务,岂不是更爽快?CPU忙起来吧!

提高系统的吞吐率

多线程编程使得一个进程中可以有多个并发(即同时进行)的操作。例如,当一个线程因为I/O操作而处于等待时,其他线程然可以执行其操作。

响应速度快

还以下载文件的案例,若我们请求一个下载接口,要等下载完才返回成功,那岂不是需要等太久了,如果我们业务逻辑都没问题直接返回成功岂不是更好?然后下载任务交由其他线程去处理。

缺点

线程切换是有开销的,这会在一定情况下导致程序运行变慢。

多线程程序必须非常小心地同步代码,否则会引起死锁或数据不准确。

多线程程序极难调试,并且一些bug非常隐蔽,可能你99次都是对的,但是有1次是错的,不像单线程程序那么容易暴露问题。

4问:线程和进程的区别?

4答

进程是资源分配的基本单位。

线程是处理器(CPU)调度的基本单位。

进程是操作系统级别的,线程是进程级别的。

一个进程包含多个线程。(我们可以打开一个IDEA/Eclipse,然后JConsole去看线程数,会发现一IDEA/Eclipse进程启动了N多个线程。

5问:用Runnable还是Thread?

5答

这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你实现多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。也是大家一直所说的:面向接口编程。

6答:Thread 类中的start()和 run()方法有什么区别?

6答:真正启动线程的是start()方法而不是run(),run()和普通的成员方法一样,可以重复使用,但不能启动一个新线程。start()方法才会启动新线程。

7问:sleep()和wait()的区别?

7答

sleep为Thread的方法,而wait为Object的方法。

最大本质的区别是:sleep不释放锁,wait释放锁。

用法上的不同:sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用interreput()来终止线程;wait()可以用notify()/notifyAll()直接唤起。

wait在使用前必须要获取锁(synchronized块包起来),而sleep可以在任何地方使用。

8问:阻塞与等待的区别?

8答

阻塞:当一个线程试图获取对象锁(非java.util.concurrent库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程来唤醒自己,不响应中断

等待:当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语义更丰富,可响应中断。例如调用:Object.wait()、Thread.join()以及等待Lock或Condition。

9问:创建线程的方式有哪些?

9答

继承Thread类

实现Runnable接口

匿名内部类(直接Thread/实现Runnable接口 )

带返回值的方式(FutureTask)

线程池的方式

10问:为什么wait和notify方法要在同步块中调用?

10答:因为只有走进同步块,才说明该线程有对资源的持有权,而只要对资源有持有权,才有资格去进行释放锁和通知其他没有获取到锁的线程。否则就无法保证代码的原子性。

11问:什么是同步锁和死锁?

11答

同步锁

当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用synchronized 关键字来取得一个对象的同步锁。

死锁

何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

Volatile

12问:简单聊聊volatile?

12答

被volatile修饰的共享变量,就具有了以下两点特性:

保证了多线程对该变量操作的内存可见性

禁止指令重排序

但是并不保证原子性。

13问:什么是可见性?

13答:每个线程都有独立的工作内存,所以线程对变量进行修改的时候会先将值修改到工作内存,其他线程是不可见的,可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。加上volatile可以保证可见性,volatile变量保证修改的新值能够立马同步到主存,其他线程使用时也会感知到这个共享变量已经有新值了,让自己的工作内存值失效,立即从主存重新获取 ,这就保证了多线程操作时变量的可见性。

14问:指令重排是什么意思?

14答

JVM会对编译后的class进行优化,重排序也是JVM优化的手段之一,也就是A、B两行代码的顺序编译后可能变成B、A,这就是重新排序了。有时候可能会造成一些问题,比如状态标记量、双重检查锁的单例写法。

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

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

问题的根源如下:

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

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

15问:volatile怎么保证原子性的?

15答:这是个坑,因为volatile无法保证原子性。

16问:volatile和synchronized有啥区别?

16答

volatile仅仅作用在变量上,synchronized可以作用在变量上也可以方法上,还支持代码块。

volatile能保证可见性和防止重排序,不能保证原子性。synchronized能保证原子性和可见性和防止指令重排序。

volatile不会造成线程阻塞,synchronized会造成线程阻塞。

其实全是围绕这句话来说的:volatile不是锁,synchronized是把可重入排他锁。

17问:volatile用在什么地方?

17答

线程安全的单例:双重锁检查无法保证线程安全性,需要volatile防止重排序来保证。

状态标记量:一个状共享态变量需要多个线程读写的时候,可以用volatile保证可见性。

18问:volatile是如何保证可见性和指令重排的(它的实现原理)?

18答

加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,这个内存屏障包含如下三个功能:

它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

它会强制将对缓存的修改操作立即写入主存。

如果是写操作,它会导致其他CPU中对应的缓存行失效。

关键词: