第一步:细说多线程之Thread VS Runnable
https://www.imooc.com/learn/312
第二步:Java Socket应用
https://www.imooc.com/learn/161
第三步:Java高并发之魂:synchronized深度解析
https://www.imooc.com/learn/1086
第四步:Java多线程之内存可见性
https://www.imooc.com/learn/352
一、 线程的实现方式
Java多线程的4种实现方式
- 1.继承Thread并重写run方法,并调用start方法
/**
* Java实现多线程的方式1
* 继承Thread类,重写run方法
*/
class MyThread extends Thread {
@Override
public void run() {
//此处为thread执行的任务内容
System.out.println(Thread.currentThread().getName());
}
}
public class Demo03 {
public static void main(String[] args) {
for(int i=0;i<2;i++) {
Thread t = new MyThread();
//输出:
//Thread-0 Thread-1
t.start();
}
}
}
- 2.实现Runnable接口,并用其初始化Thread,然后创建Thread实例,并调用start方法
/**
* Java实现多线程的方式2
* 实现Runnable接口
*/
class MyThread implements Runnable {
@Override
public void run() {
//此处为thread执行的任务内容
System.out.println(Thread.currentThread().getName());
}
}
public class Demo03 {
public static void main(String[] args) {
for(int i=0;i<2;i++) {
Thread t = new Thread(new MyThread());
//输出:
//Thread-0 Thread-1
t.start();
}
}
}
- 3.实现Callable接口,并用其初始化Thread,然后创建Thread实例,并调用start方法
/**
* Java实现多线程的方式3
* 实现Callable接口
*/
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return null;
}
}
public class Demo03 {
public static void main(String[] args) {
for(int i=0;i<2;i++) {
//创建MyThread实例
Callable<Integer> c = new MyThread();
//获取FutureTask
FutureTask<Integer> ft = new FutureTask<Integer>(c);
//使用FutureTask初始化Thread
Thread t = new Thread(ft);
//输出:
//Thread-0 Thread-1
t.start();
}
}
}
- 4.使用线程池创建
/**
* Java实现多线程的方式4
* 线程池
*/
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 0;
}
}
public class Demo03 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i=0;i<2;i++) {
executorService.execute(new MyThread());
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread2());
//输出
// pool-1-thread-1
// pool-1-thread-2
// pool-1-thread-3
// pool-1-thread-4
executorService.submit(ft);
}
executorService.shutdown();
}
}
二、 正确启动线程的方式
启动线程的三种方式
- 1)、继承Thread类:
- a.定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程
- b. 需要完成的任务。因此把run方法称为线程执行体。
创建Thread子类的实例,即创建了线程对象。 - c.调用线程对象的start()方法来启动该线程。
- 2)、实现Runnable接口:
a.定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体
b.创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。
c.调用线程对象的start()方法来启动该线程。
- 3)、匿名内部类:
匿名内部类本质上也是一个类实现了Runnable接口,重写了run方法,只不过这个类没有名字,直接作为参数传入Thread类,示例代码:
public class Main {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "执行" + i);
}
}
}).start();
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "执行" + i);
}
}
}
三、 如何停止线程
使用标志位
//定义一个标志位:cancelled,cancelled为true是即run方法结束,反之继续while里面的任务。通过cancell方法来停止任务。
static class CancelledTaggedRunnnable implements Runnable{
private volatile boolean cancelled = false;
@Override
public void run() {
while(!cancelled){
//没有停止需要做什么操作
}
//线程停止后需要干什么
System.out.println("任务结束了");
}
public void cancell(){
cancelled = true;
}
}
@Test
public void testCancelledTaggedRunnnable() throws InterruptedException {
CancelledTaggedRunnnable taggedRunnnable = new CancelledTaggedRunnnable();
Thread thread = new Thread(taggedRunnnable);
thread.start();
System.err.println(thread.isAlive());
Thread.sleep(1000);
taggedRunnnable.cancell();
Thread.sleep(1000);
System.err.println(thread.isAlive());
Thread.sleep(1000);
System.err.println(thread.isAlive());
}
强行停止
对于多线程,使用中断策略 较为优雅,也是官方的推荐。线程停止前你应该做一些操作,从而保证该线程运行后的数据不会被丢失,这一点在多线程中极为重要。
但是对于中断策略还是有一个很大的缺陷,那就是,必须通过中断的阻塞函数,如:我使用的BlockingQueue的put方法,也可以是Thread.sleep()方法,才能抛出InterruptedException。如果抛不出这样的异常呢?
对于单线程,还有一个简单粗暴的方式,那就是Java已经不再使用的Thread stop方法。
使用方法即直接调用:thread.stop()即可。
如果你对该线程的操作不再关心了,对结果也不再在意了,使用该方法也是可以的。
但多线程情况下,或者线程带锁的情况下 那就要慎用了。该方法不安全
四、 如何中断线程
现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线程呢?答案就是使用 interrupt() 方法来中断线程。
需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。
public class InterruptThread1 extends Thread{
public static void main(String[] args) {
try {
InterruptThread1 t = new InterruptThread1();
t.start();
Thread.sleep(200);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
System.out.println("i=" + i);
}
}
}
从输出的结果我们会发现 interrupt 方法并没有停止线程 t 中的处理逻辑,也就是说即使 t 线程被设置为了中断状态,但是这个中断并不会起作用,那么该如何停止线程呢?
这就需要使用到另外两个与线程中断有关的方法了:
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
这两个方法使得当前线程能够感知到是否被中断了(通过检查标志位)。
所以如果希望线程 t 在中断后停止,就必须先判断是否被中断,并为它增加相应的中断处理代码:
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
//判断是否被中断
if(Thread.currentThread().isInterrupted()){
//处理中断逻辑
break;
}
System.out.println("i=" + i);
}
}
输出结果,for 循环在执行完成前就提前结束了:
在上面这段代码中,我们增加了 Thread.isInterrupted() 来判断当前线程是否被中断了,如果是,则退出 for 循环,结束线程。
这种方式看起来与之前介绍的“使用标志位终止线程”非常类似,但是在遇到 sleep() 或者 wait() 这样的操作,我们只能通过中断来处理了。
public static native void sleep(long millis) throws InterruptedException
Thread.sleep() 方法会抛出一个 InterruptedException 异常,当线程被 sleep() 休眠时,如果被中断,这会就抛出这个异常。
(注意:Thread.sleep() 方法由于中断而抛出的异常,是会清除中断标记的。)
五、 线程的生命周期
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
- 新建:就是刚使用new方法,new出来的线程;
- 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
- 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
-
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
完整的生命周期图如下:
六、 notify、join、yield的方法说明
- notify: 在执行notify方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。
- join: 在B线程里面写threadA.join( ),B挂起,让A运行,看起来B很礼貌让A运行了,但其实B很虚伪,让别人运行又不释放锁。
class Demo implements Runnable
{
public void run()
{
for(int x=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"...."+x);
}
}
}
//将执行join()方法的线程终止,释放其CPU执行权和执行资格,等待join()方法所属的线程执行完成后,才能重新获取CPU执行资格
class JoinDemo
{
public static void main(String[] args)throws Exception
{
Demo st=new Demo();
Thread t1=new Thread(st);
Thread t2=new Thread(st);
t1.start();
t1.join(); //main 线程终止,等待t1线程执行完成才继续执行。
t2.start();
//t1.join();//main 线程终止,t1和t2抢夺CPU执行权,main线程等待t1线程执行完成才继续执行。
for(int x=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"...."+x);
}
System.out.println("over");
}
}
- yield: 方法:使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里随机选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的。也就是从Running变为Ready,一直在Runnable里面。
class Demo implements Runnable
{
public void run()
{
for(int i=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"...."+x);
Thread.yield();//释放CPU执行权,注意:释放CPU执行权,不代表它自己不能再次获取CPU执行权
}
}
}
七、 线程的异常处理
方法一:子线程中try... catch...
public class ChildThread implements Runnable {
public void run() {
doSomething1();
try {
// 可能发生异常的方法
exceptionMethod();
} catch (Exception e) {
// 处理异常
System.out.println(String.format("handle exception in child thread. %s", e));
}
doSomething2();
}
}
方法二:为线程设置“未捕获异常处理器”UncaughtExceptionHandler
为线程设置异常处理器。具体做法可以是以下几种:
(1)Thread.setUncaughtExceptionHandler设置当前线程的异常处理器;
(2)Thread.setDefaultUncaughtExceptionHandler为整个程序设置默认的异常处理器;
如果当前线程有异常处理器(默认没有),则优先使用该UncaughtExceptionHandler类;否则,如果当前线程所属的线程组有异常处理器,则使用线程组的
UncaughtExceptionHandler;否则,使用全局默认的DefaultUncaughtExceptionHandler;如果都没有的话,子线程就会退出。
注意:子线程中发生了异常,如果没有任何类来接手处理的话,是会直接退出的,而不会记录任何日志。
所以,如果什么都不做的话,是会出现子线程任务既没执行成功,也没有任何日志提示的“诡异”现象的。
设置当前线程的异常处理器:
public class ChildThread implements Runnable {
private static ChildThreadExceptionHandler exceptionHandler;
static {
exceptionHandler = new ChildThreadExceptionHandler();
}
public void run() {
Thread.currentThread().setUncaughtExceptionHandler(exceptionHandler);
System.out.println("do something 1");
exceptionMethod();
System.out.println("do something 2");
}
public static class ChildThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(String.format("handle exception in child thread. %s", e));
}
}
}
或者,设置所有线程的默认异常处理器
public class ChildThread implements Runnable {
private static ChildThreadExceptionHandler exceptionHandler;
static {
exceptionHandler = new ChildThreadExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
}
public void run() {
System.out.println("do something 1");
exceptionMethod();
System.out.println("do something 2");
}
private void exceptionMethod() {
throw new RuntimeException("ChildThread exception");
}
public static class ChildThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(String.format("handle exception in child thread. %s", e));
}
}
}
命令行输出:
do something 1
handle exception in child thread. java.lang.RuntimeException: ChildThread exception
方法三:通过Future的get方法捕获异常(推荐)
使用线程池提交一个能获取到返回信息的方法,也就是ExecutorService.submit(Callable)
在submit之后可以获得一个线程执行结果的Future对象,而如果子线程中发生了异常,通过future.get()获取返回值时,可以捕获到
ExecutionException异常,从而知道子线程中发生了异常。
子线程代码:
public class ChildThread implements Callable<String> {
public String call() throws Exception {
System.out.println("do something 1");
exceptionMethod();
System.out.println("do something 2");
return "test result";
}
private void exceptionMethod() {
throw new RuntimeException("ChildThread1 exception");
}
}
父线程代码:
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(8);
Future future = executorService.submit(new ChildThread());
try {
future.get();
} catch (InterruptedException e) {
System.out.println(String.format("handle exception in child thread. %s", e));
} catch (ExecutionException e) {
System.out.println(String.format("handle exception in child thread. %s", e));
} finally {
if (executorService != null) {
executorService.shutdown();
}
}
}
}
命令行输出:
do something 1
handle exception in child thread. java.util.concurrent.ExecutionException: java.lang.RuntimeException: ChildThread1 exception
八、 死锁的解决方案
java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。
解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现,
而如果不恰当的使用了锁,且出现同时要锁多个对象时,会出现死锁情况。
死锁是两个甚至多个线程被永久阻塞时的一种运行局面,这种局面的生成伴随着至少两个线程和两个或者多个资源。
避免死锁方针:
a:避免嵌套封锁:这是死锁最主要的原因的,如果你已经有一个资源了就要避免封锁另一个资源。如果你运行时只有一个对象封锁,那是几乎不可能出现一个死锁局面的。
b:只对有请求的进行封锁:你应当只想你要运行的资源获取封锁.如果我们只对它所属领域中的一个感兴趣,那我们应当封锁住那个特殊的领域而并非完全的对象。
c:避免无限期的等待:如果两个线程正在等待对象结束,无限期的使用线程加入,如果你的线程必须要等待另一个线程的结束,若是等待进程的结束加入最好准备最长时间。