Java并发编程从入门到精通 - 第2章:认识Thread
线程实现的三种方法:
1、三种实现方式的简记:
继承Thread类,重写run()方法;
实现Runnable接口,重写run()方法,子类创建对象并作为Thread类的构造器参数;
实现Callable接口,重写call()方法,子类创建对象并作为FutureTask类的构造器参数,FutureTask类创建对象并作为Thread类的构造器参数;
2、三种实现方法的比较:
继承Thread类:因为是单继承,所以扩展性不好;
实现Runnable接口:接口可以多重实现;并且还可以再继承一个类;扩展性好
实现Callable接口:Runnable无法返回结果,且不能抛出返回的异常;而Callable接口可以,Callable产生结果,FutureTask可以拿到结果,也可以捕获Callable抛出的异常;
最常用的就是第二种,实现Runnable接口;
Thread里面的属性和方法:
线程的中断机制:
1、详述:
Java中断机制是一种协作机制,也就是说通过中断不能直接终止另一个线程,而需要被请求中断的线程自己处理中断,且该线程可以选择接受请求中断自己,也可以选择不接受请求不中断自己;
每个线程都有一个boolean类型的标识(中断状态位),代表是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身),如果有中断请求,该标志位会被设置为true;
2、三个方法的比较:
public void interrupt():
用于中断线程;调用该方法仅仅只是将线程的中断状态位设为true,并不会真的停止线程,还是需要用户自己去监视线程的状态位并做处理;
中断是通过调用Thread.interrupt()方法来做的;这个方法通过修改被调用线程的中断状态来告知那个线程,说它被请求中断了;对于非阻塞中的线程,只是改变了中断状态,即Thread.isInterrupted()将返回true;对于可取消的(不解)阻塞状态中的线程,比如等待在这些函数上的线程:Thread.sleep(),Object.wait(),Thread.join()等,这个线程收到中断信号后,会抛出InterruptedException,同时会把中断状态置回为false;
当一个线程处于中断状态时(意思是它的中断状态位为true),如果再由wait、sleep以及jion三个方法引起的阻塞,那么JVM会将线程的中断标志重新设置为false,并抛出一个InterruptedException异常;
本质作用:根据try-catch功能块捕捉jvm抛出的InterruptedException异常来做各种处理,比如如何退出线程;总之interrupt的作用就是需要用户自己去监视线程的状态位并做处理;
public static boolean interrupted():返回线程的上次的中断状态,并清除中断状态(清除是什么意思,将true改为false,还是既不是true也不是false);
public boolean isInterrupted():判断线程是否中断;
线程的生命周期:
1、线程生命周期的5中状态:
(1)、新建(new Thread):此时线程有自己的内存空间,但并没有运行;且线程还不是活着的;
(2)、就绪(runnable):线程已经被启动(具备了运行条件),正在等待被分配给CPU时间片(不一定会被立即执行,处于线程就绪队列);此时线程是活着的;
(3)、运行(running):线程获得CPU资源正在执行任务(执行run()方法);此时除非线程放弃CPU或者有优先级更高的线程进入,线程将一直运行到结束;此时线程是活着的;
(4)、阻塞(blocked):由于某种原因导致正在运行的线程让出CPU并暂停自己的操作(任务执行),即进入阻塞状态;此时线程还是活着的;阻塞原因如下:
正在休眠:线程调用sleep(long t)方法进入休眠,休眠到指定时间后进入就绪状态;
正在等待:线程调用wait()方法,可调用notify()方法回到就绪状态;
被另一个线程所阻塞:调用suspend()方法,可调用resume()方法恢复;
(5)、死亡(dead):当线程执行完毕或被其他线程杀死,线程就进入死亡状态;此时线程不可能再进入就绪状态等待执行;此时线程不是活着的;
死亡原因如下:
自然终止:正常运行run()方法后终止;
异常终止:调用stop()方法让一个线程终止运行;
2、与线程状态对应的常用方法:
run():必须被重写,实现具体的业务功能;
start():启动线程;
sleep():释放CPU执行权,不释放锁;
wait():释放CPU执行权,释放锁;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池(Waiting Pool)中,同时失去了对象锁,只是暂时失去对象锁,wait后(不解)还要返还对象锁;当前线程必须拥有当前对象的锁,如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常,所以wait()必须在同步块(synchronized block)中调用;
notify()/notifyAll():唤醒在当前对象等待池中等待的第一个线程/所有线程;notify()/notifyAll()也必须拥有相同对象锁,否则也会抛出IllegalMonitorStateException异常;
yied():使当前正在运行的线程临时暂停,让出CPU的使用权,让同等优先权的线程运行(但并不保证当前线程会被JVM再次调度,使该线程重新进入Running状态);如果没有同等优先权的线程,那么yied()方法将不会起作用;
3、状态转换:
新建 -> 就绪:
start();
就绪 -> 运行:
获得CPU执行权;
运行 -> 就绪:
yield();
运行 -> 阻塞:
sleep()、wait()、join()、synchronized;
阻塞 -> 就绪:
sleep()结束、wait()结束、IO完成;
运行 -> 死亡:
正常结束、异常退出;
守护线程:
可以简单的理解为后台运行线程;
进程结束,守护线程跟着自动结束,不需要手动的去关心和通知其状态;
Java的垃圾回收是一个守护线程;
当正在运行的线程都是守护线程时,Java虚拟机退出;
JRE判断程序是否执行结束的标准是所有的前台运行线程执行完毕了,而不管后台线程的状态;
当进程中所有非守护线程已结束或退出时,即使仍有守护线程在运行,进程仍将结束;
线程组:
ThreadLocal:
当前线程副本;
当使用 ThreadLocal 维护变量的时候,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程对应的副本;从线程的角度看,目标变量就像是线程的本地变量;
ThreadLocal在处理线程的局部变量的时候比synchronized同步机制解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性;
注意:使用ThreadLocal,一般都是声明在静态变量(没说局部变量)中,如果不断创建ThreadLocal而且没有调用其remove()方法,将会导致内存泄漏;
线程的异常处理:
详解:
run方法不允许抛出异常,所有的异常必须在run方法中进行处理;就是说run方法中可以抛出异常,但不能往run方法外抛出异常,在run方法里面抛出的异常也必须在run方法内处理掉;
在run方法中,抛出的已检查异常(checked exception)必须使用try...catch...进行处理,否则报错,编译不通过;(不是在run方法中抛出已检查异常,则既可以使用try...catch...进行处理,也可以使用throws继续往上抛)
在run方法中,虽然向外抛出未检查异常不会报错,但这样不合理(run方法中的异常应该在run处理);
线程中处理异常的方法总结:
不能直接在一个线程中抛出异常;
如果是已检查异常(checked exception),推荐采用try...catch...块来处理;
如果是未检查异常(unchecked exception),推荐方法:注册一个实现UncaughtExceptionHandler接口的对象实例来处理;
线程中,处理未检查异常的方法的具体步骤总结:
定义一个类实现UncaughtExceptionHandler接口,在需要实现的方法里面包含对异常处理的逻辑和步骤;
定义一个线程,执行需要的业务逻辑功能;
在创建和执行该子线程的方法中,在thread.start()语句前增加一个thread.setUncaughtExceptionHandler(自定义异常处理类对象)语句来实现异常处理逻辑的注册;