多线程面试问题解析

回首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:等待的点指的是起点

CountDownLatch

CyclicBarrier

不同点比较

6. volatile关键字

太重要了只能说。我简单列举一下里面的知识点:

(1)、Java内存模型:主内存和每个Java的工作内存

Java内存模型 Java内存模型2

(2)、原子性,可见性,顺序性。这三个特性是多线程里面的重要特性。而volatile可以保证可见性和顺序性。但是不能保证原子性。

如何保证可见性和顺序性的

(3)、如何进一步保证原子性?可以和CAS(Compare And Swap)结合进一步保证原子性,这样也就实现了一个轻量级的无锁结构,主要借助的操作系统层面的指令来保证多线程下的数据安全。

CAS知识 Automic类

7. 线程之间如何共享数据

这也是比较重要的一个问题,可以看出你的知识面或者你对多线程的理解程度。或者也可以理解为生产者消费者的实现。一般有以下几种方式:

(1)、wait/notify 

wait和notify是Object内的方法,注意它们的使用需要获取对象锁

(2)、await/signal

和wait/notify基本一致,不过它是在J.U.C中使用

await/signal分析

(3)、BlockingQueue阻塞队列

linkedBlockQueue

8. sleep和wait的区别

它们都可以放弃CPU一定的时间,但是wait会释放锁,但是sleep不会释放锁。这里涉及到对象锁的问题。

wait/notify分析 sleep分析

9. ThreadLocal的分析

ThreadLocal是解决多线程数据问题的一种独特的方式,以空间换时间,每一个Thread内维护一个Map,将数据进行隔离,就不存在线程安全的问题。

ThreadLocal

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。

synchronized

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做的扩展。它本质上是一个双向队列,里面存放的是因为获取锁失败而进入等待的线程。

AQS

24. Semaphore

信号量,即0和1,它的作用比较有意思,如果你想控制某段代码的并发量,或者限制某段代码最多能有多少线程访问,可以使用它。

Semaphore

25. join的问题

让线程以自己想要的顺序来执行,但是你知道内部是怎么保证这样一个顺序的么?其实是获取线程对象的对象锁,基于此加锁后释放锁时可以进行通知。

join原理分析

后续进行更新....

猜你喜欢

转载自blog.csdn.net/qq_32924343/article/details/80665103