- 1. 进程和线程的区别?
- 2. 什么叫线程安全?
- 3. 线程的几种状态?
- 4. Java创建多线程后,调用start()和run()方法的区别?
- 5. 对Future和FutureTask的理解?
- 6. 怎么解决共享资源竞争,实现线程安全?
- 7. synchronized,volatile和CAS的区别?
- 8. 用volatile修饰的变量和用atomic修饰的变量有什么不同?
- 9. 使用volatile关键字的场景?
- 10. synchronized和Lock的区别?
- 11. 对wait(),notify(),和notifyAll()的理解?
- 12. wait()和sleep()的区别?
- 13. 对yield(),join()的理解?
- 14. Java中什么是竞态条件?
- 15. 定时线程的使用?
- 16. 线程同步常见方法?
- 17. 如何停止一个线程?
- 18. interrupt(),interrupted(),isInterrupted()方法的区别?
- 19. 线程池的优点?
- 20. 实现多线程有哪些方式(线程的创建方式)?
- 21. 什么是乐观锁和悲观锁?
- 22. 什么叫死锁?
- 23. 产生死锁的四个必要条件?
- 24. 导致死锁的原因?
- 25. 守护(后台)线程和非守护线程的区别?
- 26. 什么是多线程的上下文切换?
- 27. 怎么唤醒一个阻塞的线程?
- 28. 线程池中execute()和submit()方法的区别?
- 29. 如果提交任务时,线程池队列已满,这时会发生什么?
- 30. 线程池的类型?
- 31. 线程池的几个重要参数?
- 32. 线程池的阻塞队列有哪些?
- 33. 线程池的拒绝策略有哪些?
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位,一个进程包含多个线程;
- 并发性:多个进程可以并发执行,多个线程也可以并发执行;
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有资源,但可以访问所属进程的资源;
- 系统开销:在创建和撤销进程的时候,都要为其分配和释放资源,所以创建和撤销进程的资源要大于创建和撤销线程的资源;
- 内存空间:进程有独立的地址空间,线程有自己的堆栈和局部变量,但没有独立的地址空间。
如果代码所在进程中有多个线程在同时运行,并且这些线程会同时运行某一段代码,如果每次运行的结果和单线程运行这段代码的结果一致,而且各个变量的值也和预期的一样,那么这段代码就是线程安全的。
3. 线程的几种状态?- 新建状态(New):新创建了一个线程对象;
- 就绪状态(Runnable):线程处于可运行状态,除了CPU之外,其他的运行所需资源都已全部获得;
- 运行状态(Running):就绪状态的线程获得了CPU的使用权,程序代码开始执行;
- 阻塞状态(Blocking):线程因某种原因而放弃CPU,暂时停止运行。阻塞的情况分三种:
- 等待阻塞:调用了wait()方法,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒;
- 同步阻塞:运行的线程所需要的对象的同步锁被其他线程占用;
- 其他阻塞:调用了sleep()或join()方法,或等待某个I/O请求完成,调用yield()方法并不会使线程阻塞,即线程仍处于就绪状态。
- 死亡状态(Dead):线程执行完毕或因异常退出了run()方法,该线程结束生命周期。
调用start()方法后会初始化一个线程,然后线程会被放到等待队列,等待CPU调度,这意味着该线程并不会马上执行,start()方法体中会调用到具体任务的run()方法;
而单单调用run()方法的话只是普普通通地调用Runnable对象的一个方法,并没有经历start()方法所做的线程的准备工作,和普通对象调用普通方法一样。
补充:定义任务用Runnable或Callable接口,区别是前者不产生返回值,后者产生返回值,后者通常结合Future和FutureTask使用,用于异步获取结果,调用ExecutorService对象的submit()方法会产生Future对象,可以用isDone()方法来查询Future是否完成,用get()方法来获取结果;定义线程用Thread。
- Future是一个接口,为Runnable或Callable任务提供:①取消②查询是否取消成功③查询是否执行成功④获取执行结果的功能;
- FutureTask是Future接口的实现类,它既可以作为一个Runnable任务,也可以作为一个Future,它具备Future接口的所有功能,还额外提供了许多其他功能,例如设置结果,处理可能发生的中断等。
Future模式的思想就是在子线程执行的时候,主线程不阻塞仍继续执行,等到主线程需要子线程的执行结果时再去获取子线程的执行结果(此时子线程没有执行完成的话就会阻塞直至完成),主线程还可以根据一定的逻辑对子线程进行操作,例如设置是否需要阻塞等待子线程完成。
- 使用synchronized关键字修饰方法或代码块:只有获得锁的线程才能访问资源;
- 使用显示的Lock对象对代码块进行加锁释放锁:只有获得锁的线程才能访问资源;
- 使用原子类(AtomicInteger,AtomicLong,AtomicReference)来保证原子性:原子操作是不能被线程调度机制中断的操作,一旦开始,那么就会在线程上下文切换之前完成,这样就保证了其他线程来不及修改这个变量,仅适用于简单操作,原子性就是其他线程不能对本线程做出的修改进行干涉,即不能中途插入进来改变变量的值;
- 使用volatile修饰域:只要某个线程对这个域进行了修改,其他的线程都将看到变化;
- 使用线程本地存储(ThreadLocal):根除对变量的共享,为使用相同变量的每个不同的线程都创建不同的存储。
- synchronized保证可见性和原子性,会导致其他线程阻塞,属于悲观锁;
- volatile保证可见性,不保证原子性,禁止指令重排,不需要加锁;
- CAS保证可见性及原子性,不会导致其他线程阻塞,属于乐观锁。
补充:
Java内存模型:Java所有变量都存储在主内存中,所有线程都可以访问;每个线程创建时,JVM会为其创建一个私有的工作内存,里面保存该线程使用到的变量的副本;线程对变量的操作都必须在自己的工作内存中进行,读取时从主内存将变量读取到工作内存中,在工作内存中修改完毕后再刷新到主内存中。
指令重排:是指代码的执行过程有时并不是按照代码书写过程来的,编译器和CPU可以保证在输出结果一致的情况下对代码进行重排序优化,以提高执行效率。
内存屏障:内存屏障是一个CPU指令,可以禁止指令重排,它可以确保屏障前的指令不会排到屏障后面,屏障后的指令不会排到屏障前面,即在内存屏障这条指令执行时,它前面的指令都已执行完毕。
volatile和内存屏障的关系:当用volatile修饰一个变量时,会在对变量进行读操作前插入一个读屏障指令,在对变量进行写操作后插入一个写屏障指令。
volatile为什么不能保证原子性呢:修改volatile变量分为四步,①从主内存中读取变量到工作内存中;②修改变量值;③写回到工作内存;④刷新回主内存。举个例子,一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,在字节码及底层,i++被抽象为三个操作:即先取值,再自加,再赋值三个操作,线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,接下来就不会再读取而是直接对变量进行改动了,所以这个可见性来的有点晚,即线程A不知道变量i已经被线程B修改了还在傻乎乎地延续它自己的修改,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
用volatile修饰的变量不具有原子性,用atomic修饰的变量具有原子性。
9. 使用volatile关键字的场景?- 对变量的写操作不依赖于当前值,比如自增、自减等操作;
- 该变量不宜和其他变量位于同一表达式中。
总而言之,就是需要满足原子性操作的前提。
10. synchronized和Lock的区别?- synchronized是一个关键字,而Lock是一个接口;
- synchronized是隐式的加锁,Lock是显式的加锁;
- synchronized可以作用在方法和代码块上,而Lock只能作用在代码块上;
- synchronized是阻塞式加锁,而Lock中的tryLock()支持非阻塞式加锁;
- synchronized没有超时机制,而Lock中的tryLock()可以支持超时机制;
- synchronized不可中断,而Lock中的lockInterruptibly()可中断的获取锁。(ReentrantLock.lockInterruptibly()允许在等待时由其它线程调用等待线程的Thread.interrupt()方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。ReentrantLock.lock()方法不允许Thread.interrupt()中断,使检测到Thread.isInterrupted()一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程);
- synchronized采用的是monitor对象监视器,Lock的底层原理是AQS;
- synchronized只有一个同步队列和一个等待队列,而Lock有一个同步队列,可以有多个等待队列(同步队列:排队取锁的线程所在的队列;等待队列:调用wait()方法后,线程会从同步队列转移到等待队列);
- synchronized是非公平锁,而Lock可以是公平锁也可以是非公平锁;
- synchronized用notify()方法进行唤醒,而Lock用condition唤醒;
- Lock有ReadWriteLock支持并发读。
- wait()会在等待外部事件产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生时,即发生了某些令人感兴趣的事件时,这个任务才会被唤醒并重新执行;
- wait()方法释放锁,notify()或notifyAll()方法加锁;
- 只能在同步控制方法或同步代码块(即用synchronized修饰)里调用wait(),notify()或notifyAll()方法;
- 在调用wait(),notify()或notifyAll()方法前必须先获得对象的锁;
- notify()唤醒单个线程,但是不能指定具体条件,所以只有一个线程在等待时(即只有一个线程调用了wait()方法)才适用,notifyAll()唤醒所有等待线程(即所有调用了wait()方法的线程)。
- 调用sleep()方法会导致此线程暂停执行指定时间,将CPU时间片让给其他线程,但是它不会释放已获得的对象锁;
- 调用wait()方法会释放对象锁,只有针对此对象调用notify()或notifyAll()方法后线程才会继续执行。
- yield()方法用于暂停正在执行的线程,将CPU的控制权转交给其他线程,本线程进入就绪状态(注意不是阻塞状态),意在合理控制线程的执行流程;
- join()方法可理解为插队,当一个线程需要等待另一个线程的返回结果时,就需要用到该方法,调用该方法的线程的优先级达到最高,其他线程要等待该线程执行完毕。
当多个线程访问同一资源时,如果对线程访问资源的顺序有要求,就称存在竞态条件。
15. 定时线程的使用?- 使用定时器Timer类;
- 使用定时调度线程池ScheduledExecutorService。
sleep(),wait(),notify(),notifyAll(),await(),signal(),signalAll()。
17. 如何停止一个线程?- run()方法或call()方法执行完后线程自动停止;
- 使用interrupt()方法中断线程。
- interrupt()方法用于中断线程,但是仅仅是将线程的中断状态设置为ture,线程是否真的中断需要线程自己时不时的判断这个状态来决定。具体地说,当一个线程调用interrupt()方法时,①如果线程处于阻塞状态(sleep(),wait(),join()等),那么线程将立即退出阻塞状态,并抛出一个InterruptedException异常,仅此而已,中断状态会复位为false;②如果线程处于运行状态或者I/O异常状态或者其他状态,那么中断状态将置为true,仅此而已,不会真的中断线程。也就是说,线程检查到中断标志位为true时,就自行停止线程,处于阻塞状态的线程在检测到InterruptedException异常后,就需要在catch中停止线程。
- interrupted()方法用于返回当前线程的中断状态,并且会清除中断标志(即中断标志复位为false),也就是说,如果一个被中断线程(中断标志为true)调用了该方法,那么返回的结果是true,但是当前线程的中断状态标志被清除为false了,下一次再调用该方法返回的状态结果就是false了。
- isInterrupted()方法用于返回调用该方法的线程对象所对应的线程的中断状态,但是它不会修改中断标志。interrupted()方法和isInterrupted()方法都是用来返回线程的中断状态的,区别就在于作用的线程对象不同,还有是否要清除中断标志状态位。
补充:当抛出InterruptedException异常或者任务调用了interrupted()方法时,中断状态将被复位,其他的都不会修改中断状态。
- 重用存在的线程,减少开销;
- 可有效地控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免阻塞;
- 可提供定时执行,并发数控制等功能。
- 实现Runnable接口,重写run()方法;
- 实现Callable接口,重写call()方法;
- 继承Thread类,重写run()方法,Thread实现了Runnable接口,因此含有run()方法;
- 使用线程池。
- 乐观锁:就像它的名字一样,对并发间操作产生的线程安全问题持乐观状态,它认为竞争不总是会发生,因此它不需要持有锁,将比较-替换两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,则进行相应的重试机制;
- 悲观锁:还是像它的名字一样,对并发间操作产生的线程安全问题持悲观状态,它认为竞争总是会发生,因此每次对某资源进行操作时,总是会对资源进行加锁,除非它释放锁,不然别的线程只有一直等待。
某个线程在等待另一个线程,而后者又等待别的线程,这样一直下去,直到这个链条上的线程又在等待第一个线程释放锁,这就造成了各个线程之间相互等待无法释放锁,导致线程无法继续执行下去,这被称为死锁。
23. 产生死锁的四个必要条件?- 互斥条件:线程使用的资源中至少有一个是不能共享的;
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:资源不能被线程强行抢占;
- 循环等待条件:若干线程之间形成一种头尾相连的循环等待资源关系。
同时满足以上四个条件时,就会产生死锁。
24. 导致死锁的原因?- 资源竞争不当
- 线程获取锁的顺序不当
守护线程是指程序运行的时候在后台提供一种通用服务的线程。当所有的非守护线程结束时,程序也就终止了,也就是JVM停止运行了,同时会杀死进程中的所有守护线程,换句话说,只要有任何非守护线程还在运行,程序就不会终止,守护线程也不会被干掉。守护线程的一个例子就是垃圾回收器,只要JVM不停止,垃圾收集器就会一直运行下去。
26. 什么是多线程的上下文切换?多线程的上下文切换是指CPU的控制权由一个正在运行的线程切换到另一个处于就绪状态并等待CPU执行权的线程的过程。
27. 怎么唤醒一个阻塞的线程?- sleep()或join()导致的阻塞:等待时间完成或上一个线程执行完成;
- wait():使用notify()或notifyAll()。
- execute()是Executor接口中的方法,submit()是ExecutorService接口中的方法,ExecutorService接口继承了Executor接口;
- execute()只能接受Runnable任务参数,submit()既可以接受Runnable任务参数也可以接受Callable任务参数;
- execute()没有返回值,submit()返回Future类型对象。
非定长的队列会先扩容,扩容到不能再扩容了就调用拒绝策略,定长的线程池队列会直接调用拒绝策略。
30. 线程池的类型?
补充:7.默认线程池修改
ThreadPool threadPoolExecutor = new ThreadPoolExecutor(参数);
| 线程池类型 | 队列类型 | 备注 |
|---|---|---|
| newCachedThreadPool | SynchronousQueue | 同步队列,无缓冲,拿一个出来,放一个进去,例如生产者-消费者模式 |
| newFixedThreadPool | LinkedBlockingQueue | |
| newScheduledThreadPool | DelayedWorkQueue | |
| newSingleThreadExecutor | LinkedBlockingQueue | |
| newSingleThreadScheduledExecutor | DelayedWorkQueue | |
| newWorkStealingPool | 可使用多种队列,每个线程对应一种队列 | |
| ThreadPoolExecutor | 自行通过参数指定 | 默认 |



