Java笔记 —— 多线程

进程与线程
  1. 进程:
    正在运行的程序,是系统进行资源分配和调用的独立单位。
    每一个进程都有它自己的内存空间和系统资源
  2. 线程:
    是进程中的单个顺序控制流,或者说就是一个单独执行路径
    一个进程如果只有一条执行路径,称之为单线程程序。一个进程如果有多条执行路径,称之为多线程程序。
    线程是包含在进程中的。
并行与并发

并行应该是物理上可以同时发生,两个或多个事件在同一时刻发生。比喻是电话来了,放下筷子打电话,打完电话再吃饭。因为只有一个人只有一张嘴,无法同时打电话和吃饭,所以需要两张嘴才能做到同时打电话和吃饭。强调是同一时刻互不干扰的同时执行。

并发是逻辑上同时发生,两个或多个事件在同一时间间隔内内发生。比喻是一边打电话一边吃饭,用很小的时间间隔吃一口饭,说一句话,两者交替发生,宏观上好像是同时发生,但实际不是。强调的是利用小的时间段交替的完成。

Java程序的运行原理:

java 命令会启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。
该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。也就是说,像前面学习java的过程中,写的程序其实都是单线程的。

而JVM本身启动的时候是多线程,因为JVM至少有主线程和垃圾回收线程两个线程。

线程调度

假设计算机只有一个 CPU, CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么具体应该是哪个线程得到CPU呢,这就牵扯到了线程调度问题。
线程有两种调度模型:

  1. 分时调度模型 :所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  2. 抢占式调度模型 :优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。 (这里的优先并不保证一定能抢到CPU,只是可能性更大一些)
多线程的实现
实现方式一、继承Thread类,并重写run()方法
package test.MyThread;

public class MyThreadDemo1 extends Thread {
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(i);
        }
    }
}

package test.MyThread;

public class Demo1 {
    public static void main(String[] args) {
        //创建两个线程m1和m2
        MyThreadDemo1 m1 = new MyThreadDemo1();
        MyThreadDemo1 m2 = new MyThreadDemo1();

        //start()方法的调用,首先单独启动了一个线程,然后再由JVM去调用该线程的run()方法
        m1.start();
        m2.start();
    }

}

结果是从0打印到10 ,而且连续打印两次

这里需要注意的是:

  1. 不能直接用线程对象调用run方法,直接调用run方法,就相当于普通的调用方法,与多线程无关。要想看到多线程的效果,就必须用start()来启动
  2. 调用run()与调用start()的区别
    run()的调用仅仅是封装了被线程执行的代码,但是直接调用的话是普通的方法调用
    start()方法的调用,首先单独启动了一个线程,然后再由JVM去调用该线程的run()方法
  3. 线程启动调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体
  4. 同一个线程不能被start()启动两次,如果这样做会报错java.lang.IllegalThreadStateException
获取和设置线程名字
  1. 通过构造方法给线程起名字:
    Thread(String name) 分配一个新的 Thread对象。
  2. 通过方法给线程起名字:
    void setName(String name) 将此线程的名称更改为等于参数 name
package test.MyThread;

public class MyThreadDemo2 extends Thread{
//通过构造方法给线程起名字
    public MyThreadDemo2(){
        super();
    }

    public MyThreadDemo2(String name){
        super(name);
    }

    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(getName()+"---"+i);
        }
    }
}

package test.MyThread;

public class Demo2 {
    public static void main(String[] args) {
        //方式一、通过构造方法给线程起名字
        MyThreadDemo2 m1 = new MyThreadDemo2("张");
        MyThreadDemo2 m2 = new MyThreadDemo2("陈");

        m1.start();
        m2.start();

        //方式二、通过方法给线程起名字
        MyThreadDemo2 m3 = new MyThreadDemo2();
        MyThreadDemo2 m4 = new MyThreadDemo2();
        MyThreadDemo2 m5 = new MyThreadDemo2();

        m3.setName("杜");
        m4.setName("周");

        m3.start();
        m4.start();
        m5.start();

        //public static Thread currentThread()返回对当前正在执行的线程对象的引用
        //通过currentThread()获取当前的线程对象,再通过getName()获取线程名字
        //拿这个程序的main方法的主线程举例
        System.out.println(Thread.currentThread().getName());

    }
}

结果太长,只截取部分图片
在这里插入图片描述

从结果分析线程抢占cpu的过程

通过这个图片就可以看出来,线程之间是以抢夺的方式来占据cpu的,只有占据了cpu的线程才可以运行。因此当线程m1占用cpu成功后,运行run()方法的方法体,打印了输出语句“张—0”。而所有线程是交错运行的,每个线程只能占据cpu的一小段时间,这样看起来是一起运行,实际上不是。因此线程m1运行结束后,线程m3抢占了cpu,打印了语句“杜—0”。

另外这里面还夹着一个字符串 “main”,这是 System.out.println(Thread.currentThread().getName());语句的输出结果

图片的下面是Thread-2,这个是线程对象m5的名字,我故意没有给m5赋名,这里就可以看出来,如果自己不命名,则系统会自动给一个名字。

实现方式二、实现Runnable接口

1、创建自定义类实现Runnable接口
2、重写run()方法
3、创建自定义类的对象
4、创建Thread类的对象,将第三步创建的自定义类对象作为参数传递到构造方法中

package test.MyThread;

public class MyRunnable implements Runnable {
    @Override
    public void run(){
        for(int i=0;i<100;i++){
           
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

关于Thread.currentThread().getName()这条语句的解释:
由于实现的Runnable接口中并没有getName()方法,所以这里无法使用Thread类中的getName()方法
此时如果我们获取当前线程的名字,就需要间接调用,即通过currentThread()获取当前正在执行run()方法的线程对象,再通过getName()获取线程名字
currentThread()获得的线程对象是Thread类型,因此可以用getName()方法

package test.MyThread;

public class Demo3 {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        t1.setName("张");
        t2.setName("陈");
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

两种创建方式的对比
  1. 通过Runnable接口实现的类可以继承其他类,继承Thread的类不行。可以避免由于Java单继承带来的局限性。
  2. Runnable适合多个相同程序的代码去处理同一个资源的情况,比如
package test.MyThread;

public class RunnableThread implements Runnable{
    int num = 100;
    @Override
    public void run(){
        while(true){
            if(num>0){
                System.out.println(Thread.currentThread().getName()+"---"+(num--));
            }
        }
    }

}

package test.MyThread;

public class Test1 {
    public static void main(String[] args) {
        RunnableThread rt = new RunnableThread();

        Thread t1 = new Thread(rt,"窗口一");
        Thread t2 = new Thread(rt,"窗口二");
        Thread t3 = new Thread(rt,"窗口三");

        t1.start();
        t2.start();
        t3.start();

    }
}

在这里插入图片描述
三个窗口,也就是三个线程是共用一个变量num的,即三个窗口卖票的总和是100(这里的num是票数),三个线程共用一个RunnableThread类的变量num

package test.MyThread;

public class ExtendsThread extends Thread {
    int num = 100;
    @Override
    public void run(){
        while(true){
            if(num>0){
                System.out.println(getName()+"---"+(num--));
            }
        }
    }
}

package test.MyThread;

public class Test1 {
    public static void main(String[] args) {
        ExtendsThread e1 = new ExtendsThread();
        ExtendsThread e2 = new ExtendsThread();
        ExtendsThread e3 = new ExtendsThread();

        e1.setName("窗口一");
        e2.setName("窗口二");
        e3.setName("窗口三");

        e1.start();
        e2.start();
        e3.start();

    }
}

在这里插入图片描述
这里可以看出来,三个线程,也就是三个窗口各有一百张票,总共三百张票。每个线程都有一个自己独自的ExtendsThread类的实例变量num

这里如果想让extends继承产生的线程也共用一个变量,就需要将int num = 100改为
public static int num = 100;

因此总结一下是:Runnable可以实现多个相同的程序代码的线程去共享同一个资源,而Thread虽然可以做到但不推荐

线程的优先级

获取线程的优先级
public final int getPriority()返回此线程的优先级。
设置线程的优先级
public final void setPriority(int newPriority)更改此线程的优先级, 参数newPriority的范围为1-10之间

注意:

  1. 线程的默认优先级为5
  2. 线程优先级的范围是1-10
  3. 线程优先级高仅仅表示的是获取CPU时间片的几率会高一些,但不代表优先级高的线程一定能抢到CPU
package test.MyThread;

public class MyPriority extends Thread {
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(getName()+"---"+i);
        }
    }
}

package test.MyThread;

public class Demo4 {
    public static void main(String[] args) {
        MyPriority m1 = new MyPriority();
        MyPriority m2 = new MyPriority();

        //获取m1和m2的优先级
        System.out.println(m1.getPriority());
        System.out.println(m2.getPriority());

        //设置m1和m2的优先级
        m1.setPriority(1);
        m2.setPriority(10);

        //启动线程
        m1.start();
        m2.start();
    }
}

在这里插入图片描述
可以看出来线程m2抢到CPU的概率高了很多

加入线程

加入线程:
public final void join():其他线程等待这个线程死亡
注意事项:
在线程设置为加入线程之前,先将该线程变为就绪状态,也就是调用start()方法

package test.MyThread;

public class JoinThread extends Thread{
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(getName()+"---"+i);
        }
    }
}

package test.MyThread;

public class Demo5 {
    public static void main(String[] args) {
        JoinThread j1 = new JoinThread();
        JoinThread j2 = new JoinThread();
        JoinThread j3 = new JoinThread();

        j1.setName("张");
        j2.setName("周");
        j3.setName("陈");

        j1.start();

        try {
            j1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        j2.start();
        j3.start();
    }
}

在这里插入图片描述
可以看出来,等到线程j1全部运行结束后,j2和j3才正常抢占cpu运行程序

线程休眠
package test.MyThread;

public class SleepThread extends Thread{
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(getName()+"---"+i);
            try {
                //sleep(long millis)millis是毫秒数,一秒等于1000毫秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package test.MyThread;

public class Demo6 {
    public static void main(String[] args) {
        SleepThread s1 = new SleepThread();
        SleepThread s2 = new SleepThread();
        SleepThread s3 = new SleepThread();

        s1.setName("张");
        s2.setName("周");
        s3.setName("陈");

        s1.start();
        s2.start();
        s3.start();
    }
}

在这里插入图片描述
实际在控制台打印结果的时候,按照
张—0
周—0
陈—0
这样三个为一组的顺序,每隔一秒打印一次,直至打印完毕,但是内部的顺序可以改变,比如
周—1
张—1
陈—1
因为三个进程每次抢占完CPU打印后都要休眠一秒,因此第一个进程休眠的时间内,足够其余的进程完成一次输出,进程m1打印完就休眠,不再抢夺CPU。接着m2打印完休眠,最后m3打印完休眠。不会出现m1刚刚打印完就立刻开始抢占CPU的情况。所以以三个为一组打印。但这三个进程休眠结束后,会再次抢占CPU,所以组内的顺序不一样。

线程生命周期图

在这里插入图片描述
线程Thread通过start()来启动线程,真正实现多线程运行,此时线程处于就绪状态。只有抢到了CPU后可以进入运行状态,Thread调用run()方法,完成线程的执行语句。

而当语句完成后就回进入死亡状态,之前的代码里面都是while循环,所以输出语句完成后还会继续循环,一直没有完成run方法。因此在运行主函数的时候会发现,程序不会结束,需要手动关闭。原因就在于这个while循环一直是true,线程一直没办法死亡,程序也就不会结束。

中断线程

public final void stop():让正在运行的线程停止。run方法剩下的代码不会执行,此方法已经过时弃用了
public void interrupt():中断正在运行的线程,被中断的线程会将run方法执行完毕,并抛出异常

正常执行的代码
import java.util.Date;

public class MyStopThread extends Thread {
    @Override
    public void run() {
        System.out.println("开始执行时间:" + new Date());

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("执行结束时间:" + new Date());
        System.out.println("test");
    }
}

package test.MyThread;

public class ThreadStopDemo {
    public static void main(String[] args) {
        MyStopThread t1 = new MyStopThread();

        t1.start();

    }
}


在这里插入图片描述

开始执行的时间和执行结束的时间,中间隔了十秒

中断执行的代码
package test.MyThread;

public class ThreadStopDemo {
    public static void main(String[] args) {
        MyStopThread t1 = new MyStopThread();

        t1.start();

        try {
            Thread.sleep(3000);
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
这里的Thread.sleep(3000);是指主线程,也就是调用main方法的主线程,暂停三秒。然后中断线程 t1的运行
线程 t1本来是休眠十秒然后结束,这里因为interrupt中断了t1的休眠,所以这里的结束执行时间与开始执行时间只隔了三秒

礼让线程

礼让线程:
public static void yield()
暂停当前正在执行的线程对象,并执行其他线程
它的作用是为了让多个线程之间更加和谐一点,并不能一定保证多个线程一人一次执行

package test.MyThread;

public class YieldThread extends Thread{
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(getName()+"---"+i);
        }
    }
}

package test.MyThread;

public class Demo8 {
    public static void main(String[] args) {
        YieldThread y1 = new YieldThread();
        YieldThread y2 = new YieldThread();

        y1.start();
        y2.start();
    }
}

在这里插入图片描述
类似于这样一人一句,进程占用CPU打印完后会礼让一下资源,当然礼让过后依然抢到了也是有可能的

后台线程(守护线程)

Java中有两类线程:用户线程、守护线程
用户线程:我们在学习多线程之前所有程序代码,运行起来都是一个个的用户线程。
守护线程:所谓的守护线程,指的就是程序运行的时候在后台提供了一种通用服务的线程。比如说
垃圾回收线程,就是一个守护线程,这种线程并不属于程序不可或缺的部分。所以非守护线程结束的时候,程序也就终止了,同时会杀死进所有的守护线程。反过来说,只要程序种存在非守护线程,程序就不会终止。

守护线程的设置方法:
public final void setDaemon(boolean on)
通过这个方法将该线程对象标记为守护线程或者非守护线程。
当运行的程序只有一个线程且是守护线程的时候,Java虚拟机退出

注意:将线程设置为守护线程这一步骤,必须在启动前设置。

package test.MyThread;

public class DaemonThread extends Thread{
    @Override
    public void run() {
        for(int i =0;i<10;i++){
            System.out.println(getName()+":"+i);
        }
    }
}

package test.MyThread;

public class Demo9 {
    public static void main(String[] args) {
        DaemonThread d1 = new DaemonThread();
        DaemonThread d2 = new DaemonThread();
        DaemonThread d3 = new DaemonThread();

        d2.setDaemon(true);
        d3.setDaemon(true);

        d1.start();
        d2.start();
        d3.start();
    }
}

在这里插入图片描述
当非守护线程d1结束后,守护线程d2和d3会立刻结束,即使循环还没有结束

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一纸春秋

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

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

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

打赏作者

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

抵扣说明:

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

余额充值