1、什么是线程?
1)线程是轻量级的进程
2)线程没有独立的地址空间(内存空间)
3)线程由进程创建(寄生在进程)
4)一个进程可拥有多个线程,同一进程的多个线程间可并发执行
5)线程状态:新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
2、多线程的优缺点?
优点:
1)使程序响应速度更快;
2)无处理任务时可将CPU时间让给其它任务;
3)可定期将CPU时间让给其它任务;
4)可随时停止任务;
5)可分别设置各任务优先级以优化性能
缺点:
1)线程需要占用内存,线程越多占内存越多;
2)多线程需协调和管理,需要CPU时间跟踪线程;
3)线程间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
4)线程太多会导致控制复杂;
3、多线程的几种实现方式?
1)继承Thread类,重写run方法;
2)实现Runnable接口,重写run方法;
3)实现Callable接口通过FutureTask包装器来创建Thread线程;
4)通过线程池创建;
4、什么是线程安全?
若多个线程同时执行一段程序结果和单线程结果一样,且其他变量值也和预期一样,就是线程安全。线程安全问题由全局变量及静态变量引起。若每个线程对全局变量、静态变量只有读操作,这个全局变量是线程安全的;若多个线程同时执行写操作,需要考虑线程同步。存在竞争的线程不安全,不存在竞争的线程就是安全的。
5、Vector,SimpleDateFormat是线程安全类吗?
Vector是线程安全的类,其在add()等操作上添加synchronized关键字实现同步,但并非绝对的线程安全,当迭代时,如果在另一个线程执行add(),remove()操作,仍有机率抛出异常ConcurrentModificationException。
SimpleDateFormat是线程不安全类,一般不要定义为static变量,如果定义为static必须加锁,或使用DateUtils工具类。JAVA8下替代方式:用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat;
6、Java哪种原型不是线程安全的?
对基本类型的操作都是原子级的,除了long和double类型。JVM将32位作为原子操作,并非64位。当线程把主存中的long/double类型的值读到线程内存时,可能是两次32位值的写操作,如果几个线程同时操作,就可能会出现高低2个32位值出错的情况发生。
在线程间共享long与double字段,必须在synchronized中操作,或声明为volatile。
7、哪些集合类是线程安全的?
线程安全的集合对象:Vector、HashTable、StringBuffer
非线程安全的集合对象:ArrayList、LinkedList、HashMap、HashSet、TreeMap、TreeSet、StringBulider
8、多线程中的忙循环是什么?
就是用循环让一个线程等待且不放弃CPU(而wait(), sleep()或yield()都放弃了CPU),目的是为了保留CPU缓存(在多核系统中,一个等待线程醒来时可能会在另一个内核中运行,这样会重建缓存,为避免重建缓存和减少等待重建时间)。
9、什么是线程局部变量Thread-Local Variable?
分别为每个线程存储各自的属性值,给每个线程使用。使用get()读取值,set()设置值。如果线程是第一次访问线程局部变量,线程局部变量可能还没有为它存储值,这时initialValue()会被调用,并返回当前时间值。线程局部变量也提供remove(),用来删除线程已存储的值。
Java并发API包含了InheritableThreadLocal类,如果一个线程是从其他某个线程中创建的,这个类将提供继承的值。如果一个线程A在线程局部变量已有值,当它创建其它某个线程B时,线程B的局部变量将跟线程A一样。你可以覆盖ChileValue(),这个方法用来初始化子线程在线程局部变量中的值。它使用父线程在线程局部变量中的值作为传入参数。
10、进程间如何通讯,线程间如何通讯?
进程与线程的区别:
1)线程产生的速度快,通讯快,切换快,因为处于同一地址空间。
2)线程的资源利用率好。
3)线程使用公共变量或内存时需要同步机制,进程不用。
一、进程通讯:
管道(pipe):一种半双工的通信方式,数据只能单向流动,且只能在具有亲缘关系(通常指父子进程关系)的进程间使用。
有名管道(namedpipe):也是半双工的通信方式,允许无亲缘关系进程间的通信。
信号量(semophore):是一个计数器,可用来控制多个进程对共享资源的访问。常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。主要作为进程间及同一进程内不同线程间的同步手段。
消息队列(messagequeue):消息链表,存放在内核中并由消息队列标识符标识。克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号(sinal):是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存(shared memory):映射一段能被其他进程访问的内存,由一个进程创建,多个进程都可以访问。是最快的IPC方式,与其他通信机制配合使用(如信号量),实现进程间的同步和通信。
套接字(socket):也是一种进程间通信机制,可用于不同设备间的进程通信。
二、线程通信
锁机制:包括互斥锁、条件变量、读写锁
互斥锁:提供了以排他方式防止数据结构被并发修改的方法。
读写锁:允许多个线程同时读共享数据,对写操作互斥。
条件变量:以原子方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
信号机制(Signal):类似进程间的信号处理
11、什么是多线程环境下的伪共享(false sharing)?
在主存中缓存是以cache line为单元存储的。cache line长度是2的指数倍,一般为32到256之间。大部分cache line长度为64 bytes。伪共享是指当不同线程修改在同一个cache line中的多个不相关变量时,造成性能下降的问题。
12、同步和异步有何异同,在什么情况下分别使用他们?
同步交互:指发送一个请求需要等待返回,然后才能够发送下一个请求,有个等待过程;
异步交互:指发送一个请求不需等待返回,随时可以再发送下一个请求,即不需要等待。
区别:一个需要等待,一个不需要等待,大部分情况下,优先选择异步交互方式。
同步示例:银行的转账系统,对数据库的保存操作等;
13、ConcurrentHashMap和Hashtable区别?
都可用于多线程环境,ConcurrentHashMap仅锁定map的某个部分,Hashtable会锁定整个map。
14、ArrayBlockingQueue, CountDownLatch用法?
BlockingQueue:一种阻塞的FIFO queue,每个BlockingQueue都有一个容量。当容量满时,添加数据会阻塞,当容量为空时,取元素会阻塞。BlockingQueue的两个实现类:
ArrayBlockingQueue
1)由数组支持的有界阻塞队列。
2)按FIFO(先进先出)排序元素。
3)一旦创建好这个数组,就不能再增加其容量。
4)向已满队列中放入元素会阻塞。
5)从空队列中提取元素会阻塞。
LinkedBlockingQueue
1)基于已链接节点的、范围任意的blocking queue的实现
2)按FIFO(先进先出)排序元素。
3)新元素插入到队列尾部,队列检索会获得队列头部的元素。吞吐量通常高于基于数组的队列。
4)容量范围可在构造方法参数中指定,防止队列过度扩展。
5)如果未指定容量,则它等于Integer.MAX_VALUE。除非插入节点会使队列超出容量,否则每次插入后会动态创建链接节点。
6)是线程阻塞安全的
7)不接受null元素
8)实现了Collection和Iterator接口的所有可选方法
二者区别:
1)队列中锁的实现不同
ArrayBlockingQueue中的锁是没有分离的,生产和消费用同一个锁;
LinkedBlockingQueue中的锁是分离的,生产用putLock,消费是takeLock;
2)在生产或消费时操作不同
ArrayBlockingQueue在生产和消费时,直接将枚举对象插入或移除;
LinkedBlockingQueue在生产和消费时,需要把枚举对象转换为Node<E>进行插入或移除,会影响性能;
3)队列大小初始化方式不同
ArrayBlockingQueue必须指定队列大小;
LinkedBlockingQueue可以不指定队列大小,默认是Integer.MAX_VALUE;
CountDownLatch:同步辅助类,在完成一组正在其他线程中执行的操作前,允许一个或多个线程一直等待。主要方法:
1)public CountDownLatch(count); //指定了计数的次数
2)public void countDown(); //当前线程调用此方法则计数减一
3)public void await() ; //调用该方法会一直阻塞当前线程,直到计时器的值为0
15、ConcurrentHashMap的并发度(Concurrency Level)是什么?
并发度:程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数(即Segment[]数组长度)。默认并发度为16,可以在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)。运行时通过将key的高n位(n = 32 – segmentShift)和并发度减1(segmentMask)做位与运算定位到所在的Segment。segmentShift与segmentMask都是在构造过程中根据concurrency level被相应的计算出来。如果并发度设置过小,会带来严重的锁竞争问题;如果并发度设置过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。
16、CyclicBarrier和CountDownLatch有什么不同?各自的内部原理和用法是什么?
CountDownLatch:一个或多个线程,等待另外N个线程完成某个事情后才能执行。
CyclicBarrier:N个线程相互等待,任何一个线程完成,所有线程都必须等待。
CountDownLatch是计数器,线程完成一个就记一个,类似报数, 只不过是递减的。
CyclicBarrier像一个水闸,线程执行像水流在水闸处都会堵住,等水满(线程到齐)才开始泄流。
总结:
CyclicBarrier就是一个栅栏,等待所有线程到达后再执行操作。barrier在释放等待线程后可重用。
CounDownLatch对于管理一组相关线程非常有用。
17、Semaphore的用法
一个线程同步辅助类,可以控制同时访问资源的线程个数,并提供了同步机制。例如,实现一个文件允许的并发访问数。单个信号量的Semaphore对象可以实现互斥锁功能,由一个线程获得“锁”,再由另一个线程释放“锁”,可应用于死锁恢复的场合。
Semaphore的主要方法:
void acquire():获取一个许可,在提供许可前一直将线程阻塞,否则线程被中断。
void release():释放一个许可,将其返回给信号量。
int availablePermits():返回此信号量中当前可用的许可数。
boolean hasQueuedThreads():查询是否有线程正在等待获取。
18、启动一个线程是调用run()还是start()?start()和run()有什么区别?
调用start()可启动线程,run()只是thread的一个普通方法,还是在主线程里执行。
19、调用start()时会执行run(),为什么不能直接调用run()?
run()只是类的一个普通方法,如果直接调用,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
20、sleep()和对象的wait()都可以让线程暂停执行,有什么区别?
sleep()是线程类的静态方法,让当前线程暂停指定的时间,将CPU让给其他线程,但对象的锁依然保持,休眠时间结束后会自动恢复。
wait()是Object类的方法,调用对象的wait()导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()或notifyAll()时才能唤醒等待池中的线程进入锁定池(lock pool),线程重新获得对象的锁就可以进入就绪状态。
21、yield()有什么作用?sleep()和yield()有什么区别?
1)sleep()给其他线程运行机会时不考虑线程优先级;yield()只给相同或更高优先级的线程运行机会;
2)线程执行sleep()后转入阻塞(blocked)状态,执行yield()转入就绪(ready)状态;
3)sleep()声明抛出InterruptedException,yield()没有声明任何异常;
4)sleep()比yield()具有更好的可移植性。
22、如何停止一个线程?
1)当run()完成后线程终止。
2)使用interrupt()中断线程。
23、stop()和suspend()为何不推荐使用?
stop()作为一种粗暴的线程终止行为,在线程终止前没有对其做任何清除操作,具有不安全性。
suspend()具有死锁倾向,调用suspend()时目标线程会挂起。如果目标线程挂起时在保护关键系统资源的监视器上持有锁,则在目标线程重新开始前,其他线程都不能访问该资源。对任何其他线程来说,如果想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。
24、如何在两个线程间共享数据?
1)如果每个线程执行的代码相同,可使用同一个Runnable对象,例如卖票系统。
2)如果每个线程执行的代码不同,需要用不同的Runnable对象,例如设计4个线程,其中两个线程每次对j增加1,另外两个线程每次对j减1,银行存取款。
25、如何让正在运行的线程暂停一段时间?
1)使用Sleep():并不会让线程终止,一旦线程从休眠中唤醒,线程的状态将会变为Runnable,并且根据线程调度执行。
2)使用wait():wait()里参数是暂停时间长度,以毫秒为单位。
26、什么是线程组,为什么Java不推荐使用?
线程组是由线程组成的管理线程的类,java.lang.ThreadGroup类。ThreadGroup类中的某些方法,可以对线程组中的线程产生作用。线程组中的线程可以修改组内的其他线程,包括那些位于分层结构最深处的。一个线程不能修改位于自己所在组或下属组之外的任何线程。
线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等,这些方法会导致线程安全问题(主要是死锁),已被废弃。
线程组ThreadGroup不是线程安全的,在使用过程中获取的信息并不是及时有效的,降低了使用价值。
27、你是如何调用wait()的?使用if块还是while循环?为什么?
wait()应该在循环调用,当线程获得CPU开始执行时,其他条件可能还没满足,应在处理前循环检测条件是否满足。
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}
28、有哪些不同的线程生命周期?
新建(new):当创建Thread类的一个实例(对象)时,此线程进入新建状态。
就绪(runnable):线程已启动,在就绪队列中排队等候得到CPU资源。例如:t1.start();
运行(running):线程获得CPU资源正在执行任务run(),此时除非此线程自动放弃CPU资源或者有更高优先级的线程进入,线程将一直运行到结束。
死亡(dead):当线程执行完或被其它线程杀死,进入死亡状态,这时线程不能再进入就绪状态。
堵塞(blocked):由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,进入堵塞状态。
睡眠:sleep(long t) 可使线程进入睡眠,一个睡眠的线程在指定的时间过去后可进入就绪状态。
等待:调用wait(),调用motify()或notifyAll()回到就绪状态。
被另一个线程所阻塞:调用suspend(),调用resume()恢复。
29、线程状态BLOCKED和WAITING有什么区别?
线程处于BLOCKED状态的场景:
1)当前线程在等待一个monitor lock,比如等待执行synchronized代码块或使用synchronized的方法。
2)在synchronized块中循环调用Object.wait()
线程处于WAITING状态的场景:
1)调用Object.wait(),但没指定超时值。
2)调用Thread.join(),但没指定超时值。
3)调用LockSupport对象的park()。
线程处于TIMED_WAITING状态的场景:
1)调用Thread.sleep()。
2)调用Object.wait(),指定超时值。
3)调用Thread.join(),指定超时值。
4)调用LockSupport.parkNanos()。
5)调用LockSupport.parkUntil()。
30、画一个线程的生命周期状态图
31、ThreadLocal用途是什么,原理是什么,用的时候要注意什么?
ThreadLocal使用场景有:用来解决数据库连接、Session管理等。
ThreadLocal线程本地变量在每个线程中对该变量会创建一个副本,即每个线程内都会有一个该变量,在线程内部都可使用,线程间互不影响,不存在线程安全问题,也不会影响程序执行性能。
1)通过ThreadLocal创建的副本存储在每个线程自己的threadLocals中;
2)threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,每个线程中可有多个threadLocal变量,如longLocal和stringLocal;
3)在进行get前,必须先set,否则会报空指针异常;要想在get前不需要调用set就能正常访问,必须重写initialValue()。
32、线程池是什么?为什么要使用它?
java.util.concurrent.Executors提供了一个java.util.concurrent.Executor接口的实现用于创建线程池。线程池作用就是限制系统中执行线程的数量。
为什么要用线程池:
1)减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2)根据系统的承受能力,调整线程池中工作线线程数目,防止因为消耗过多内存,导致服务器宕机(每个线程需要大约1MB内存,线程越多消耗内存越大,最后死机)。
33、如何创建一个Java线程池?
1)newSingleThreadExecutor-单线程的线程池
线程池只有一个线程在工作,相当于单线程串行执行所有任务。如果这个唯一的线程因异常结束,会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按任务的提交顺序执行。
2)newFixedThreadPool-固定大小的线程池
每次提交一个任务就创建一个线程,直到线程达到线程池的最大值。线程池的大小一旦达到最大值就会保持不变,如果某个线程因异常结束,那么线程池会补充一个新线程。
3)newCachedThreadPool-可缓存的线程池
如果线程池的大小超过了处理任务所需要的线程,就会回收部分空闲(60秒不执行任务)线程,当任务数增加时,线程池又可以添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小依赖于JVM能够创建的最大线程大小。
4)newScheduledThreadPool-大小无限的线程池
此线程池支持定时及周期性执行任务的需求。
34、ThreadPool用法与优势?
通过ThreadPoolExecutor来创建一个线程池。
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
线程池的优点:
1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2)提高响应速度。当任务到达时,任务不需等到线程创建就能立即执行。
3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可进行统一分配、调优和监控。
35、提交任务时,线程池队列已满时会发会生什么?
ThreadPoolExecutors.submit()会抛出RejectedExecutionException异常;
36、线程池的实现策略
RejectedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,必须对新提交的任务采用一种特殊的策略进行处理。这个策略默认配置是AbortPolicy,表示无法处理新任务而抛出异常。
JAVA提供了4种策略:
1)AbortPolicy:直接抛出异常
2)CallerRunsPolicy:只用调用所在的线程运行任务
3)DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4)DiscardPolicy:不处理,丢弃掉。
37、线程池的关闭方式有几种,各自的区别是什么?
对ExecutorService关闭方式有两种:一种是调用shutdown(),另一种是调用shutdownNow()。
shutdown:
1)调用后不允许继续往线程池内添加线程;
2)线程池变为SHUTDOWN状态;
3)所有在调用shutdown()前提交到ExecutorSrvice的任务都会执行;
4)一旦所有线程结束执行当前任务,ExecutorService才会真正关闭。
shutdownNow():
1)返回尚未执行的任务列表;
2)线程池变为STOP状态;
3)阻止所有正在等待启动的任务, 并停止当前正在执行的任务;
38、线程池中submit()和execute()有什么区别?
1)接收参数不一样
2)submit有返回值,execute没有
3)submit方便Exception处理
39、Java中用到的线程调度算法是什么?
Windows中Java线程调度:
Java线程一对一地绑定到Win32线程上,Win32线程也是一对一绑定到内核级线程上。JVM通过将Java线程的优先级映射到Win32线程的优先级上,从而影响系统的线程调度决策。
Windows内核使用了32级优先权模式来决定线程的调度顺序。优先权分为两类:可变类优先权(1-15级)、不可变类优先权(16-31级)。调度程序为每一个优先级建一个调度队列,从高优先级到低优先级队列逐个查找,直到找到一个可运行的线程。
Windows采用基于优先级的、抢占的线程调度算法。调度程序保证总是让具有最高优先级的线程运行。一个线程仅在如下四种情况下才会放弃CPU:被更高优先级的线程抢占、结束、时间片到、执行导致阻塞的系统调用。
当线程的时间片用完后,降低其优先级;当线程从阻塞变为就绪时,增加线程的优先级;当线程长时间没有机会运行时,系统也会提升线程优先级。Windows区分前台和后台进程,前台进程往往获得更长的时间片。以上措施体现了Windows基于动态优先级、分时和抢占的CPU调度策略。
Linux中Java线程调度:
在Linux上Java线程一对一映射到内核级线程上。Linux中不区分进程和线程,同一个进程中的线程可看作是共享程度较高的一组进程。Linux也是通过优先级来实现CPU分配,应用程序可以通过调整nice值(谦让值)来设置进程的优先级。nice值反映了线程的谦让程度,该值越高说明线程越有意愿把CPU让给别的线程。JVM需要实现Java线程的优先级到nice的映射。linux调度器实现了一个抢占的、基于优先级的调度算法,支持两种类型的进程调度:实时进程的优先级范围为[0,99],普通进程的优先级范围为[100,140]。
进程的优先权越高,所获得的时间片越大。每个就绪进程都有一个时间片,内核将就绪进程分为活动的(active)和过期的(expired)两类:只要进程的时间片没有耗尽,就一直有资格运行,称为活动的;当进程的时间片耗尽后,就没有资格运行了,称为过期的。调度程序总是在活动的进程中选择优先级最高的进程执行,直到所有的活动进程都耗尽了时间片。当所有的活动进程都变成过期的之后,调度程序再将所有过期的进程置为活动的,并为他们分配相应的时间片,重新进行新一轮的调度。
40、什么是多线程中的上下文切换?
CPU通过时间片段的算法来循环执行线程任务,循环执行即每个线程在允许运行的时间后切换,这种循环切换使各个程序从表面看是同时进行的。切换时会保存之前的线程任务状态,当切换到该线程任务时,会重新加载该线程的任务状态。这个从保存到加载的过程称为上下文切换。
若当前线程还在运行而时间片结束后,CPU将被剥夺并分配给另一个线程。
若线程在时间片结束前阻塞或结束,CPU进行线程切换,不会造成CPU资源浪费。
41、你对线程优先级的理解是什么?
每个线程都有优先级,高优先级的线程在运行时具有优先权,这依赖于线程调度的实现,这个实现和操作系统相关(OSdependent)。可以定义线程的优先级,但并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10,1最低,10最高)。
42、什么是线程调度器 (Thread Scheduler) 和时间分片 (Time Slicing)?
线程调度器:是一个操作系统服务,负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片:指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或线程等待的时间。线程调度并不受JVM控制,由应用程序来控制它更好。
43、请说出你所知的线程同步的方法?
wait():使一个线程处于等待状态,并释放所持有的对象lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意在调用此方法时,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,且不是按优先级。
notifyAll():唤醒所有处入等待状态的线程,并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
44、synchronized的原理是什么?
synchronized底层是通过一个monitor对象来完成,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步块或方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException异常的原因。
45、synchronized和ReentrantLock有什么不同?
1)Synchronized是java关键字,是原生语法层面的互斥,需要jvm实现。ReentrantLock是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
2)Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。在执行monitorenter指令时,首先尝试获取对象锁。如果这个对象没被锁定,或当前线程已拥有了那个对象锁,把锁计算器加1,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放。如果获取对象锁失败,当前线程就要阻塞,直到对象锁被另一个线程释放。
ReentrantLock是java.util.concurrent包下的一套互斥锁,ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放时,正在等待的线程可以放弃等待,避免出现死锁。
2.公平锁,多个线程等待同一个锁时,必须按申请锁的时间顺序获得锁,Synchronized是非公平锁,ReentrantLock默认构造函数是创建的非公平锁,可通过参数设为公平锁,但公平锁性能不高。
3.锁绑定多个条件,一个ReentrantLock对象可同时绑定多个对象。
46、什么场景下可以使用volatile替换synchronized?
Synchronized修饰范围为:类、方法、代码块、静态方法,不能修饰变量,不能使变量共享。
volatile使用场景
1)volatile可以修饰变量,共享变量。
2)保障共享变量对所有线程的可见性。保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量值,新值对其他线程是立即可见的。
Synchronized与volatile的区别:
1)volatile字段没有涉及到锁的操作。
2)volatile声明的是变量,修饰的是变量。
47、有T1,T2,T3三个线程,怎么确保它们按顺序执行?怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
要保证T1、T2、T3三个线程顺序执行,可以利用Thread.join()。
Thread.join()主要作用是同步,可使线程间的并行执行变为串行执行。当调用某个线程的这个方法时,会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
48、同步块内的线程抛出异常会发生什么?
无论同步块是正常还是异常退出,里面的线程都会释放锁,该功能可在finally block里释放锁实现。
49、当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
不能,其他线程只能访问该对象的非同步方法,同步方法则不能进入;synchronized修饰符要求执行方法时要获得对象锁,如果已经进入A方法,说明对象锁已经被取到。
50、使用synchronized修饰静态方法和非静态方法有什么区别?
1) synchronized static是某个类的范围,synchronized static cSync{}防止多个线程同时访问这个类中的synchronized static方法,可以对类的所有对象实例起作用。
2)synchronized是某实例的范围,synchronized isSync(){}防止多个线程同时访问这个实例中的synchronized方法。
51、如何从给定集合里创建一个synchronized的集合?
使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized集合。
52、Java Concurrency API中的Lock接口是什么?对比同步它有什么优势?
Lock与synchronized都能对多线程静态资源的修改做同步(互斥)控制;
Lock优势:
1)操作方面(锁控制):Lock可手动控制加锁与释放锁,synchronized自动释放锁。
2)性能方面:使用Lock接口的实现类ReadWriteLock子类ReentrantReadWriteLock的对象rwl,在多线程同时对静态资源进行修改时,可以使用rwl.readLock().lock()或rwl.writeLock().lock()对静态资源做并发修改控制。读写锁可以实现读写互斥,读读不互斥,这是synchroized实现不了的,synchroized对读读也互斥。
3)线程通信方面:Lock对应的实现类对象lock通过lock.newCondition()可以实例化Condition对象condition,condition通过condition.await()实现synchronized + t.wait()的效果,t代表线程,通过condition.signal()或condition.signalAll()实现线程唤醒,且lock可以为读写线程创建两种不同操作(read or write)类型的Condition对象,使得线程间通信要比传统的wait()、notifiy()进行线程通信的效率高。使得加锁、释放锁的操作更具选择性,精准性。
53、Lock与Synchronized的区别?Lock接口比synchronized块的优势是什么?
1)Lock是一个接口,synchronized是关键字,synchronized是在JVM层面上实现的,可以通过一些监控工具监控synchronized的锁定,在代码执行时出现异常,JVM会自动释放锁,Lock则不行,lock是通过代码实现的,要保证锁一定会被释放,就必须将unLock()放到finally{}中;
2)synchronized发生异常时,会自动释放线程占有的锁,不会导致死锁;Lock在异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized不行,等待的线程会一直等待下去,不能响应中断;
4)通过Lock可以知道有没有成功获取锁,synchronized无法办到。
5)Lock可以提高多个线程读操作的效率。当竞争资源非常激烈时,Lock性能远优于synchronized。
54、ReadWriteLock是什么?
读写锁是用来提升并发程序性能的锁分离技术。一个ReadWriteLock维护一对关联的锁,一个用于只读一个用于写。在没有写线程的情况下,一个读锁可能会同时被多个读线程持有。写锁是独占的,可以使用ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。
55、锁机制有什么用?
有些业务逻辑在执行过程中要求对数据进行排他性访问,需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。
56、什么是乐观锁(Optimistic Locking)?如何实现乐观锁?如何避免ABA问题?
Hibernate支持悲观锁和乐观锁两种锁机制。
悲观锁:悲观的认为在数据处理过程中极有可能存在修改数据的并发事务,并将处理的数据锁定。必须依赖数据库本身的锁机制才能真正保证数据访问的排他性。
乐观锁:对并发事务持乐观态度,通过宽松的锁机制来解决由于悲观锁排他性的数据访问对系统性能造成的严重影响。最常见的乐观锁是通过数据版本标识来实现的,读取数据时获得数据的版本号,更新数据时将此版本号+1,然后和数据库表对应记录的当前版本号进行比较,如果提交的数据版本号大于数据库中此记录的当前版本号则更新,否则认为是过期数据无法更新。
Hibernate中通过Session的get()和load()方法从数据库加载对象时可以通过参数指定使用悲观锁;而乐观锁可以通过给实体类加整型的版本字段再通过XML或@Version注解进行配置。
CAS是乐观锁技术,当多个线程使用CAS同时更新一个变量时,只有其中一个线程能更新变量值,其它都失败,失败的线程并不会被挂起,而是被告知这次竞争失败,并可再次尝试。
CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置值与预期原值匹配,那么处理器会自动将该位置值更新为新值,否则不操作。无论哪种情况,它都会在CAS指令前返回该位置的值。CAS有效说明了“我认为位置V应该包含值A;如果包含,则将B放到这个位置;否则不更改,然后告诉我这个位置现在的值即可。”这和乐观锁的冲突检查+数据更新原理一样。
CAS会导致“ABA问题”。
CAS算法实现的重要前提是需要取出内存中某时刻的数据,在下一时刻比较并替换,在这个时间差内可能会导致数据变化。比如线程1从内存位置V中取出A,这时线程2也从内存中取出A,并进行了一些操作变成了B,然后又将位置V的数据变成A,这时线程1进行CAS操作发现内存中仍是A,然后线程1操作成功。尽管线程1的CAS操作成功,但不代表这个过程没有问题。
通过对数据加版本号方式来解决ABA问题,乐观锁每次执行数据修改时,都会带上一个版本号,版本号和数据版本号一致就可以执行修改并对版本号执行+1操作,否则失败。
57、解释以下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁?
重排序:通常是编译器或运行时环境为了优化程序性能对指令进行重新排序执行的一种手段。重排序分为两类:编译器重排序和运行期重排序,分别对应编译时和运行时环境。
公平锁/非公平锁
公平锁:是指多个线程按照申请锁的时间顺序来获取锁。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,后申请的线程可能比先申请的优先获取锁。可能会造成优先级反转或饥饿现象。
ReentrantLock通过构造函数指定该锁是否公平锁,默认是非公平锁(优点:吞吐量比公平锁大)。
Synchronized也是非公平锁,不像ReentrantLock通过AQS来实现线程调度,无法使其变成公平锁。
可重入锁/不可重入锁
可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍可使用,且不发生死锁(前提是同一个对象或class)。ReentrantLock和synchronized都是可重入锁。
不可重入锁:与可重入锁相反,不可递归调用,递归调用就发生死锁。
独享锁/共享锁
独享锁:该锁每次只能被一个线程所持有。Synchronized是独享锁。
共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,读锁可被共享,写锁每次只能被独占。读锁的共享可保证并发读非常高效,但读写和写写,写读都是互斥的。
独享锁与共享锁也是通过AQS来实现的。
互斥锁/读写锁
互斥锁:在访问共享资源前加锁,访问后解锁。 加锁后,任何试图再次加锁的线程会被阻塞,直到当前线程解锁。如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被变成就绪状态, 第一个变为就绪状态的线程又执行加锁操作,其他线程又会进入等待。 在这种方式下,只有一个线程能访问被互斥锁保护的资源。
读写锁:既是互斥锁,又是共享锁,读共享,写互斥(排它锁)。具体实现是ReadWriteLock;
读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态。
乐观锁/悲观锁
悲观锁:总是假设最坏的情况,每次取数据时都认为别人会修改,所以每次取数据时都会上锁,别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作前先上锁。synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:总是假设最好的情况,每次取数据时都认为别人不会修改,所以不会上锁,在更新时会判断一下在此期间别人有没有更新这个数据,可以使用版本号机制和CAS算法实现。适用于多读的应用类型,可以提高吞吐量,像数据库提供的类似于write_condition机制。java.util.concurrent.atomic包下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
偏向锁/轻量级锁/重量级锁
锁的状态分为四级:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态;
四种状态会随着竞争逐渐升级,是不可逆的过程,即不可降级。这四种状态都不是Java中的锁,而是Jvm为提高锁的获取与释放效率而做的优化(使用synchronized时)。
偏向锁:一段同步代码一直被一个线程所访问,该线程会自动获取锁,降低获取锁的代价。
轻量级锁:当锁是偏向锁时,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:当锁为轻量级锁时,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数时,还没获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁(spinlock):当一个线程在获取锁时,如果锁已被其它线程获取,该线程将循环等待,然后不断的判断锁是否能够被获取,直到获取锁才会退出循环。
自旋锁:线程获取锁时,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
自旋锁无法保证公平性、可重入性。
基于自旋锁,可以实现具备公平性和可重入性质的锁。
58、什么时候应该使用可重入锁?
可重入锁指在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;
场景1:如果已加锁,则不再重复加锁
场景2:如果发现该操作已在执行,则尝试等待一段时间,等待超时则不执行
场景3:如果发现该操作已加锁,则等待一个个加锁(同步执行,类似synchronized)
场景4:可中断锁
59、简述synchronized锁的等级方法锁、对象锁、类锁?
线程进入同步代码块或方法时会自动获得锁,在退出时会释放锁。获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。内置锁是互斥锁,最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或阻塞,直到线程B释放这个锁,如果线程B不释放,线程A将永远等待。
方法锁和对象锁是一个东西,即只有方法锁(对象锁)和类锁两种锁;
对象锁和类锁:对象锁是用于对象实例方法,或一个对象实例上的,类锁是用于类的静态方法或类的class对象上的。类的对象实例可以有多个,但每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但每个类只有一个类锁。
60、Java中活锁和死锁有什么区别?
活锁和死锁类似,活锁的线程状态是不断改变的,活锁可认为是一种特殊的饥饿。活锁和死锁的主要区别是前者进程的状态可以改变却不能继续执行。
61、什么是死锁(Deadlock)?导致线程死锁的原因?如何确保N个线程可以访问N个资源同时不导致死锁?
两个或以上线程都在互相等待对方执行完毕才能继续执行时就发生死锁。死锁产生的四个必要条件:
1)互斥使用:当资源被一个线程使用时,别的线程不能使用
2)不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由占有者主动释放。
3)请求和保持:当资源请求者在请求其他资源的同时保持对原有资源的占有。
4)循环等待:存在一个等待环路队列,P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。
避免死锁的方式:指定获取锁的顺序,并强制线程按指定顺序获取锁。
62、死锁与饥饿的区别?
1)死锁是同步的,饥饿是异步的。
2)死锁可认为是两个线程或进程同时在请求对方占有的资源,饥饿可认为是一个线程或进程在无限的等待另外多个线程或进程占有的但不会释放的资源。
63、怎么检测一个线程是否拥有锁?
java.lang.Thread中有个方法holdsLock(),返回true表示当前线程拥有某个对象的锁。
64、如何实现分布式锁?
1)基于数据库。
2)基于缓存环境,redis、memcache等。
3)基于zookeeper。
65、有哪些无锁数据结构,它们实现的原理是什么?
无锁数据结构的实现主要基于两个方面:原子性操作和内存访问控制方法。
在构建无锁数据结构时需要用到RMW操作,包括:compare-and-swap (CAS)、fetch-and-add (FAA)、test-and-set (TAS) 等。其中最基本的是CAS,其他操作可通过CAS实现。
66、读写锁可用于什么应用场景?
有多个读数据的线程和单个写数据线程的场景,在读取数据的地方加读锁,在写数据的地方加写锁。
67、Executors类是什么?Executor和Executors的区别?
Executors类提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口,ExecutorService继承了Executor;
区别:
Executor是一个抽象层面的核心接口,将任务本身和执行任务分离;
ExecutorService接口对Executor接口进行了扩展,提供了返回Future对象、终止、关闭线程池等方法。当调用shutDown方法时,线程池会停止接受新的任务,但会完成正在pending中的任务。
Future对象提供了异步执行,无需等待任务执行完成,只要提交需要执行的任务,然后在需要时检查Future是否已经有结果,如果任务已经执行完成,就可以通过Future.get()获得执行结果。Future.get()是一个阻塞式的方法,如果调用时任务还没完成,会等待直到任务执行结束。
Executors是一个工具类,类似于Collections,提供工厂方法来创建不同类型的线程池。
1)ExecutorService接口继承了Executor接口,是Executor的子接口
2)Executor接口定义了execute()用来接收Runnable接口的对象,ExecutorService接口中submit()可以接受Runnable和Callable接口的对象。
3)Executor中的execute()不返回任何结果,ExecutorService中的submit()可以通过Future对象返回运算结果。
4)ExecutorService还提供用来控制线程池的方法。比如:调用shutDown()终止线程池。
5)Executors类提供工厂方法用来创建不同类型的线程池。
68、什么是Java线程转储(Thread Dump),如何得到它?
线程堆栈是虚拟机中线程(包括锁)状态的一个瞬间状态的快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。
线程堆栈的信息都包含:
1)线程名字,id,线程的数量等。
2)线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程在等待锁等)
3)调用堆栈包含完整的类名,所执行的方法,源代码的行数等,不同虚拟机打印堆栈略有些不同。
获取线程信息方式:
1、linux下执行Kill -3 PID可以生成jvm的thread dump
1) $ ps - ef | grep java 获得当前正在运行的java进程pid
2) kill -3 <pid>
2、使用Java命令jstack获得Thread dump
1)获取java进程pid,获取方式如jps -v命令,ps -ef|grep java命令,top命令等等。。
2)jstack -f <pid>
3、使用jvisualvm工具获得Thread dump
4、在Windows下可以在JVM的console窗口上敲Ctrl-Break。根据不同的设置,thread dump会输出到当前控制台上或应用服务器的日志里。如果想将日志输出到文件,可以修改tomcat/bin目录下的“catalina.bat”,在文件最后的四个ACTION后增加“>>%CATALINA_BASE%/logs/thread_demp.out”,同时修改bin目录下的startup.bat为:
rem call "%EXECUTABLE%" start "%CMD_LINE_ARGS%"
call "%EXECUTABLE%" run "%CMD_LINE_ARGS%"
这样就可以将日志输出到logs下的thread_dump.out文件中。也可以下载相应的分析工具对其进行分析。需要说明的一点是,将startup.bat修改为以上内容后,关闭tomcat时,直接关闭DOS窗口就可以了,不用shutdown.bat。
69、如何在Java中获取线程堆栈?
Java虚拟机提供了线程转储(thread dump)的后门,通过这个后门可以把线程堆栈打印出来。
命令:$jstack [option] pid >> 文件
70、说出3条在Java中使用线程的最佳实践
1)对线程命名
2)将线程和任务分离,使用线程池执行器来执行Runnable或Callable。
3)使用线程池
71、在线程中你怎么处理不可捕捉异常
1)实现UncaughtExceptionHandler接口来捕获抛出的异常
2)在Thread类中设置一个静态域
在java多线程程序中,所有线程都不允许抛出未捕获的checked exception(比如sleep时的InterruptedException),也就是说各个线程需要把自己的checked exception处理掉。
72、实际项目中使用多线程举例。你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?
多线程中常遇到的有Memory-interface、竞争条件、死锁、活锁和饥饿。
73、请说出与线程同步以及线程调度相关的方法?
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象锁;
sleep():使正在运行的线程处于睡眠状态,是静态方法,调用此方法要处理InterruptedException;
notify():唤醒一个处于等待状态的线程,调用此方法并不能确切的唤醒某个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁lock()和解锁unlock()方法,还提供了newCondition()方法来产生用于线程间通信的Condition对象;此外Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程数量。在对资源进行访问前,线程必须得到信号量的许可(调用Semaphore.acquire());完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore.release())。
74、如何在Windows和Linux上查找哪个线程使用的CPU时间最长?
windows上用任务管理器,linux下可以用top工具。 如果要查找具体的进程,可以用ps命令,如查找java:ps -ef |grep java
75、如何确保main()方法所在的线程是Java程序最后结束的线程?
需要用到Thread.join():
在一个线程中启动另外一个线程的join(),当前线程将会挂起,执行被启动的线程,直到被启动的线程执行完毕后,当前线程才继续执行。
76、什么是竞态条件? 举个例子说明。
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。临界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现。
class Counter {
protected long count = 0;
public void add(long value) {
this.count = this.count + value;
}
}