深入浅出Java多线程

系列文章目录



前言

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。
在这里插入图片描述


一、多线程基础概念介绍

进程是程序(任务)的执行过程,它持有资源(共享内存,共享文件)和线程。

分析:

① 执行过程 是动态性的,你放在电脑磁盘上的某个eclipse或者QQ文件并不是我们的进程,只有当你双击运行可执行文件,使eclipse或者QQ运行之后,这才称为进程。它是一个执行过程,是一个动态的概念。

② 它持有资源(共享内存,共享文件)和线程:我们说进程是资源的载体,也是线程的载体。这里的资源可以理解为内存。我们知道程序是要从内存中读取数据进行运行的,所以每个进程获得执行的时候会被分配一个内存。

③ 线程是什么?
在这里插入图片描述
如果我们把进程比作一个班级,那么班级中的每个学生可以将它视作一个线程。学生是班级中的最小单元,构成了班级中的最小单位。一个班级有可以多个学生,这些学生都使用共同的桌椅、书籍以及黑板等等进行学习和生活。

在这个意义上我们说:

线程是系统中最小的执行单元;同一进程中可以有多个线程;线程共享进程的资源。

④ 线程是如何交互?

就如同一个班级中的多个学生一样,我们说多个线程需要通信才能正确的工作,这种通信,我们称作线程的交互。

⑤ 交互的方式:互斥、同步

类比班级,就是在同一班级之内,同学之间通过相互的协作才能完成某些任务,有时这种协作是需要竞争的,比如学习,班级之内公共的学习资料是有限的,爱学习的同学需要抢占它,需要竞争,当一个同学使用完了之后另一个同学才可以使用;如果一个同学正在使用,那么其他新来的同学只能等待;另一方面需要同步协作,就好比班级六一需要排演节目,同学需要齐心协力相互配合才能将节目演好,这就是进程交互。

##一个线程的生命周期
线程经过其生命周期的各个阶段。下图显示了一个线程完整的生命周期。
在这里插入图片描述
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。

死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程的状态转换图

在这里插入图片描述
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程的调度

1、调整线程优先级:

每一个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。

4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。

7、常见线程名词解释
主线程:JVM调用程序main()所产生的线程。
当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。
前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。

一些常见问题

1、线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是main,非主线程的名字不确定。
2、线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。
3、获取当前线程的对象的方法是:Thread.currentThread();
4、每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
5、当线程目标run()方法结束时该线程完成。
6、一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。
7、线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。
众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。
8、尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列唱呢个一个队列的事实。
9、尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。

二、Java 中线程的常用方法介绍

Java语言对线程的支持

  主要体现在Thread类和Runnable接口上,都继承于java.lang包。它们都有个共同的方法:public void run()。

run方法为我们提供了线程实际工作执行的代码。

下表列出了Thread类的一些重要方法:
在这里插入图片描述
测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

在这里插入图片描述

Thread常用的方法

在这里插入图片描述

三、线程初体验(编码示例)

创建线程的方法有两种:

1.继承Thread类本身

2.实现Runnable接口

线程中的方法比较有特点,比如:启动(start),休眠(sleep),停止等,多个线程是交互执行的(cpu在某个时刻。只能执行一个线程,当一个线程休眠了或者执行完毕了,另一个线程才能占用cpu来执行)因为这是cpu的结构来决定的,在某个时刻cpu只能执行一个线程,不过速度相当快,对于人来将可以认为是并行执行的。

在一个java文件中,可以有多个类(此处说的是外部类),但只能有一个public类。

这两种创建线程的方法本质没有任何的不同,一个是实现Runnable接口,一个是继承Thread类。

使用实现Runnable接口这种方法:

1.可以避免java的单继承的特性带来的局限性;

2.适合多个相同程序的代码去处理同一个资源情况,把线程同程序的代码及数据有效的分离,较好的体现了面向对象的设计思想。开发中大多数情况下都使用实现Runnable接口这种方法创建线程。

实现Runnable接口创建的线程最终还是要通过将自身实例作为参数传递给Thread然后执行

语法: Thread actress=new Thread(Runnable target ,String name);

例如: Thread actressThread=new Thread(new Actress(),“Ms.runnable”);

actressThread.start();

代码示例:

package com.study.thread;

public class Actor extends Thread{
    public void run() {
        System.out.println(getName() + "是一个演员!");
        int count = 0;
        boolean keepRunning = true;

        while(keepRunning){
            System.out.println(getName()+"登台演出:"+ (++count));
            if(count == 100){
                keepRunning = false;
            }
            if(count%10== 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(getName() + "的演出结束了!");
    }

    public static void main(String[] args) {
       Thread actor = new Actor();//向上转型:子类转型为父类,子类对象就会遗失和父类不同的方法。向上转型符合Java提倡的面向抽象编程思想,还可以减轻编程工作量
       actor.setName("Mr. Thread");
       actor.start();

       //调用Thread的构造函数Thread(Runnable target, String name)
       Thread actressThread = new Thread(new Actress(), "Ms. Runnable");
       actressThread.start();
    }

}
//注意:在“xx.java”文件中可以有多个类,但是只能有一个Public类。这里所说的不是内部类,都是一个个独立的外部类
class Actress implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "是一个演员!");//Runnable没有getName()方法,需要通过线程的currentThread()方法获得线程名称
        int count = 0;
        boolean keepRunning = true;

        while(keepRunning){
            System.out.println(Thread.currentThread().getName()+"登台演出:"+ (++count));
            if(count == 100){
                keepRunning = false;
            }
            if(count%10== 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "的演出结束了!");
    }

}

/**
 *运行结果Mr. Thread线程和Ms. Runnable线程是交替执行的情况
 *分析:计算机CPU处理器在同一时间同一个处理器同一个核只能运行一条线程,
 *当一条线程休眠之后,另外一个线程才获得处理器时间
 */

运行结果:
在这里插入图片描述
示例2:

ArmyRunnable 类:

package com.study.threadTest1;

/**
 * 军队线程
 * 模拟作战双方的行为
 */
public class ArmyRunnable implements Runnable {

    /* volatile关键字
     * volatile保证了线程可以正确的读取其他线程写入的值
     * 如果不写成volatile,由于可见性的问题,当前线程有可能不能读到这个值
     * 关于可见性的问题可以参考JMM(Java内存模型),里面讲述了:happens-before原则、可见性
     * 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的值
     */
    volatile boolean keepRunning = true;

    @Override
    public void run() {
        while (keepRunning) {
            //发动5连击
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"进攻对方["+i+"]");
                //让出了处理器时间,下次该谁进攻还不一定呢!
                Thread.yield();//yield()当前运行线程释放处理器资源
            }
        }
        System.out.println(Thread.currentThread().getName()+"结束了战斗!");
    }

}

KeyPersonThread 类:

package com.study.threadTest1;


public class KeyPersonThread extends Thread {
    public void run(){
        System.out.println(Thread.currentThread().getName()+"开始了战斗!");
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"左突右杀,攻击隋军...");
        }
        System.out.println(Thread.currentThread().getName()+"结束了战斗!");
    }

}

Stage 类:

1 package com.study.threadTest1;
 2 
 3 /**
 4  * 隋唐演义大戏舞台 6  */
 7 public class Stage extends Thread {
 8     public void run(){
 9         System.out.println("欢迎观看隋唐演义");
10         //让观众们安静片刻,等待大戏上演
11         try {
12             Thread.sleep(5000);
13         } catch (InterruptedException e1) {
14             e1.printStackTrace();
15         }
16         System.out.println("大幕徐徐拉开");
17 
18         try {
19             Thread.sleep(5000);
20         } catch (InterruptedException e1) {
21             e1.printStackTrace();
22         }
23 
24         System.out.println("话说隋朝末年,隋军与农民起义军杀得昏天黑地...");
25         ArmyRunnable armyTaskOfSuiDynasty = new ArmyRunnable();
26         ArmyRunnable armyTaskOfRevolt = new ArmyRunnable();
27 
28         //使用Runnable接口创建线程
29         Thread  armyOfSuiDynasty = new Thread(armyTaskOfSuiDynasty,"隋军");
30         Thread  armyOfRevolt = new Thread(armyTaskOfRevolt,"农民起义军");
31 
32         //启动线程,让军队开始作战
33         armyOfSuiDynasty.start();
34         armyOfRevolt.start();
35 
36         //舞台线程休眠,大家专心观看军队厮杀
37         try {
38             Thread.sleep(50);
39         } catch (InterruptedException e) {
40             e.printStackTrace();
41         }
42 
43         System.out.println("正当双方激战正酣,半路杀出了个程咬金");
44 
45         Thread  mrCheng = new KeyPersonThread();
46         mrCheng.setName("程咬金");
47         System.out.println("程咬金的理想就是结束战争,使百姓安居乐业!");
48 
49         //停止军队作战
50         //停止线程的方法
51         armyTaskOfSuiDynasty.keepRunning = false;
52         armyTaskOfRevolt.keepRunning = false;
53 
54         try {
55             Thread.sleep(2000);
56         } catch (InterruptedException e) {
57             e.printStackTrace();
58         }
59         /*
60          * 历史大戏留给关键人物
61          */
62         mrCheng.start();
63 
64         //万众瞩目,所有线程等待程先生完成历史使命
65         try {
66             mrCheng.join();//join()使其他线程等待当前线程终止
67         } catch (InterruptedException e) {
68             e.printStackTrace();
69         }
70         System.out.println("战争结束,人民安居乐业,程先生实现了积极的人生梦想,为人民作出了贡献!");
71         System.out.println("谢谢观看隋唐演义,再见!");
72     }
73 
74     public static void main(String[] args) {
75         new Stage().start();
76     }
77 
78 }

运行结果:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java毕设王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值