文章目录
第一部分 基础知识
第一章 简介
1.1线程的作用?
线程会共享进程范围内资源,如:内存句柄和文件句柄,但每个线程都有各自的程序计数器、栈以及局部变量等。
第二章 线程安全性
2.1 如何编写线程安全的代码?
编写线程安全的代码,其核心在于对状态访问操作的管理,特别是对共享和可变状态的访问。
2.2 什么叫线程安全?
当多个线程访问某个类时,这个类始终都能表现出正确的行为,就称这个类是线程安全的。
2.3 什么叫无状态对象?
无状态对象一定是线程安全的。无状态对象指:既不包含任何域,也不包含任何对其他类中域的引用的对象。
2.4 竞态条件是如何产生的?
当某个计算的正确性取决于多线程交替执行时序时,就会产生竞态条件。
2.5 synchronized锁是什么?
synchronized锁也称为内置锁、监视器锁,是一个可重入互斥锁。
2.6 串行访问是什么?
串行访问意味着多线程依次以独占的方式访问对象,而不是并发访问。
2.7 锁粒度原则?
尽量将不影响共享状态且执行时间较长的代码从同步代码块中分离出去。
第三章 对象的共享
3.1 内存可见性是什么?
一个线程修改了对象的状态后,其他线程能够立刻看到状态变化。
3.2 指令重排序是什么?
在没有同步的情况下,编译器、处理器和运行时等都能对操作的执行顺序进行调整。
3.3 字撕裂是什么?
非volatile类型的long、double变量,JVM规范允许将64位的读写操作分解为两个32位的操作。因此,多线程下,有可能会读取到一个变量的前32位和另一个变量的后32位。
3.4 volatile关键字的作用?
防止指令重排序、保证内存可见性,但不保证i++原子性。
3.5 并发程序的对象访问策略?
- 线程封闭:仅在单线程内访问对象;
- 只读共享
- 线程安全共享:只能通过持有特定的锁来访问对象。
第四章 对象的组合
4.1 实例封闭机制是什么?
将数据封装在对象内部,将数据的访问限制在方法上。
4.2 java监视器模式?
使用私有的锁对象而不是对象的内置锁。
private final Object lock = new Object();
4.3 注意事项?
将同步策略文档化。
第五章 基础构建模块
5.1 常用同步容器类?
Vector、HashTable、Collections.synchronizedXxx()。将它们的状态封装起来,并对每个公有方法进行同步。
5.2 ConcurrentHashMap的特点?
分段锁,任意数量线程可以并发读,一定数量线程可以并发写。
5.3 CopyOnWriteAyyayList的特点?
在每次对容器修改的时候,都会创建并发布一个新的容器。
5.4 如何解决队列超负荷?
- 减轻负荷,减少生产者的线程数量;
- 将多余的工作项序列化到磁盘。
5.5 为什么有的方法会抛出InterruptedException?
当方法抛出InterruptedException,表示该方法是一个阻塞方法,可能会导致线程进入阻塞状态(BLOCKED、WAITING、TIMED-WAITING)。
5.6 常用同步工具类?
CountDownLatch、FutureTask、Semaphore、CyclicBarrier、Exchanger
第二部分 结构化并发应用程序
第六章 任务执行
6.1 Executors工具类的常用方法?
- Executors.newFixedThreadPool:固定长度线程池,每次提交任务就创建一个线程,直到达到线程池最大数量。如果有线程遇到Exception而结束,线程池也会补充一个新的线程;
- Executors.newCachedThreadPool:可缓存线程池,线程池的最大长度位Integer.MAX_VALUE,线程数会随着任务数而变化;
- Executors.newScheduledThreadPool:固定长度线程池,且以延迟或定时的方式执行任务;
- Executors.newSingleThreadExecutor:单线程的Executor。
6.2 ExecutorService的生命周期?
ExecutorService的生命周期有3种状态:运行、关闭和已终止。
- shutdown方法:将平缓的执行关闭过程:不再接收新任务,且等待已接收任务执行完成;
- shutdownNow方法:将暴力的执行关闭过程:尝试取消正在执行的任务,且直接返回(但不执行)未启动的任务。
6.3 Runnable与Callable的异同?
Runnable与Callable都用来描述抽象的计算任务,但是Runnable没有返回值也不能返回受检异常,而Callable有返回值且能抛出异常。
第7章 取消与关闭
7.1 实例方法interrupt()方法的作用?
直接调用interrupt()方法只会将线程的中断标识设置为true,但当线程调用了sleep()、wait()、join()方法处于阻塞状态时,调用interrupt()方法后中断状态会被清除,同时会抛出一个InterruptedException。
7.2 静态方法interrupted()方法的作用?
清除当前线程的中断状态
7.3 屏蔽中断请求?
只有实现了线程中断策略的代码才可以屏蔽中断请求(捕获InterruptedException并做出处理)。
7.4 “毒丸”对象?
关闭生产者-消费者模式的其中一个方法就是使用“毒丸”对象,当得到这个对象时,立即停止(以毒丸对象作为退出循环的条件)。
7.5 RuntimeException?
RuntimeException默认会在控制台中输出栈追踪信息,并终止线程。
7.6 JVM对异常的处理?
当线程由于未捕获异常而终止时,JVM会将这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提供任何异常处理器,默认会将栈追踪信息输出到System.err。
7.7 ThreadPool的异常处理器?
当需要为线程池中的每一个线程设置UncaughtExceptionHandler时,可以为ThreadPoolExecutor构造函数提供一个ThreadFactory。在线程池中,由execute提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler处理;由submit提交的任务,它抛出的异常将被future.get()封装在ExecutionException中抛出。
public class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
return t;
}
}
7.8 守护线程?
守护线程与普通线程仅在线程退出时存在差异。每当有线程退出时,JVM就会检查所有的线程,如果只剩守护线程,JVM就跟守护线程一起退出。JVM退出时,会跳过所有守护线程的finally、回卷栈操作。
第8章 线程池的使用
8.1 什么是线程饥饿死锁?
所有正在执行任务的线程都由于等待其它处在工作队列的任务而阻塞,称为线程饥饿死锁。
8.2 如何处理等待超时?
如果等待超时,可以把任务标识为失败,然后中止任务或者把任务重新返回队列等待下次执行。
8.3 计算任务运行时间?
重写ThreadPoolExecutor的beforeExecute和afterExecute方法,计算任务运行时间。
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
private final ThreadLocal<Long> startTimes = new ThreadLocal();
@Override protected void beforeExecute(final Thread t, final Runnable r) {
super.beforeExecute(t, r);
startTimes.set(System.currentTimeMillis());
}
@Override protected void afterExecute(final Runnable r, final Throwable t) {
super.afterExecute(r, t);
long endTime = System.currentTimeMillis();
if (endTime - startTimes.get() > 5000) {
//TODO
}
}
}
8.4 ThreadPoolExecutor的各个参数?
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数,每次向线程池提交任务时就会创建一个线程,直到线程数等于核心线程数。此时,继续提交的任务会被放入工作队列,只有在工作队列满了的情况下,才会创建超过corePoolSize数量的线程数;
- maximumPoolSize:最大线程数,当线程池的线程数量已达到最大线程数,仍有任务提交时,就会
- keepAliveTime、unit:存活时间,当某个线程的空闲时间大于存活时间,且存活线程数超过核心线程数,该线程就会被回收。
- workQueue:工作队列
- ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO;
- LinkedBlockingQuene:基于链表的无界阻塞队列(最大容量为Interger.MAX),FIFO;
- PriorityBlockingQueue:具有优先级的无界阻塞队列;
- SynchronousQueue:不缓存任务的阻塞队列,生产者提交的任务必须等到消费者取出这个任务,如果没有可用线程,则创建新线程,如果线程数已达到maximumPoolSize,则执行饱和策略handler;
- threadFactory:线程创建工厂,可以指定线程的名字、未捕获异常;
- handler:饱和策略
- AbortPolicy,中止
- DiscardPolicy,抛弃
- DiscardOldestPolicy,抛弃最旧
- CallerRunsPolicy,调用者运行
第9章 图形用户界面应用程序
第三部分 活跃性、性能与测试
第10章 避免活跃性危险
10.1 什么是死锁?
多个线程由于存在环路的锁依赖关系而无限地等待下去。
10.2 数据库对死锁的处理?
当数据库服务器检测到死锁时,将选择一个牺牲者并放弃这个事务。
10.3 什么是锁顺序死锁?
两个线程尝试以不同的顺序获得相同的两个锁。如果所有的线程都以固定的顺序获得锁,那么程序就不会出现锁顺序死锁问题。
10.4 什么是动态锁顺序死锁?
由于无法控制锁对象的顺序,从而导致的锁顺序死锁。
解决:必须定义锁的顺序,可以使用System.identityHashCode()方法。在极少数情况下,两个对象可能拥有相同的散列值,可以使用“加时赛”锁。
private final Object LOCK = new Object();
public void transfer(Account from, Account to, Dollars) {
int fromHash = System.identityHashCode(from);
int tomHash = System.identityHashCode(to);
if(fromHash > tomHash){
synchronized (from){
synchronized (to){
//TODO
}
}
}else if(fromHash < tomHash){
synchronized (to){
synchronized (from){
//TODO
}
}
}else{
synchronized (LOCK){
synchronized (from){
synchronized (to){
//TODO
}
}
}
}
}
10.5 协作对象间发生的死锁?
如果在持有锁的同时调用某个外部方法,这个外部方法可能获取获取其它锁,因此导致死锁或者阻塞时间过长。
10.6 什么是开放调用?
如果在调用某个方法时不需要持有锁,就称为开放调用。
10.7 什么是活锁?
当多个相互协作的线程都对彼此进行相应从而修改各自的状态,并使得任何一个线程都无法继续执行,就产生了活锁。(例如两个太过礼貌的人彼此让路,结果在下一个路由又遇到了,再继续让路)
解决:在重试机制中引入随机性。
10.8 Thread API设置线程优先级?
在Thread API中定义的线程的10个优先级只是作为线程调度的参考,JVM会根据需要将它们映射为操作系统的调度优先级,但这种映射关系与特定平台相关。在某些操作系统中,如果调度优先级数量小于10个,那么多个java调度优先级可能会被映射为一个优先级。
10.9 如何避免产生死锁?
- 尽量减少加锁数量,多使用开放调用;
- 确保多个线程获取锁时,保持固定的顺序;
- 支持定时锁释放,将超过指定时间后的锁释放并返回错误信息;
- 将获取锁时要遵守的规范文档化。
第11章 性能与可伸缩性
11.1 如何减少锁的竞争?
- 减少锁的持有时间;
- 减小锁的粒度:
- 锁分解:如果一个锁需要保护多个相对独立的状态变量,可以将锁分解为多个锁,每个锁保护一个变量;
- 锁分段:将锁分解进一步扩展为对一个对象进行分解,如ConcurrentHashMap;
- 采用非独占式的锁或者非阻塞锁代替独占锁,如ReadWriteLock(读取操作可以同时访问共享资源,写入操作必须以独占方式获取锁)。
第12章 并发程序的测试
第四部分 高级主题
第13章 显示锁
13.1 内置锁的局限性?
无法中断一个正在等待获取锁的线程;无法在请求获取锁超时后中断获取。
13.2 显示锁的特点?
可定时、可轮询、可中断、公平性,必须在finally中释放。
- 可定时、可轮询的功能由tryLock方法实现;
- 可中断的功能由lockInterffuptibly方法实现;
- 公平性与非公平性由构造函数fair字段控制。
13.3 显示锁的公平性?
ReentrantLock的构造函数提供fair字段,用于创建公平锁与非公平锁(默认)。
- 公平锁:新发出的请求将被放入队列中,线程按照请求的顺序获得锁;
- 非公平锁:只有当锁被某个线程占有时,新发出的请求才被放入队列中。如果在发起请求的同时,锁刚好处于可用状态,那么这个请求线程将直接跳过队列中所有等待线程,直接获得锁。
13.4 非公平锁性能优于公平锁的原因?
在恢复一个被挂起的线程与这个线程真正运行起来之间存在延迟,而“插队”线程正好能利用这段延迟时间。
13.5 显示锁与内置锁的选择?
java5前,显示锁远胜于内置锁;java6后,显示锁略胜于内置锁。由于内置锁使用更普遍,且内置锁避免了编程人员忘记释放锁的状况,因此仅当内置锁不能满足需求时,才考虑使用显示锁。
第14章 构建自定义的同步工具
14.1 Object.wait与Thread.sleep方法的区别?
- wait:会自动释放锁,并请求操作系统挂起当前线程,等待被唤醒;
- sleep:不会释放锁,线程休息结束后自动进入就绪妆态。
14.2 JUC.Condition?
Object.wait <> Condition.await
Object.notify <> Condition.signal
Object.notifyAll <==> Condition.signalAll
14.3 JUC.AbstractQueuedSynchronizer?
AQS是一个用于构建锁和同步器的框架,是JUC包的核心,ReentrantLock、Semaphore、CountDownLatch、FutureTask都是基于AQS构建的。
第15章 原子变量与非阻塞同步机制
15.1 CAS是什么?
CAS(Compare And Swap),比较并交换,先检查再运行。
boolean compareAndSet(int expect, int update)
如果oldValue == expect ,则将oldValue修改为update值,并返回true;如果oldValue != expect,直接返回false。
15.2 CAS的缺陷?
CAS的最大缺陷在于难以围绕CAS构建正确的外部算法。
15.3 锁与CAS性能比较?
大多数情况下,CAS性能优于锁;仅在高度竞争情况下,锁性能优于CAS。
15.4 非阻塞算法是什么?
一个线程的失败或挂起不会导致其他线程也失败或挂起,就称为非阻塞算法。
第16章 Java内存模型
16.1 为什么主内存和工作内存数据不一致?
JMM规定每个处理器都有自己的缓存,并且定期与主内存进行协调。允许不同的处理器在任意时刻从同一存储位置上看到不同的值。
16.2 Happens-Before是什么?
JMM为程序中的所有操作定义了一个偏序关系,也称为Happens-Before。要想保证执行操作B的线程一定能看到操作A的结果(无论操作A和B是否是同一个线程),那么就操作A和操作B就必须满足Happens-Before的关系。如果操作A和B之间缺乏Happens-Before的关系,那么JVM可以对他们进行任意的重排序。
- 程序顺序规则:如果操作A的代码在操作B前,那么程序A在程序B前执行;
- 监视器锁规则:解锁操作会在获得锁前执行;
- volatile变量规则:对volatile的写入操作会在读操作前执行。