多线程面试总结
1.并发三要素:
原子性:指在一次或多次操作中,要么都执行成功,要么都不执行;
可见性:指多线程操作一个共享变量时,
其中一个线程对其修改,其他线程可以立马看到修改的结果;
有序性:程序的执行顺序按照代码的执行顺序来执行;
2.实现可见性有哪些方法
synchronized或者Lock;
保证同一个时刻只有一个线程获取锁执行代码,
锁释放之前把最新的值刷新到主内存,实现可见性。
3.创建线程的有哪些方式?
1.继承thread创建线程:thread本质上是实现了Runnable方法;
使用也非常简单,继承thread方法,
重写run方法,然后new一个线程调用.start方法开启一个线程,
然后它会去调用重写的run方法;
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
2.通过Runnable接口创建线程
如果已经继承了其他类,没发再继承thread类,可以通过实现Runnable接口,重写run方法;
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
3.实现Callable接口重写call()方法,通过FutureTask包装器来创建Thread线程;Callable的call()方法是带有返回值的;当需要线程返回执行结果时,可使用callable接口;
/*
* Callable接口实现多线程Demo
*/
class MyCallable<V> implements Callable<V>
{
@Override
public V call() throws Exception {
// TODO Auto-generated method stub
System.out.println("I am Callable thread : "+Thread.currentThread().getName());
return null;
}
}
public static void main(String[] args) {
/*
* 使用Callable来创建线程
*/
Callable <Integer> aCallable = new MyCallable<Integer>();
FutureTask<Integer> aTask = new FutureTask<Integer>(aCallable);
Thread aThread = new Thread(aTask);
aThread.start();
}
优缺点:
使用thread方法优点是编程简单,但是已经继承thrad不能继承其他父类;
使用callable接口,有点可带有返回结果;
4.通过线程池创建
线程池的优点?
1)重用存在的线程,减少对象创建销毁的开销。
2)可有效的控制最大并发线程数,提高系统资源的使用率,
同时避免过多资源竞争,避免堵塞。
3)提供定时执行、定期执行、单线程、并发数控制等功能;
1.四种常用线程池:
newFixedThreadPool (固定长度的线程池,只需要设置核心线程数)
newCachedThreadPool 可缓存的无边界线程池,空闲时间60秒,就会自动回收;
newScheduledThreadPool 创建定长线程池,能够定时执行任务
newSingleThreadExecutor 单线程,保证按照顺序执行
但是通常是自己ThreadPoolExecutor类创建自定义线程池;
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1.corePoolSize:核心线程池的大小,如果核心线程池有空闲位置,这时新的任务就会被核心线程池新建一个线程执行,当超过时会先将任务存入缓存队列中。
2.maximunPoolSize:最大线程池数量,当缓存队列已满时,
但是当前线程数并没有超过最大线程数,会创建新的线程去执行,但当超过时会使用拒绝处理策略;
3.keepAliveTime:非核心线程能够空闲的最长时间,只对超过核心线程数的部分起作用;
4.unit:时间单位,和keepAliveTime配合使用。
5.workQueue:缓存队列,用来存放等待被执行的任务。
ArrayBlockingQueue;数组结构的有界阻塞队列,先进先出FIFO,不能同时存入和取出
LinkedBlockingQueue;链表结构的无界阻塞队列,先进先出, 可同时存入和取出
SynchronousQueue; 队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任
6.threadFactory:线程工厂,负责生产线程的。
7.handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略,四种策略为
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 ;
如果使用此策略,假设线程满了,会交给上层线程执行,
上层线程需要处理自己的任务,又要处理源源不断的线程,可能导致线上故障;
5.线程的状态流程图
1)新建状态(New):当线程对象创建后,即进入了新建状态,
如:Thread t = new MyThread();
2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),
线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,
随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,
此时线程才得以真正执行,即进入到运行状态。
注:就绪状态是进入到运行状态的唯一入口,也就是说,
线程要想进入运行状态执行,首先必须处于就绪状态中;
4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,
暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,
直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a.等待阻塞:运行状态中的线程执行wait()方法,
使本线程进入到等待阻塞状态;
b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),
它会进入同步阻塞状态;
c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,
线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、
或者I/O处理完毕时,线程重新转入就绪状态。
5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,
该线程结束生命周期。
6.volatile关键字的作用:
参考:https://www.cnblogs.com/zhengbin/p/5654805.html
对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent ,此包大部分基于CAS实现;
7、CAS 的问题
CAS: Compare-And-Swap 比较替换;
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
1)CAS容易造成ABA问题
一个线程a将数值改成了b,接着又改成了a,
此时CAS认为是没有变化,其实是已经变化过了,
而这个问题的解决方案可以使用版本号标识,
每操作一次version加1。
在java5中,已经提供了AtomicStampedReference来解决问题。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,
而不能保证整个代码块的原子性。
比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3)CAS造成CPU利用率增加
之前说过了CAS里面是一个循环判断的过程,
如果线程一直没有获取到状态,cpu资源会一直被占用。
8.什么是乐观锁和悲观锁
乐观锁:乐观认为竞争不总是发生,所以不会进行上锁,
将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量;
当发生冲突时,会尝试重试;
悲观锁:认为竞争时刻发生,所以在操作某资源时一定会上锁,对它进行独占;
类似synchronized;
9.线程B怎么知道线程A修改了变量
volatile修饰变量
synchronized修饰修改变量的方法
10.synchronized、volatile、CAS比较:
synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile是削弱的同步机制,提供多线程共享变量保障可见性和禁止指令重排序优化。
CAS是基于冲突检测的乐观锁(非阻塞)
11.sleep方法和wait方法有什么区别?
1.sleep是thread类的方法,调用sleep会暂停执行,
目的是让出CPU给其他线程机会,但是sleep不会释放锁,
所以就算这条线程休眠了,但是其他线程还是访问不了它的锁对象;
sleep方法不需要唤醒,当到达休眠时间后,自动唤醒;
但是不会马上执行,会进入到可运行状态,等待cpu的调用;
2.wait是object类,执行wait方法会释放锁;
并且需要notify或者notifyAll才能唤醒,并且是随机唤醒;
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,
而sleep可以在任何地方使用
12.Thread类中的start()和run()方法有什么区别?
start是真正开启线程;
run方法直接调用的话,就只是一个普通方法,不会创建新的线程;
13.什么是进程,什么是线程?
进程是资源分配的最小单位,线程是CPU调度的最小单位;
在操作系统层面来解释
进程:在一定的环境下,把静态的程序代码运行起来,通过使用不同的资源,来完成一定的任务。比如说,进程的环境包括环境变量,进程所掌控的资源,有中央处理器,有内存,打开的文件,映射的网络端口等等;
强调进程管理的是资源;
线程:是进程的一部分,作用是去利用中央处理器运行代码;
线程主抓中央处理器执行代码的过程,其余的资源的保护和管理由整个进程去完成。
14.在Java中Executor、ExecutorService?
ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口;
Executor 中的 execute() 方法不返回任何结果,
而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果;
15.Java中notify 和 notifyAll有什么区别?
notify不具有唤醒某个特定线程的能力,
它只能随机唤醒一个线程;竟然锁池等待CPU的调度;
notifyall能够一次性将等待池中的线程移动到锁池去竞争资源,
确保至少有一个能够正常执行;
16.concurrentHashMap为什么是线程安全的?
17.什么是上下文切换?
程序的执行环境等于程序的上下文;
当任务从保存到再加载的过程称为上下文切换;
比如:我们在读英语书,突然一个单词不认识,需求切换到字典书去查下什么意思,这个时候需要保存英语书的页码,等查完再切换回来;
这个过程可以称为是程序的上下文切换;
18.Lock与synchronized的区别?
参考: https://www.cnblogs.com/baizhanshi/p/6419268.html
19.产生死锁的条件?
1.互斥:一个资源一次允许一个资源访问;
2.占有等待:一个资源占有自己的资源,并且还未满足,需要去等待其他线程释放该资源;
3.不可争抢:不能说你去抢资源,别人就释放这个资源;
4.循环等待:你要我的,我要你的,互相争抢,互相等待;