一、简单了解
多线程:多任务的一种特别的形式,但使用了更小的资源开销,能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
二、创建线程
1)通过继承 Thread 类本身。
2)通过实现 Runnable 接口。
3)两者比较:推荐通过实现Runnable接口实现线程,首先避免了类只能单继承的问题,其次可以被多个线程(Thread实例)共享。
三、线程分类
1)线程可分为用户线程,守护线程两种:
2)守护线程的创建:可以通过调用Thread类的setDaemon(ture)方法设置当前线程为守护线程。
四、线程的生命周期
可分为五种状态:
1)创建:通过继承Thread类或实现Runnable 接口创建线程。
2)就绪:创建完成后调用start()方法,要等待JVM里线程调度器的调度。
3)堵塞:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态
4)运行:如果就绪状态的线程获取 CPU 资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
5)终止:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
五、线程的启动、休眠、中断
1)启动:
1、调用start()方法启动线程,而不用run()方法。run()方法是由main线程执行的,而要子线程执行就一定要先调用start()启动新线程去执行。run()只是一个类中的普通方法,调用run()跟调用普通方法一样。而调用start(),则会做一系列工作去创建新线程,然后在新线程中执行run()里面的任务内容。
2)休眠:
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态方法sleep()方法来实现。
sleep()方法有两种重载形式:
1.static void sleep( long millis ):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。
2.static void sleep( long millis,int nanos ):加上纳秒,很少使用该方法。
3)停止:
1、使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。
2、使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。
3、使用 interrupt 方法中断线程。interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
六、线程常用方法
1) wait():
等待方法是让线程进入等待队列,使用方法是 obj.wait(); 这样当前线程就会暂停运行,并且进入obj的等待队列中,称作“线程正在obj上等待”。
如果线程想执行 wait 方法,线程必须拥有锁。如果线程进入等待队列,就会释放其实例的锁。
有两种形式的wait方法
一种是:接受毫秒数作为参数,含义与sleep中方法里接受参数的意思相同,都是指“在此期间暂停”,但是与sleep方法不同的是,wait 在暂停等待期间,对象锁是释放的,而sleep是拥抱着这把锁。
另一种是:wait 方法不接受任何参数,这种wait(),将无限等待下去,直到线程接收到nofity或者notifyAll的通知信息,才能继续执行。
2)notify():
用于唤醒单个线程。 此方法仅为一个等待特定对象的线程提供通知。
如果使用notify()方法并且有多个线程正在等待通知,那么只有一个线程获得通知,而剩下的线程必须等待进一步的通知。
3)join() :
加入线程(当前执行的线程是A线程,调用join()方法的是B线程) 当前线程阻塞 执行B线程 B执行结束之后A线程才能执行放弃当前线程的执行。
4)yield():
暂停当前方法,释放自己拥有的CPU,线程进入就绪状态,不会释放同步锁。。
它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;
但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;
也有可能是当前线程又进入到“运行状态”继续运行!
七、线程的异常处理
1)默认异常处理:
线程都不允许抛出未捕获的checked exception(比如sleep时的InterruptedException),也就是说各个线程需要自己把自己的checked exception处理掉。
在Java中,线程方法的异常(无论是checked还是unchecked exception),都应该在线程代码边界之内(run方法内)进行try catch并处理掉。换句话说,我们不能捕获从线程中逃逸的异常。
2)JVM捕获的线程抛出的异常:
dispatchUncaughtException方法,此方法就是用来处理线程中抛出的异常的。JVM会调用dispatchUncaughtException方法来寻找异常处理器(UncaughtExceptionHandler),处理异常。默认的未捕获异常处理器处理时,会调用 System.err 进行输出,也就是直接打印到控制台了。
3) 自定义处理异常:
Thead提供了一个setUncaughtExceptionHandler方法,我们只需要将自定义未捕获异常处理器作为参数传入进入就可以了。
//自定义未捕获异常处理器 thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("尝试捕获线程抛出的异常!"); } })
八、死锁的解决方案
死锁:两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
(1). 最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程;
(2). 撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。
这时又分两种情况:
一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。
一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素;
(3). 进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。