回首1年多博客的时间,多线程的占比还是很大的,因为多线程问题是所有程序员在学习道路上的一道坎,最初的时候是害怕它,接触的多,看的多,感觉就没那么怕了。
首先贴一下自己整理的一些并发知识,基于这些我可以说能解决你大部分的多线程面试题了。
0.什么是线程安全
理论问题,每个人理解不一样,我的理解就是:
多线程下执行的结果和单线程下一致,就是线程安全的。
1. 创建线程的方式
(1)、继承Thread类。
(2)、实现Runaable接口。
至于哪一个好,肯定是实现接口的方式好。
2. start()和run()方法的区别
主要的点在于,start()才是启动一个新线程,而run()则只是在当前线程下执行一个普通方法而已。
3. Runnable和Callable区别
一般我们去做多线程时总是直接启动之后就算了,没有关注返回值的情况:
Runnable:run方法没有返回值。只是纯粹执行代码。
Callable:call方法是有返回值的,它可以和Future或者FutureTask配合使用用来获取异步执行的返回值。
5. CyclicBarrier和CountDownLatch区别
主要的区别呢就是所谓的等待的点的不同:
CyclicBarrier:等待的点指的是终点
CountDownLatch:等待的点指的是起点
6. volatile关键字
太重要了只能说。我简单列举一下里面的知识点:
(1)、Java内存模型:主内存和每个Java的工作内存
(2)、原子性,可见性,顺序性。这三个特性是多线程里面的重要特性。而volatile可以保证可见性和顺序性。但是不能保证原子性。
(3)、如何进一步保证原子性?可以和CAS(Compare And Swap)结合进一步保证原子性,这样也就实现了一个轻量级的无锁结构,主要借助的操作系统层面的指令来保证多线程下的数据安全。
7. 线程之间如何共享数据
这也是比较重要的一个问题,可以看出你的知识面或者你对多线程的理解程度。或者也可以理解为生产者消费者的实现。一般有以下几种方式:
(1)、wait/notify
wait和notify是Object内的方法,注意它们的使用需要获取对象锁。
(2)、await/signal
和wait/notify基本一致,不过它是在J.U.C中使用
(3)、BlockingQueue阻塞队列
8. sleep和wait的区别
它们都可以放弃CPU一定的时间,但是wait会释放锁,但是sleep不会释放锁。这里涉及到对象锁的问题。
9. ThreadLocal的分析
ThreadLocal是解决多线程数据问题的一种独特的方式,以空间换时间,每一个Thread内维护一个Map,将数据进行隔离,就不存在线程安全的问题。
10. 为什么wait方法必须在同步块内使用
这个在上面的wait分析中其实已经存在了,wait方法的调用必须要获取对象锁,jdk源码注释中已经强调。
11. wait和notify在释放锁时的不同
wait执行之后,会立即释放当前的锁。而notify方法则只是一个通知的作用,告诉其他线程可以参与到锁的竞争之中,但是锁的获取必须等执行notify的线程执行完剩余代码之后才能真正释放。
12. synchronized和lock的区别
常见面试题,需要从两者的场景和实现去理解分析。这边说下我个人积累的一些观点。
1. synchronized是一个关键字,而Lock是一个类,Lock的话提供了更丰富的接口用于帮助实现线程安全。
2. synchronized在执行完之后自定释放锁,或者在异常发生时也会自动释放锁,无须手动释放。而Lock把释放锁的动作交给了客户端,需要在finally里手动释放,不然会造成死锁。
3. 使用Lock时线程可以相应中断,中断这个概念在学习多线程时是一个重要的操作,在J.U.C中多处使用这个操作。但是synchronized不能相应中断,等待的线程一直等待下去。
4. Lock可以区分出读锁和写锁,针对读和写不同要求的场景设置不同的策略,synchronized不行。
5. Lock可以获取当前锁的信息,synchronized不行。
6. 最后一点锁机制不同,Lock内底层使用的是park方法,但是synchronized是基于对象头,也就是Mark Word。
13. ConcurrentHashMap
它比HashMap多的一层就是线程安全,线程安全的点在于segment的出现,并且锁是加在segment上,segment总共是16个,并发度能达到16。这也是它和HashTable最大的不同,HashTable是在方法层面加的同步,并发性不高。
14. ReadWriteLock
读写锁。区分出了读锁和写锁。读读不会互斥,但是读写,写读和写写才会互斥。
15. 编写一个死锁程序
理论相信大家都知道,编写的话,是不是有人突然就不知道怎么办了?其实很简单,记住一个点,线程A和线程B针对2个资源各自获取到其中一个,再想获取另外一个时会出现死锁。
public static void main(String[] args) throws IOException {
final Object lock1 = new Object();
final Object lock2 = new Object();
new Thread(new Runnable() {
public void run() {
synchronized (lock1){
try{
Thread.sleep(50);
}catch (Exception e){
}
synchronized (lock2){
System.out.println("now lock2");
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
synchronized (lock2){
try{
Thread.sleep(50);
}catch (Exception e){
}
synchronized (lock1){
System.out.println("now lock1");
}
}
}
}).start();
}
16. 怎么唤醒一个线程
针对J.U.C的研读,我们发现可以采用中断这种方式唤醒线程。抛出一个InterruptedException,当然这种方式还是基于线程调用了wait(),sleep()或者join()。对于IO的阻塞,无能为力了。
17. 不可变对象在线程安全中的作用
不可变对象天生具有线程安全,因为它不能被修改,所以针对不可变对象不需要进行额外的同步操作。
18. 线程的状态
比较杂,上面的文章内图可以概括一切。
19. 线程池的工作原理
工作原理内其实就是线程池的几个参数会影响它的工作过程,一个是核心线程数,一个是最大线程数,一个是工作队列。
主要要知道工作队列是无界队列和有界队列的区别(涉及到线程池是否会新建线程来处理任务)。
20. sleep(0)
睡眠多少毫米,此处是0,作用其实是手动触发操作系统进行时间片的分配
21. 自旋
synchronized的做法会导致操作系统进行上下文切换,上下文切换是一个比较耗时且耗资源的操作,所以如果同步的代码很简单,那么或许可以考虑另一种折中的方式,就是自旋,简单来说就是for循环,这样每个线程都不会阻塞。比较经典的问题上面有过,就是CAS。
22. 乐观锁和悲观锁
这个是值Java内的实现。数据库内也是存在乐观锁和悲观锁这个概念的,不去延伸了。
23. AQS
这是一个很深的东西,J.U.C的基础有两个,一个是CAS,一个是AQS。AQS是一个抽象队列同步器,J.U.C下面很多同步类都是基于AQS做的扩展。它本质上是一个双向队列,里面存放的是因为获取锁失败而进入等待的线程。
24. Semaphore
信号量,即0和1,它的作用比较有意思,如果你想控制某段代码的并发量,或者限制某段代码最多能有多少线程访问,可以使用它。
25. join的问题
让线程以自己想要的顺序来执行,但是你知道内部是怎么保证这样一个顺序的么?其实是获取线程对象的对象锁,基于此加锁后释放锁时可以进行通知。
后续进行更新....