目录
面试总结
Java并发在面试过程中经常会问到,属于必知必会的知识点,有的面试官甚至还会问的比较深入,所以有时间还是建议好好学习一下这方面的知识
问题汇总与答案整理(仅供参考)
1. 实现线程的方式及区别
这个问题从不同的角度来看有不同的答案,例如从有无返回值可以划分为继承Thread类,实现Runnable接口的无返回值类型以及实现Callable的有返回值类型。(这样类似的答案在网上搜有很多,比如通过线程池实现,定时器实现等)。
但是这个问题从Oracle的官方文档上来看是分为两类的,一种是实现Runnable接口,一种就是继承Thread类,但其实一般实现Runnable接口用的比较多,因为这个相比继承整个Thread类开销小,而且Java是单继承的,如果继承了Thread类就不能继承其他类了,但是Java是可以实现多个接口的,所以使用接口也更加的方便。
但通过源码来看,其实他们两个本质上都是一样的,都是通过Thread类的run方法实现的,Thread类的run()方法代码是这样写的:
public void run(){
if(target!=null){
target.run();
}
}
这里的target是属于Runnable类的一个对象,如果是继承Thread类,本质上是重写这个run()方法,而如果实现Runnable接口则是调用这个接口写的run()方法。
最后再总结一下,其实准备的讲创建线程只有一种方法就是构造Thread类,而实现线程的执行单元则是上述两种方式。
PS:这个问题我相信这么回答一定会比网上搜到的很多答案回答起来会加分不少
2. 线程的启动
2.1 为什么要用start()方法启动线程而不用run()启动
因为如果直接用run()方法来启动,还是运行在main()线程中,如果用start()方法启动,则会调用Java的native方法从而使得底层的线程调度器创建一个新的线程
2.2 一个线程两次调用start()方法会发生什么
会抛出一个非法的线程状态异常,因为在start方法里,首先会有对线程状态的判断,代码为
if(threadStatus!=0){
throw new IllegalThreadStateException();
}
这里的threadStatus的变量初始值为0,如果一个线程调用start()方法后,其就不为0了,这时候在调用就会start()方法就会抛出异常了
3. 线程的停止
目前Java中停止线程的方法主要是通过Interrupt的中断操作来进行停止的,但这需要请求方与被停止方的相互配合,在Java以前的版本中使用stop()方法或者suspend()方法来停止线程,但是这个方法已经被弃用了,因为这个方法是线程不安全的,使用该方法来停止线程会立刻停止run()方法中的剩余全部工作,比如银行存款的时候,要存好几笔款,突然停了,这就会造成数据得不到同步,不一致等问题。还有一个方法是通过volatile的boolean标志位结合while循环来停止线程,因为volatile可以使得这个标志位在多个线程之间是可见的,但是如果程序发生阻塞,一直在这个个循环中则无法判断标志位,就会导致无法停止线程
4. 线程的状态转换
线程总共分为六个状态:
- New 新建状态
- Runnable 可运行状态
- Blocked 被阻塞状态
- Waiting 等待状态
- Timed Waiting 超时等待状态
- Terminatd 终止状态
状态转换关系如下,图源Java线程状态转换图:
5. 线程安全的定义
《Java Concurrency In Practice》的作者对线程安全的定义为:当多个线程访问一个对象时,如果不用考虑这些线程运行时的调度,也不需要进行额外的同步操作,调用这个对象的行为都可以获得一个正确的结果,那么这个线程就是安全的
6. wait()/notify()与sleep()
6.1 wait()/notify()与sleep()的异同
相同点:
- 都会使线程进入阻塞状态
- 都能够响应中断
不同点:
- wait()/notify()要用在同步方法中,不然可能造成两个线程互相等待的情况
- wait()方法会释放锁,而sleep()方法则不会
- wait()/notify()方法属于Object类而sleep()方法属于Thread类
6.2 为什么线程通信的方法wait()/notify()定义在Object类,而sleep()定义在Thread类
因为在Java中锁是对象级的而不是线程级的,每个对象都有个锁,而线程可以通过获得这些对象来获得这些锁,如果定义在Thread类就会导致获取锁不明确,也无法同时获取多个锁,所以wait()方法是定义在Object类的,而sleep是指线程沉睡多长时间是线程级别的,并且其也不会释放锁,只是让线程在预期的时间内去执行
7. 线程池
7.1 为什么要用线程池
其实类似相关的池化技术都可以用下面的句子来回答
- 降低资源消耗:利用线程池可以重复利用已经创建的线程,从而避免了线程频繁创建和销毁带来的开销
- 提高响应速度:利用线程池技术,线程不需要时间再进行创建,任务到来就能立即执行
- 提高管理性:利用线程池可以进行统一的分配,调优和监控
7.2 创建线程池的7个参数
阿里巴巴Java开发手册建议大家使用ThreadPoolExecutor的方式来创建线程池,ThreadPoolExecutor有7个参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler){
…
}
下面分别介绍一下括号中7个参数的含义:
- corePoolSize: 线程池核心线程大小,线程池中维护的最小运行线程数量
- maximumPoolSize: 线程池最大线程数量,一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量(maximumPoolSize)的限制
- keepAliveTime: 如果一个线程处于空闲状态,且当前线程池中的线程数量大于corePoolSize,那么在指定时间keepAliveTime之后会被销毁
- unit: keepAliveTime的时间单位
- workQueue: 工作队列,新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务,jdk中提供了四种工作队列:
- ArrayBlockingQueue,LinkedBlockingQuene,SynchronousQuene,PriorityBlockingQueue
- threadFactory: 线程工厂,创建新线程的时候会用到,可以用来设定线程名等
- handler: 拒绝策略,当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,就会发生拒绝策略,jdk提供了四种拒绝策略:CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy
更为详细的解答推荐大家参考博客Java线程池七个参数详解
8. volatile关键字和synchronized关键字
8.1 volatile的作用
volatile具有两个作用:
- 可见性:读一个volatile变量之前,需要先使对应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存
- 禁止指令重排序,例如解决单例模式双重锁乱序问题
8.2 synchronized的作用
synchornized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能由一个线程执行,synchronized的使用方式有以下几种:
- 修饰实例方法:其作用的范围是整个方法,作用的对象是调用这个方法的对象
- 修饰静态方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
- 修饰代码块:其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象
8.3 volatile和synchronized的关系
- volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞
- volatile仅能使用在变量级别,synchronized则可以使用在变量、方法
- volatile仅能实现变量修改的可见性,而synchronized则可以保证变量修改的可见性和原子性
- volatile不会造成线程阻塞,synchronized会造成线程阻塞
- 使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域
这个推荐大家一个博客volatile和synchronized的区别
9. synchornized和Reentrantlock的区别
- synchronized 竞争锁时会一直等待,ReentrantLock 可以尝试获取锁,并得到获取结果
- synchronized 获取锁无法设置超时,ReentrantLock 可以设置获取锁的超时时间
- synchronized 无法实现公平锁,ReentrantLock 可以满足公平锁,即先等待先获取到锁
- synchronized要么随机唤醒一个线程要么唤醒全部线程,ReenTrantLock提供了一个Condition类,用来实现分组唤醒需要唤醒的线程们
- synchronized 是 JVM 层面实现的,ReentrantLock 是 JDK 代码层面实现
10. JUC包下面的一些常见类
并发容器类:ConcurrentHashMap,CopyOnWriteArrayList (需要掌握其实现原理)
同步工具类:CountDownLatch,CyclicBarrier,Semaphore (需要掌握其实现原理)
阻塞队列类:ArrayBlockingQueue, LinkedBlockingQueue
原子变量类:AtomicInteger, AutomicBoolean
11. Java并发相关代码
11.1 实现两个线程轮流打印奇偶数
方法一:使用wait/notify轮流唤醒
import java.util.*;
public class PrintOddEven {
public final static Object lock = new Object();
public static int count = 0;
public static void main(String[] args) {
MyRunnable1 r1 = new MyRunnable1();
MyRunnable2 r2 = new MyRunnable2();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
static class MyRunnable1 implements Runnable{
public void run() {
while(count<100) {
synchronized(lock) {
lock.notify();
System.out.println(Thread.currentThread().getName()
+':'+count++);
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class MyRunnable2 implements Runnable{
public void run() {
while(count<100) {
synchronized(lock) {
lock.notify();
System.out.println(Thread.currentThread().getName()
+':'+count++);
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
方法二:使用synchronized抢占锁
public class RunnableStyle{
public final static Object lock = new Object();
public static int count = 0;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while(count<100) {
synchronized(lock){
if(count%2==0)
System.out.println(Thread.currentThread().
getName() + count++);
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(count<100) {
synchronized(lock){
if(count%2==1)
System.out.println(Thread.currentThread().
getName() + count++);
}
}
}
}).start();
}
}
11.2 实现生产者消费者模型(自己书写阻塞队列)
import java.util.ArrayList;
public class ProduceConsumer {
public static void main(String[] args) {
PCStore store = new PCStore();
Producer producer = new Producer(store);
Consumer consumer = new Consumer(store);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
class PCStore{
private ArrayList<Integer> store;
int maxsize = 10;
public PCStore(){
store = new ArrayList<>();
}
public synchronized void put(int i) {
if(store.size()==maxsize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
store.add(i);
System.out.println("仓库放入了:"+ i);
notify();
}
public synchronized void take(int i) {
if(store.size()==0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
store.remove(store.size()-1);
System.out.println("仓库拿走了:"+ i);
notify();
}
}
class Producer implements Runnable{
private PCStore store;
public Producer(PCStore store){
this.store = store;
}
@Override
public void run() {
for(int i=0;i<100;i++) {
store.put(i);
}
}
}
class Consumer implements Runnable{
private PCStore store;
public Consumer(PCStore store){
this.store = store;
}
@Override
public void run() {
for(int i=0;i<100;i++) {
store.take(i);
}
}
}
11.3 单例模式的书写及相关问题
单例模式的写法有八种,推荐大家去看博客都了解一下八种怎么写,一般面试官只会要求写一种线程安全的,在这里我推荐两种线程安全的写法:
单例模式–双重校验法
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
单例模式–枚举法
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
常见问题:
-
为什么需要双重校验?
因为如果只进行上面的if进行校验的话就会在多线程的情况下导致线程不安全,而如果只进行下面的if进行校验的话就会导致性能很差,每次有线程使用的时候都会使得其他线程发生阻塞 -
为什么在创建对象的时候需要volatile?
因为Java在创建对象的时候,其实是分为三步:
1.为 uniqueInstance 分配内存空间
2.初始化 uniqueInstance
3.将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能第三步发生在第二步之前。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 1 执行了 第一步和 第三步,此时线程2调用 getInstance() 后发现 Instance 不为空,因此返回Instance,但此时 Instance 还未被初始化。 -
枚举法的优点?
能够实现线程安全,还能防止反序列化重新创建新的对象,写法也很简单