了解java多线程

前言

今天我进一步了解了java多线程的相关知识,根据几个相关问题进行学习并作出了解答,这篇文章将用来记录我学习到的有关知识。

问题列表

  1. 怎么实现多线程?
  2. 正确启动线程的方式
  3. 线程中常用的方法说明
  4. 如何停止线程?
  5. 如和中断线程?
  6. 线程的生命周期
  7. 线程的异常处理
  8. 死锁的解决方案

1. 怎么实现多线程?

实现多线程的方法有两种:

  1. 继承Thread类、并重写run方法。
  2. 实现Runnable接口、并重写run方法。

关于这两种方法有一个比较,如图:

2. 正确启动线程的方式

  1. 如果线程类继承了Thread类,那么在主方法中可以之间创建该线程类对象,使该对象调用start()方法即可启动线程。
  2. 如果线程类实现了Runnable接口,那么在主方法中同样需要创建该线程类对象,然后作为Thread的构造方法的参数创建一个线程,再调用start()方法即可启动线程。

现在对刚刚上面的两个问题进行总结:


实现线程的两种方式.png

3. 线程中常用的方法说明

sleep方法:
当在当前线程中调用Thread.sleep(long millis)方法时,当前线程会进入阻塞状态。millis参数指定了线程睡眠的时间,单位是毫秒。 当时间结束之后,线程会重新进入就绪状态。
代码演示:

public class SleepTest extends Thread {
    static int i=0;
    public void run(){
        while(++i < 5) {
            System.out.println(Thread.currentThread().getName()+ "输出" + i);
            try {
                Thread.sleep(1000);     //间隔一秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        SleepTest t = new SleepTest();
        t.start();
    }
}

运行结果:

结果

结果的输出是由间隔时间的。

join方法:
它是Thread的实例方法,在当前线程里的另外一个线程调用了join()方法时,当前线程会进入阻塞状态等待这个调用了join()方法的线程执行完毕后,再由阻塞状态进入到就绪状态。
代码演示:

public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new app("t1"));
        Thread t2 = new Thread(new app("t2"));
        Thread t3 = new Thread(new app("t3"));
        t1.start();
        t1.join();   //这里会等待t1线程执行完毕才会开启t2线程
        
        t2.start();  //同上
        t2.join();
        
        t3.start();
        t3.join();
    }
    
    static class app implements Runnable{
        int i = 0;
        String name;
        public app(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            while(i < 5) {
                System.out.println(name + "输出" + i++);
            }
        }
    }
}

运行结果:

结果

yield方法:
它是Thread的静态方法,如果在当前的线程中写了“Thread.yield”,那么就会使当前线程主动放弃cpu资源,进入就绪状态,让其它的线程来获取cpu资源,也有可能当前的线程还会获取cpu资源。
代码演示:

public class YieldTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new app("t1"));
        Thread t2 = new Thread(new app("t2"));
        
        t1.start();
        t2.start();
    }
    
    static class app implements Runnable{
        int i = 0;
        String name;
        public app(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            while(i < 8){
                if(i == 5) {
                    Thread.yield();
                    System.out.println(name+"放弃cpu资源");
                }
                System.out.println(name+"输出了"+i);
                i++;
            }
        }
    }
}

运行结果:

结果

wait、notify/notifyAll方法:
它们是Object的方法,需要用到synchronized关键字,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
1.wait()使当前线程阻塞,前提是必须先获得锁,当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
2.只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait(),再次释放锁。
代码演示:

public class LockTest {
    
    static Object lock = new Object();
    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new app1("t1"));
        Thread t2 = new Thread(new app2("t2"));
        t2.start();
        Thread.sleep(3000);
        t1.start();
    }
    static class app1 implements Runnable{
        String name;
        public app1(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            synchronized(lock) {
                while(++i < 10) {
                    if(i == 5) {
                        System.out.println(name+"线程唤醒其他线程");
                        lock.notify();
                    }
                    System.out.println(name+"输出"+i);
                }
            }
        }
    }
    static class app2 implements Runnable{
        String name;
        public app2(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            synchronized(lock){
                if(i != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(name+"线程被唤醒");
                System.out.println(name+"输出"+i);
            }
        }
    }
}

这段代码里面有两个线程,让t1线程一直输出直到i等于5的时候,唤醒被阻塞的t2线程,但唤醒t2线程不代表释放锁,会先等待t1线程执行完毕后才会执行t2线程。
运行结果:

结果

4. 如何停止线程?

首先,stop()方法不推荐使用,因为stop()会让线程戛然而止,使得在实际开发中不能知道线程还有哪些工作还没做、完成了哪些工作,还有我们不能够去做一些应有的清理工作。

正确的停止线程方法:设置退出标志

public class StopThread extends Thread{

    public void run() {
        test01 t = new test01();
        Thread thread = new Thread(t);
        //开始线程
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //停止线程
        t.flag = false;
    }
    public static void main(String[] args) {
        Thread stopThread = new Thread(new StopThread());
        stopThread.start();
    }
    
    static class test01 implements Runnable{

        //设置线程停止的标志
        //volatile保证了线程可以正确的读取其他线程写入的值
        volatile boolean flag = true;

        @Override
        public void run() {
            System.out.println("线程开始.......");
            while(flag) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程正在进行.....");
            }
            System.out.println("线程停止.......");
        }   
    }
}

运行结果:

结果

5. 如何中断线程?

调用interrupt()方法:
当线程调用了某些方法,比如sleep()方法而进入一种阻塞状态,此时该线程如果调用了interrupt()方法,会产生两个结果:1.线程的中断状态被清除(退出阻塞状态)2.会抛出一个InterruptedException异常,表明线程被中断了。
代码演示:

public class InterruptThread extends Thread{

    public void run() {
        while(true) {
            System.out.println("线程正在进行.....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("中断阻塞状态");
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Thread stopThread = new Thread(new InterruptThread());
        //开始线程
        System.out.println("线程开始.......");
        stopThread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断线程
        stopThread.interrupt();
    }
}

运行结果:

结果

虽然主线程已经执行结束,但stopThread 线程却还会继续进行,这里只是为了演示interrupt()方法中断阻塞状态的功能。

方法总结

6. 线程的生命周期

线程的生命周期

相关解释:
1.就绪状态:创建了线程对象后,调用了线程的start()方法(注意此时线程只是进入了线程队列,等待获取cpu服务,具备了运行条件,但并不一定已经开始运行)。
2.运行状态:处于就绪状态的线程,一旦获取了cpu资源,便进入到运行状态,开始执行run()方法里面的逻辑。
3.终止状态:线程的run()方法执行完毕,或者线程调用了stop()方法(此方法已过时,不推荐使用),线程便进入终止状态。
4.阻塞状态:一个正在执行的线程在某种情况下,由于某种原因(比如线程调用了sleep()方法、wait()方法、join()方法)而暂时让出了cpu资源,暂停了自己的执行,便进入到阻塞状态。

7. 线程的异常处理

关于线程的异常处理我是这么理解的,每一个线程在运行的时候都是相互独立的,那么它出现的异常应该由各自的线程自己来处理。

下面尝试不在线程中处理异常,尝试在主线程中处理异常:

public class EpTest implements Runnable{

    public static void main(String[] args) {
        EpTest instance = new EpTest();
        try{
            Thread t1 = new Thread(instance);
            t1.start();
        }catch (Exception e){
            System.out.println("主线程捕获线程中的异常");
        }
    }
    @Override
    public void run() {
            throw new RuntimeException();
    }
}
Exception in thread "Thread-0" java.lang.RuntimeException
    at EpTest.run(EpTest.java:19)
    at java.base/java.lang.Thread.run(Thread.java:834)

结果显示此时这个主线程并不能处理该异常
那么处理线程中出现的异常有以下我学习到的几种方法:

  1. 直接在线程中使用try、cath语句处理异常。
 @Override
    public void run() {
        try{
            ...
        }catch (Exception e){
            ...
        }
    }
  1. 对于不在线程中处理的异常,可以交给异常处理器处理UncaughtExceptionHandler
public class EpTest implements Runnable{

    public static void main(String[] args) {
        EpTest instance = new EpTest();
        Thread t1 = new Thread(instance);
        t1.setUncaughtExceptionHandler(new EpHandler());
        t1.start();
    }
    @Override
    public void run() {
            throw new RuntimeException();
    }
}
class EpHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t,Throwable e){
        System.out.println("捕获未处理的异常");
    }
}

首先需要自定义一个异常处理器:

class EpHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t,Throwable e){
        System.out.println("捕获未处理的异常");
    }
}

接着在主方法中设置这个异常处理器:

public static void main(String[] args) {
        EpTest instance = new EpTest();
        Thread t1 = new Thread(instance);
        //调用Thread类的setUncaughtExceptionHandler()设置异常处理器
        t1.setUncaughtExceptionHandler(new EpHandler());
        t1.start();
    }

3.使用线程池

public class EpTest2 implements Runnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new EpTest2());
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(t1);
        executorService.shutdown();
    }

    @Override
    public void run() {
        //在run方法中设置异常处理器
        Thread.currentThread().setUncaughtExceptionHandler(new EpHandler());
        throw new RuntimeException();
    }
}

需要注意的是,只有通过execute()方法提交任务,才能将它抛出的异常交给未捕获异常处理器。

另外的一种方法:

public class EpTest2 implements Runnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new EpTest2());
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<?> future=executorService.submit(t1);
        executorService.shutdown();
        try {
            //抛出异常
            future.get();
        }catch (Exception e){
            System.out.println("捕获未处理的异常");
        }
    }

    @Override
    public void run() {
        throw new RuntimeException();
    }
}

这种方法通过submit()方法提交的任务由于抛出了异常而结束,该异常将被Future.get()封装在ExecutionException中重新抛出。

8. 死锁的解决方案

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

产生死锁的必要条件:

(1)互斥条件:一个资源每次只能被一个线程使用,直到被该进程释放。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

解决死锁的问题:

  1. 保证加锁的顺序(按照一定的顺序给线程加锁)
  2. 设置加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

相关教程:
细说多线程之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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,949评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,438评论 1 15
  • 林炳文Evankaka原创作品。转载自http://blog.csdn.net/evankaka 本文主要讲了ja...
    ccq_inori阅读 645评论 0 4
  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 871评论 0 1
  • 一扩展javalangThread类二实现javalangRunnable接口三Thread和Runnable的区...
    和帅_db6a阅读 485评论 0 1