读书笔记----《编写高质量代码:改善Java程序的151个建议》第八/九章
第八章 异常
110:提倡异常封装
这里的“异常封装”主要是指将系统异常包装成让客户和维护人员易于理解的自定义Exception。
优点:
(1) 提高系统的友好性
(2) 提高系统的可维护性
(3) 解决Java异常机制自身的缺陷
Java中的异常一次只能抛出一个,比如doStuff方法有两个逻辑代码片段,如果在第一个逻辑片段中抛出异常,则第二个逻辑片段就不再执行了,也就无法抛出第二个异常了。
可以定义一个异常容器MyException,容纳多个异常,MyException本身并不代表任何异常含义,它所解决的是一次抛出多个异常的问题。
111:采用异常链传递异常
异常传递到业务上层时先封装成上层易于理解的异常。
112:受检异常尽可能转化为非受检异常
非受检异常: 即RuntimeException,常见的有:
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ArithmeticException - 算术运算异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常
除了非受检异常,其他的Exception都是受检异常,要求try catch或向上抛出异常。
文中列举了受检异常的不足:
- 受检异常使接口声明脆弱
- 受检异常使代码的可读性降低
- 受检异常增加了开发工作量
但是个人建议不要这么做。
113:不要在finally块中处理返回值
public static int doStuff(){
int a=1;
try{
return a;
}catch(Exception e){
}finally{
//重新修改一下返回值
a=-1;
}
return 0;
}
该方法的返回值永远是1, 而不会是-1或0
不是0可以理解, 不是-1的原因:
方法是在栈内存中运行的, 并且会按照“先进后出”的原则执行, main方法调用了doStuff方法, 则main方法在下层, doStuff在上层, 当doStuff方法执行完“return a”时, 此方法的返回值已经确定是in类型1(a变量的值, 注意基本类型都是值拷贝, 而不是引用), 此后finally代码块再修改a的值已经与doStuff返回者没有任何关系了, 因此该方法永远都会返回1。
如果想使finally中对a的修改起作用可以使用Integer替换基本类型。
再看以下例子:
public static void doSomething(){
try{
//正常抛出异常
throw new RuntimeException();
}finally{
//告诉JVM:该方法正常返回
return;
}
}
public static void main(String[]args){
try{
doSomething();
}catch(RuntimeException e){
System.out.println("这里永远都不会到达!");
}
}
上面finally代码块中的return已经告诉JVM:doSomething方法正常执行结束, 没有异常, 所以main方法就不可能获得任何异常信息了。
在finally中处理return返回值, 代码看上去很完美, 都符合逻辑, 但是执行起来就会产生逻辑错误, 最重要的一点是finally是用来做异常的收尾处理的, 一旦加上了return语句就会让程序的复杂度徒然提升, 而且会产生一些隐蔽性非常高的错误。
与return语句相似, System.exit(0)或Runtime.getRuntime().exit(0)出现在异常代码块中也会产生非常多的错误假象, 增加代码的复杂性。
注意 不要在finally代码块中出现return语句。
114:不要在构造函数中抛出异常
对于构造函数, 错误只能抛出, 这是程序人员无能为力的事情;
非受检异常不要抛出, 抛出了“对己对人”都是有害的;
受检异常尽量不抛出, 能用其他方式实现就用其他方式实现。
115:使用Throwable获得栈信息
OP编程可以很轻松地控制一个方法调用哪些类, 也能够控制哪些方法允许被调用, 一般来说切面编程(比如AspectJ)只能控制到方法级别, 不能实现代码级别的植入(Weave), 比如一个方法被类A的m1方法调用时返回1, 在类B的m2方法调用时返回0(同参数情况下), 这就要求被调用者具有识别调用者的能力:
class Foo{
public static boolean m(){
//取得当前栈信息
StackTraceElement[]sts=new Throwable().getStackTrace();
//检查是否是m1方法调用
for(StackTraceElement st:sts){
if(st.getMethodName().equals("m1")){
return true;
}
}
return false;
}
}
//调用者
class Invoker{
//该方法打印出true
public static void m1(){
System.out.println(Foo.m());
}
//该方法打印出false
public static void m2(){
System.out.println(Foo.m());
}
}
看Invoker类, 两个方法m1和m2都调用了Foo的m方法, 都是无参调用, 返回值却不同, 这是我们的Throwable类发挥效能了,
JVM在创建一个Throwable类及其子类时会把当前线程的栈信息记录下来, 以便在输出异常时准确定位异常原因:
public class Throwable implements Serializable{
//出现异常的栈记录
private StackTraceElement[]stackTrace;
//默认构造函数
public Throwable(){
//记录栈帧
fllInStackTrace();
}
//本地方法, 抓取执行时的栈信息
public synchronized native Throwable fillInStackTrace();
}
更多的时候我们使用m方法的变形体:
class Foo{
public static boolean m(){
//取得当前栈信息
StackTraceElement[]sts=new Throwable().getStackTrace();
//检查是否是m1方法调用
for(StackTraceElement st:sts){
if(st.getMethodName().equals("m1")){
return true;
}
}
throw new RuntimeException("除m1方法外, 该方法不允许其他方法调用");
}
}
除了m1方法外, 其他方法调用都会产生异常, 该方法常用作离线注册码校验, 当破解者试图暴力破解时, 由于主执行者不是期望的值, 因此会返回一个经过包装和混淆的异常信息, 大大增加了破解的难度。
116:异常只为异常服务
这句话意思是对异常的处理不应该作为主逻辑运行的依据:
//判断一个枚举是否包含String枚举项
public static< T extends Enum< T>>boolean Contain(Class< T>c, String name){
boolean result=false;
try{
Enum.valueOf(c, name);
result=true;
}catch(RuntimeException e){
//只要是抛出异常, 则认为是不包含
}
return result;
}
如以上这个例子, 将是否抛出异常作为了函数返回值的依据。
此外还有以下缺点:
- 异常判断降低了系统性能
- 降低了代码的可读性, 只有详细了解valueOf方法的人才能读懂这样的代码, 因为valueOf抛出的是一个非受检异常。
- 隐藏了运行期可能产生的错误, catch到异常, 但没有做任何处理
异常只能用在非正常的情况下, 不能成为正常情况的主逻辑, 也就是说, 异常只是主场景中的辅助场景, 不能喧宾夺主。
public static void main(String[]args){
File file=new File("文件.txt");
//经常出现的异常情况, 可以先做判断
if(fle.exists()&&!fle.isDirectory()){
try{
}catch(){
}
}
}
以上代码虽然增加了if判断语句, 增加了代码量, 但是却会减少FileNotFoundException异常出现的几率, 提高了程序的性能和稳定性。
117:多使用异常, 把性能问题放一边
单单从对象的创建上来说, new一个IOException会比String慢5倍, 这从异常的处理机制上也可以解释:因为它要执行fillInStackTrace方法, 要记录当前栈的快照。
而且, 异常类是不能缓存的, 期望预先建立大量的异常对象以提高异常性能也是不现实的。
但是 性能问题不是拒绝异常的借口。
第9章 多线程和并发
118:不推荐覆写start方法
普通情况下直接覆写start方法会导致一个错误的多线程应用, 根本就没有启动一个子线程。
更根本的讲:原因在start的实现:
public synchronized void start(){
//判断线程状态, 必须是未启动状态
if(threadStatus!=0)
throw new IllegalThreadStateException();
//加入线程组中
group.add(this);
//分配栈内存, 启动线程, 运行run方法
start0();
//在启动前设置了停止状态
if(stopBeforeStart){
stop0(throwableFromStop);
}
}
//本地方法
private native void start0();
键是本地方法start0, 它实现了启动线程、申请栈内存、运行run方法、修改线程状态等职责, 线程管理和栈内存管理都是由JVM负责的。
所以不需要关注线程和栈内存的管理, 只需要编码者实现多线程的逻辑即可(即run方法体), 这也是JVM比较聪明的地方, 简化多线程应用。
如果确实有必要覆写start方法, 是一个罕见的要求:
class MultiThread extends Thread{
@Override
public void start(){
/*线程启动前的业务处理*/
super.start();
/*线程启动后的业务处理*/
}
@Override
public void run(){
//MultiThread do something.
}
}
只要在start方法中加上super.start即可
119:启动线程前stop方法是不可靠的
所有的线程在启动前都执行stop方法, 虽然它是一个已过时(Deprecated)的方法, 但它的运行逻辑还是正常的, stop方法在此处的目的并不是停止一个线程, 而是设置线程为不可启用状态。
但是运行结果却出现了奇怪的现象:部分线程还是启动了,
Thread类的stop方法会根据线程状态来判断是终结线程还是设置线程为不可运行状态, 对于未启动的线程(线程状态为NEW)来说, 会设置其标志位为不可启动, 而其他的状态则是直接停止。stop方法的源代码如下:
public final void stop(){
if((threadStatus!=0)&&!isAlive()){
return;
}
stop1(new ThreadDeath());
}
private final synchronized void stop1(Throwable th){
/*安全检查省略*/
if(threadStatus!=0){
resume();
stop0(th);
}else{
if(th==null){
throw new NullPointerException();
}
stopBeforeStart=true;
throwableFromStop=th;
}
start方法中是这样校验的:
public synchronized void start(){
//分配栈内存, 启动线程, 运行run方法
start0();
//在启动前设置了停止状态
if(stopBeforeStart){
stop0(throwableFromStop);
}
}
//本地方法
private native void start0();
start0方法在前, 也就是说即使stopBeforeStart为true(不可启动), 也会先启动一个线程, 然后再stop0结束这个线程。所以不再使用stop方法进行状态的设置, 直接通过判断条件来决定线程是否可启用。
120:不使用stop方法停止线程
- stop方法是过时的
- stop方法会导致代码逻辑不完整
stop方法会清除栈内信息, 结束该线程, 这也就导致了run方法的逻辑不完整 - stop方法会破坏原子逻辑
多线程为了解决共享资源抢占的问题, 使用了锁概念, 避免资源不同步, 但是正因此原因, stop方法却会带来更大的麻烦:它会丢弃所有的锁, 导致原子逻辑受损。
解决办法:
- 使用线程池(比如ThreadPoolExecutor类), 那么可以通过shutdown方法逐步关闭池中的线程, 它采用的是比较温和、安全的关闭线程方法。
- 自己写一个终止方法, 如:terminate, 保证不破坏锁。
补充:
interrupt不能终止一个正在执行着的线程, 它只是修改中断标志而已。
该标识位使isInterrupted()方法返回ture。
121:线程优先级只使用三个等级
Java的线程有10个级别(准确地说是11个级别, 级别为0的线程是JVM的, 应用程序不能设置该级别)
Thread t=new Thread(this);
//设置线程优先级
t.setPriority(_priority);
线程优先级特点:
- 并不是严格遵照线程优先级别来执行的
- 优先级差别越大, 运行机会差别越明显
事实上, 不同的操作系统线程优先级是不相同的, Windows有7个优先级, Linux有140个优先级, Freebsd则有255个(此处指的是优先级总数, 不同操作系统有不同的分类, 如中断级线程、操作系统级等, 各个操作系统具体用户可用的线程数量也不相同)。Java是跨平台的系统, 需要把这个10个优先级映射成不同操作系统的优先级, 于是界定了Java的优先级只是代表抢占CPU的机会大小, 优先级越高, 抢占CPU的机会越大, 被优先执行的可能性越高, 优先级相差不大, 则抢占CPU的机会差别也不大, 这就是导致了优先级为9的线程可能比优先级为10的线程先运行。
Java的缔造者们也察觉到了线程优先问题, 于是在Thread类中设置了三个优先级, 此意就是告诉开发者, 建议使用优先级常量, 而不是1到10随机的数字。常量代码如下:
public class Thread implements Runnable{
//最低优先级
public final static int MIN_PRIORITY=1;
//普通优先级, 默认值
public final static int NORM_PRIORITY=5;
//最高优先级
public final static int MAX_PRIORITY=10;
}
大部分情况下MAX_PRIORITY的线程会比NORM_PRIORITY的线程先运行, 但是不能认为必然会先运行, 不能把这个优先级做为核心业务的必然条件, Java无法保证优先级高肯定会先执行, 只能保证高优先级有更多的执行机会。
122:使用线程异常处理器提升系统可靠性
Java 1. 5版本以后在Thread类中增加了setUncaughtExceptionHandler方法, 实现了线程异常的捕捉和处理:
class TcpServer implements Runnable{
//创建后即运行
public TcpServer(){
Thread t=new Thread(this);
t.setUncaughtExceptionHandler(new TcpServerExceptionHandler());
t.start();
}
@Override
public void run(){
//正常业务运行, 运行3秒
for(int i=0;i< 3;i++){
try{
Thread.sleep(1000);
System.out.println("系统正常运行:"+i);
}catch(InterruptedException e){
e.printStackTrace();
}
}
//抛出异常
throw new RuntimeException();
}
//异常处理器
private static class TcpServerExceptionHandler implements
Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e){
//记录线程异常信息
System.out.println("线程"+t.getName()+"出现异常, 自行重启, 请分析原因。");
e.printStackTrace();
//重启线程, 保证业务不中断
new TcpServer();
}
}
}
线程出现运行期异常(也就是Uncaught Exception)时, 由TcpServerExceptionHandler异常处理器来处理
若要在实际环境中应用, 则需要注意以下三个方面:
(1)共享资源锁定
如果线程异常产生的原因是资源被锁定, 自动重启应用只会增加系统的负担, 无法提供不间断服务。
(2)脏数据引起系统逻辑混乱
(3)内存溢出
线程异常了, 但由该线程创建的对象并不会马上回收, 如果再重新启动新线程, 再创建一批新对象, 特别是加入了场景接管, 就非常危险了。
123:volatile不能保证数据同步
原文费力列举了一个volatile失效的实例, 但主要原因在于对volatile执行的操作不是原子性的导致的, 如i++
可以使用atomic包中的对象来达到数据的同步。
124:异步运算考虑使用Callable接口
其实所有的线程类都提倡使用Callable接口代替直接继承Thread。
125:优先选择线程池
public static void main(String[]args){
//2个线程的线程池
ExecutorService es=Executors.newFixedThreadPool(2);
//多次执行线程体
for(int i=0;i< 4;i++){
es.submit(new Runnable(){
public void run(){
System.out.println(Thread.currentThread().getName());
}
});
}
//关闭执行器
es.shutdown();
}
Executors.newFixedThreadPool(2)表示创建一个具有2个线程的线程池:
public class Executors{
public static ExecutorService newFixedThreadPool(int nThreads){
//生成一个最大为nThreads的线程池执行器
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.
MILLISECONDS, new LinkedBlockingQueue< Runnable>());
}
}
这里使用了LinkedBlockingQueue作为任务队列管理器, 所有等待处理的任务都会放在该队列中, 需要注意的是, 此队列是一个阻塞式的单端队列。
线程池中的线程是在submit第一次提交任务时建立的:
public Future< ?>submit(Runnable task){
//检查任务是否为null
if(task==null)throw new NullPointerException();
//把Runnable任务包装成具有返回值的任务对象, 不过此时并没有执行, 只是包装
RunnableFuture< Object>ftask=newTaskFor(task, null);
//执行此任务
execute(ftask);
//返回任务预期执行结果
return ftask;
}
此处的关键是工作线程的创建, 它也是通过new Thread方式创建的一个线程, 只是它创建的并不是我们的任务线程, 而是经过包装的Worker线程, 代码如下:
private final class Worker implements Runnable{
//运行一次任务
private void runTask(Runnable task){
//这里的task才是我们自定义实现Runnable接口的任务
task.run();
}
//工作线程也是线程, 必须实现的run方法
public void run(){
while(task!=null||(task=getTask())!=null){
runTask(task);
task=null;
}
}
//从任务队列中获得任务
Runnable getTask(){
for(;){
return workQueue.take();
}
}
execute方法是通过Worker类启动的一个工作线程, 执行的是我们的第一个任务, 然后该线程通过getTask方法从任务队列中获取任务, 之后再继续执行, 但问题是任务队列是一个BlockingQueue, 是阻塞式的, 也就是说如果该队列元素为0, 则保持等待状态, 直到有任务进入为止:
public E take()throws InterruptedException{
//如果队列中元素数量为0, 则等待
while(count.get()==0)
notEmpty.await();
//等待状态结束, 弹出头元素
x=extract();
//如果队列数量还多于1个, 唤醒其他线程
if(c>1)
}
notEmpty.signal();
//返回头元素
return x;
}
创建一个阻塞队列以容纳任务, 在第一次执行任务时创建足够多的线程(不超过许可线程数), 并处理任务, 之后每个工作线程自行从任务队列中获得任务, 直到任务队列中的任务数量为0为止, 此时, 线程将处于等待状态, 一旦有任务再加入到队列中, 即唤醒工作线程进行处理, 实现线程的可复用性。
使用线程池减少的是线程的创建和销毁时间, 这对于多线程应用来说非常有帮助, 比如我们最常用的Servlet容器, 每次请求处理的都是一个线程, 如果不采用线程池技术, 每次请求都会重新创建一个线程, 这会导致系统的性能负荷加大, 响应效率下降, 降低了系统的友好性。
126:适时选择不同的线程池来实现
Java的线程池实现从最根本上来说只有两个:ThreadPoolExecutor类和Scheduled-ThreadPoolExecutor类, 这两个类还是父子关系, 但是Java为了简化并行计算, 还提供了一个Executors的静态类, 它可以直接生成多种不同的线程池执行器, 比如单线程执行器、带缓冲功能的执行器等。
ThreadPoolExecutor类:
public class ThreadPoolExecutor extends AbstractExecutorService{
//最完整的构造函数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long
keep Alive Time, Time Unitunit, Block in gQueue< Runnable>
workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler){
//检验输入条件
if(corePoolSize< 0||maximumPoolSize< =0||
maximumPoolSize< corePoolSize||keepAliveTime< 0)
throw new IllegalArgumentException();
//检验运行环境
if(workQueue==null||threadFactory==null||handler==null)
throw new NullPointerException();
this.corePoolSize=corePoolSize;
this.maximumPoolSize=maximumPoolSize;this.workQueue=workQueue;
this.keepAliveTime=unit.toNanos(keepAliveTime);this.threadFactory=threadFactory;
this.handler=handler;
}
}
参数名与含义:
参数名 | 含义 |
---|---|
corePoolSize | 最小线程数 |
maximumPoolSize | 最大线程数量 |
keepAliveTime | 线程最大生命期 |
unit | 时间单位(纳秒、毫秒、秒、分等) |
workQueue | 任务队列 |
threadFactory | 线程工厂 |
handler | 拒绝任务处理器 |
threadFactory:定义如何启动一个线程, 可以设置线程名称, 并且可以确认是否是后台线程等。
handler:线程池的管理是这样一个过程:首先创建线程池, 然后根据任务的数量逐步将线程增大到corePoolSize数量, 如果此时仍有任务增加, 则放置到workQueue中, 直到workQueue爆满为止, 然后继续增加池中的线程数量(增强处理能力), 最终达到maximumPoolSize, 那如果此时还有任务要增加进来呢?这就需要handler来处理。
想象以下示例:
在一条生产线上, 车间规定是可以有corePoolSize数量的工人, 但是生产线刚建立时, 工作不多, 不需要那么多的人。随着工作数量的增加, 工人数量也逐渐增加, 直至增加到corePoolSize数量为止。
此时任务如果还在增加, corePoolSize数量的工人不停歇地处理任务, 新增加的任务按照一定的规则存放在仓库中(也就是我们的workQueue中), 一旦任务增加的速度超过了工人处理的能力, 也就是说仓库爆满时, 车间就会继续招聘工人(也就是扩大线程数), 直至工人数量达到maximumPoolSize为止, 如果所有的maximumPoolSize工人都在处理任务, 而且仓库也是饱和状态, 新增任务就会扔给一个叫handler的专门机构去处理了, 它要么丢弃这些新增的任务, 要么无视, 要么替换掉别的任务。
方法: newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue< Runnable>()));
}
方法: newCachedThreadPool
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new
SynchronousQueue< Runnable>());
}
建立了一个线程池, 而且线程数量是没有限制的(当然, 不能超过Integer的最大值), 一个线程在60秒内一直是出于等待状态, 则会被终止。
任务队列使用了同步阻塞队列, 这意味着向队列中加入一个元素, 即可唤醒一个线程(新创建的线程或复用池中空闲线程)来处理, 这种队列已经没有队列深度的概念了。
方法: newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new
LinkedBlockingQueue< Runnable>());
}
127:Lock与synchronized是不一样的
//显式锁任务
class TaskWithLock extends Task implements Runnable{
//声明显式锁
private fnal Lock lock=new ReentrantLock();
@Override
public void run(){
try{
//开始锁定
lock.lock();
doSomething();
}finally{
//释放锁
lock.unlock();
}
}
}
public static void runTasks(Class< ?extends Runnable>clz)throws Exception{
ExecutorService es=Executors.newCachedThreadPool();
System.out.println("***开始执行"+clz.getSimpleName()+"任务****");
//启动三个线程
for(int i=0;i< 3;i++){
es.submit(clz.newInstance());
}
//等待足够长的时间, 然后关闭执行器
TimeUnit.SECONDS.sleep(10);
System.out.println("------"+clz.getSimpleName()+"任务执行完毕-----\n");
//关闭执行器
es.shutdown();
}
public static void main(String[]args)throws Exception{
//运行显式锁任务
runTasks(TaskWithLock.class);
//运行内部锁任务
runTasks(TaskWithSync.class);
}
Lock锁不出现互斥情况!!!
对于同步资源来说(示例中是代码块), 显式锁是对象级别的锁, 而内部锁是类级别的锁, 也就是说Lock锁是跟随对象的, synchronized锁是跟随类的, 更简单地说把Lock定义为多线程类的私有属性是起不到资源互斥作用的, 除非是把Lock定义为所有线程的共享变量。
其他的不同:
- Lock支持更细粒度的锁控制 (lock分读锁、写锁、可重入读写锁…)
- Lock是无阻塞锁, synchronized是阻塞锁
当线程A持有锁时, 线程B也期望获得锁, 此时, 如果程序中使用的是显式锁, 则B线程为等待状态(在通常的描述中, 也认为此线程被阻塞了), 若使用的是内部锁则为阻塞状态。 - Lock可实现公平锁, synchronized只能是非公平锁
当一个线程A持有锁, 而线程B、C处于阻塞(或等待)状态时, 若线程A释放锁, JVM将从线程B、C中随机选择一个线程持有锁并使其获得执行权, 这叫做非公平锁(因为它抛弃了先来后到的顺序)
显式锁默认是非公平锁, 但可以在构造函数中加入参数true来声明出公平锁, 而synchronized实现的是非公平锁, 它不能实现公平锁。 - Lock是代码级的, synchronized是JVM级的
Lock是通过编码实现的, synchronized是在运行期由JVM解释的, 相对来说synchronized的优化可能性更高
灵活、强大则选择Lock, 快捷、安全则选择synchronized。
128:预防线程死锁
synchronized是可重入的, 如以下示例:
class Foo implements Runnable{
public void run(){
//执行递归函数
fun(10);
}
//递归函数
public synchronized void fun(int i){
if(--i>0){
for(int j=0;j< i;j++){
System.out.print("*");
}
System.out.println(i);
fun(i);
}
}
}
//该段代码是可以成功输出倒三角的
*********9
********8
*******7
******6
*****5
****4
***3
**2
*1
达到线程死锁需要四个条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 资源独占条件:一个线程因请求资源而阻塞时, 对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前, 不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
可以按照以下两种方式来解决:
(1)避免或减少资源共享
(2)使用自旋锁
自旋锁(Spin Lock), 它跟互斥锁一样, 如果一个执行单元要想访问被自旋锁保护的共享资源, 则必须先得到锁, 在访问完共享资源后, 也必须释放锁。如果在获取自旋锁时, 没有任何执行单元保持该锁, 那么将立即得到锁;如果在获取自旋锁时锁已经有保持者, 那么获取锁操作将“自旋”在那里, 直到该自旋锁的保持者释放了锁为止。
public void b2(){
try{
//立刻获得锁, 或者2秒等待锁资源
if(lock.tryLock(2, TimeUnit.SECONDS)){
System.out.println("进入B.b2()");
}
}catch(InterruptedException e){
//异常处理
}finally{
//释放锁
lock.unlock();
}
}
线程A等待线程B释放锁, 在2秒内不断尝试是否能够获得锁, 达到2秒后还未获得锁资源, 线程A则结束运行, 线程B将获得资源继续执行, 死锁解除。
129:适当设置阻塞队列长度
ArrayBlockingQueue类最常用的add方法
public class ArrayBlockingQueue< E>extends AbstractQueue< E>
implements BlockingQueue< E>, java.io.Serializable{
//容纳元素的数组
private final E[]items;
//元素数量计数器
private int count;
public boolean add(E e){
//调用offer方法尝试写入
if(offer(e))
return true;
else
//写入失败, 队列已满
throw new IllegalStateException("Queue full");
}
public boolean offer(E e){
final ReentrantLock lock=this.lock;
//申请锁, 只允许同时有一个线程操作
lock.lock();
try{
//元素计数器的计数与数组长度相同, 表示队列已满
if(count==items.length)
return false;
else{//队列未满, 插入元素
insert(e);
return true;
}
}finally{
//释放锁
lock.unlock();
}
}
}
如果直接调用offer方法插入元素, 在超出容量的情况下, 它除了返回false外, 不会提供任何其他信息, 如果代码不做插入判断, 那就会造成数据的“默默”丢失, 这就是它与非阻塞队列的不同之处。
如果应用期望无论等待多长时间都要运行该任务, 不希望返回异常就需要用BlockingQueue接口定义的put方法了, 它的作用也是把元素加入到队列中, 但它与add、offer方法不同, 它会等待队列空出元素, 再让自己加入进去, 通俗地讲, put方法提供的是一种“无赖”式的插入, 无论等待多长时间都要把该元素插入到队列中:
public void put(E e)throws InterruptedException{
//容纳元素的数组
final E[]items=this.items;
final ReentrantLock lock=this.lock;
//可中断锁
lock.lockInterruptibly();
try{
try{
//队列满, 等待其他线程移除元素
while(count==items.length)
notFull.await();
}catch(InterruptedException ie){
//被中断了, 唤醒其他线程
notFull.signal();
throw ie;
}
//插入元素
insert(e);
}finally{
//释放锁
lock.unlock();
}
}
与插入元素相对应, 取出元素也有不同的实现, 例如remove、poll、take等方法, 对于此类方法的理解要建立在阻塞队列的长度固定的基础上, 然后根据是否阻塞、阻塞是否超时等实际情况选用不同的插入和提取方法。
130:使用CountDownLatch协调子线程
CountDownLatch的作用是控制一个计数器, 每个线程在运行完毕后会执行countDown, 表示自己运行结束, 这对于多个子任务的计算特别有效, 比如一个异步任务需要拆分成10个子任务执行, 主任务必须要知道子任务是否完成, 所有子任务完成后才能进行合并计算, 从而保证了一个主任务的逻辑正确性。
static class Runner implements Callable< Integer>{
//开始信号
private CountDownLatch begin;
//结束信号
private CountDownLatch end;
public Runner(CountDownLatch_begin, CountDownLatch_end){
begin=_begin;
end=_end;
}
@Override
public Integer call()throws Exception{
//跑步的成绩
int score=new Random().nextInt(25);
//等待发令枪响起
begin.await();
//跑步中……
TimeUnit.MILLISECONDS.sleep(score);
//跑步者已经跑完全程
end.countDown();
return score;
}
}
public static void main(String[]args)throws Exception{
//参加赛跑人数
int num=10;
//发令枪只响一次
CountDownLatch begin=new CountDownLatch(1);
//参与跑步有多个
CountDownLatch end=new CountDownLatch(num);
//每个跑步者一个跑道
ExecutorService es=Executors.newFixedThreadPool(num);
//记录比赛成绩
List< Future< Integer>>futures=new ArrayList< Future< Integer>>();
//跑步者就位, 所有线程处于等待状态
for(int i=0;i< num;i++){
futures.add(es.submit(new Runner(begin, end)));
}
//发令枪响, 跑步者开始跑步
begin.countDown();
//等待所有跑步者跑完全程
end.await();
int count=0;
//统计总分
for(Future< Integer>f:futures){
count+=f.get();
}
System.out.println("平均分数为:"+count/num);
}
131:CyclicBarrier让多线程齐步走
CyclicBarrier解决了两个线程独立运行, 在没有线程间通信的情况下, 两个线程汇集在同一原点的问题。
static class Worker implements Runnable{
//关卡
private CyclicBarrier cb;
public Worker(CyclicBarrier_cb){
cb=_cb;
}
public void run(){
try{
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName()+"-到达汇合点");
//到达汇合点
cb.await();
}catch(Exception e){
//异常处理
}
}
}
public static void main(String[]args)throws Exception{
//设置汇集数量, 以及汇集完成后的任务
CyclicBarrier cb=new CyclicBarrier(2, new Runnable(){
public void run(){
System.out.println("隧道已经打通!");
}
});
//工人1挖隧道
new Thread(new Worker(cb), "工人1").start();
//工人2挖隧道
new Thread(new Worker(cb), "工人2").start();
}
CyclicBarrier关卡可以让所有线程全部处于等待状态(阻塞), 然后在满足条件的情况下继续执行