java面试总结(一)- 多线程

来源:互联网 发布:小猪o2o源码下载 编辑:程序博客网 时间:2024/06/05 10:45

1、线程和进程的区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务,不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。

2、实现线程有哪几种方式?
方案一:继承Thread类创建线程类(使用继承子Thread类的子类来创建线程类时,多个线程无法共享线程类的实例变量)
步骤:① 定义Thread类的子类 并重写该类的Run方法,该run方法的方法体就代表了该线程需要完成的任务
② 创建Thread类的实例,即创建了线程对象
③ 调用线程的start方法来启动线程
方案二:实现Runnable接口(采用Ruunable接口的方式创建多个线程可以共享线程类的实例变量,这是因为在这种方式下,程序创建 的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享一个实例变量)
步骤:①定义Runnable接口的实现类,并重写它的Run方法,run方法同样是该线程的执行体!
②创建Runnable实现类的实例,并将此实例作为Thread的target创建一个Thread对象,该Thread对象才是真正的线程对象!
③调用start方法启动该线程
方案三:使用callable和future创建线程
从Java5开始,Java提供 Callable接口,Callable接口提供了一个call()方法可以作为线程执行体,看起来和Runnable很像,但call()方法更强大——call()方法可以有返回值、call()方法可以抛出异常
Java5提供了Future接口来代表Callable接口的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现类Future接口,也实现了Runnable接口——可以作为Thread的target。
实现步骤:①创建Callable接口的实现类,并实现call方法,该call方法会成为线程执行体,且call方法具有返回值,在创建callable接口的实现类!
②使用FutrueTask类来包装Callable对象,该FutrueTask封装类Callable的call方法的返回值
③使用FutrueTask对象作为Thread的target创建并启动新线程!
④使用FutrueTask的get方法获取执行结束后的返回值
实现代码
实现代码

3、线程有哪几种状态?它们之间如何流转的?
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
状态图
- 新建状态(New):
当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。
当一个线程处于新生状态时,程序还没有开始运行线程中的代码。
- 就绪状态(Runnable):
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
- 运行状态(Running):当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
- 阻塞状态(Blocked):
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
……
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
- 死亡状态(Dead):
有两个原因会导致线程死亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false。(更为详尽的解释:https://www.cnblogs.com/jijijiefang/articles/7222955.html)

4、线程中的start()和run()方法有什么区别?
start()方法被用来启动新创建的线程,而且start()方法内部调用了run()方法,这和直接调用run()方法的效果是不一样的,当调用run()方法的时候,只会在原来的线程中调用,没有新的线程启动,start()方法才会启动新的线程。

5、怎么终止一个线程?如何优雅地终止线程?
- 使用stop()方法,已被弃用。原因是:stop()是立即终止,会导致一些数据被到处理一部分就会被终止,而用户并不知道哪些数据被处理,哪些没有被处理,产生了不完整的“残疾”数据,不符合完整性,所以被废弃。
- 使用volatile标志位:实现一个Runnable接口,在其中定义volatile标志位,在run()方法中使用标志位控制程序运行,使用volatile目的是保证可见性,一处修改了标志,处处都要去主存读取新的值,而不是使用缓存。
- 使用interrupt()中断的方式,注意使用interrupt()方法中断正在运行中的线程只会修改中断状态位,可以通过isInterrupted()判断。如果使用interrupt()方法中断阻塞中的线程,那么就会抛出InterruptedException异常,可以通过catch捕获异常,然后进行处理后终止线程。有些情况,我们不能判断线程的状态,所以使用interrupt()方法时一定要慎重考虑。

6、ThreadLocal在多线程中扮演什么角色?
ThreadLocal是java里的一种特殊的变量,每个线程都有一个ThreadLocal就是每个线程都拥有自己独立的一个变量,竞争条件被彻底消除了,是为创建代价高昂的对象获取线程安全的好方法。
ThreadLocal的重点知识:
1)、ThreadLocal的理念和线程同步机制不同,前者希望做到线程间的资源隔离,后者希望做到多线程通信的问题。
2)、ThreadLocal存储的ThreadLocalMap是放在每个Thread当中,并不是放在一个大Map里面。
3)、ThreadLocal所存储资源的生命周期并不是和Thread绑定在一起的
4)、什么情况下被废弃的ThreadLocal引用会被清理?
(1)Thread线程结束
(2)ThreadLocalMap中的数量到达最大值
(3)set方法调用时,hash没有命中,new Entry时,会被清理
(4)自己调用remove()
5)、ThreadLocal有什么坑?
ThreadLocal “污染”:对于线程池这种可能会导致线程复用的工具,如果在一次使用后忘记调用remove方法,当前使用的ThreadLocal变量会被保留,线程被下一次使用的时候就会造成变量污染,就会导致一些诡异的错误。(更为详尽的解释:http://blog.csdn.net/mr253727942/article/details/54318524)

7、线程中的wait()和sleep()方法有什么区别?
- 这两个方法来自不同的类分别是Object和Thread
- 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

8、多线程同步有哪几种方法?
1)、synchronized关键字:synchronized有两种用法(synchronized方法和synchronized块)
public synchronized void mutithreadAccess();
synchronized(syncObject){代码块}
2)、wait()方法和notify()方法:当使用synchronized来修饰某个共享资源时,如果线程A1执行synchronized代码,另外一个线程A2也要同时执行同一对象的同一synchronized代码时,线程A2将要等到线程A1执行完后,才能继续执行。这种情况下可以使用wait()方法和notify()方法。
在synchronized代码被执行期间,线程可以调用对象的wait方法,释放对象锁,进入等待状态,并且可以调用notify()方法或notify()方法通知正在等待的其他线程。notify()方法仅唤醒一个线程(等待队列中的第一个线程)并允许它去获得锁,notifyAll()方法唤醒所有等待这个对象的线程并允许它们去获得锁。
3)、Lock
JDK5新增了Lock接口以及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程同步。
ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁

9、什么是死锁?如何避免死锁?
是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志排序,规定所有的进程申请资源必须以一定的顺序做操作来避免死锁。

10、多线程之间如何进行通信?
利用同步和互斥来解决多线程之间的通讯和协作;可以说资源互斥、协调竞争是要解决的因,而同步是竞争协调的果。
- 通过synchronized/notify/notifyAll来实现线程之间的通信。
- 利用了Java5中提供的Lock/Condition来实现线程之间的相互通信。
- 使用信号量,如CyclicBarrier/Semaphore/Countdownbatch。
(更为详尽的解释:http://www.importnew.com/26850.html)

11、线程怎样返回结果?如何获取?
Callable+ScheduledThreadPoolExecutor实现
这个是在Java1.5以后,添加了ScheduledThreadPoolExecutor和callable两个组件(接口),ScheduledThreadPoolExecutor用来执行线程进行调度的时候,有一个方法.schedule(Callable callable, long delay, TimeUnit unit),注意:这里的参数是Callable而不是Runnable,其实它和Runnable差不多,都能实现有个线程,只不过Callable可以添加返回值。
所以在用ScheduledThreadPoolExecutor调度线程得到返回值 ScheduledFuture后执行get()方法,就可以得到返回值了。

12、说说violatile关键字有什么用,和Synchronized有什么区别?
volatitle是一个特殊的修饰符,只有成员变量才能使用它,在java并发程序缺少同步类的情况下,多线程对成员变量的操作对其他线程是透明的,volatile变量可以保证下一个读取操作会在前一个读取操作之后发生。
区别:
1)、volatile是变量修饰符,而synchronized则作用于一段代码或方法。
2)、volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。

13、假如新建T1、T2、T3三个线程,如何保证它们按顺序执行?
T3先执行,在T3的run中,调用t2.join,让t2执行完成后再执行t3
在T2的run中,调用t1.join,让t1执行完成后再让T2执行。

14、怎么控制同一时间只有3个线程运行?
todo

15、为什么要使用线程池?
创建线程要花费昂贵的资源和时间,如果任务来了创建线程那么响应时间会变长而且一个进程能创建的线程数有限为了避免这些问题在程序启动的时候就创建若干线程来响应处理,它们被成为线程池。

16、说一说常用的几种线程池并讲讲其中的工作原理
1)、 newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
2)、newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
3)、newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4)、newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

17、线程池启动线程submit()和execute()有什么不同?
JDK5往后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,它们的区别是:
execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。——实现Runnable接口
submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。——实现Callable接口

18、说说多线程并发控制中的倒计时器、循环栅栏是什么,有什么应用场景?
是多线程并发控制中非常有用的工具类,它可以控制线程等待,直到倒计时器归0再继续执行。
待组织:https://baijiahao.baidu.com/s?id=1572134934603237&wfr=spider&for=pc
https://baijiahao.baidu.com/s?id=1572226690550158&wfr=spider&for=pc

19、什么是活锁、饥饿、无锁、死锁?
死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。
活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
饥饿,我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过JDK的CAS原理及应用即是无锁的实现。

20、什么是原子性、可见性、有序性?

原创粉丝点击