Java多线程的创建方法

  1. 线程概念

  2. 线程的创建和停止

  3. 线程的状态

1. 多线程概述

1.1 多线程介绍

​ 多线程是Java语言的重要特性,大量应用于网络编程、服务器端程序的开发,最常见的UI界面底层原理、操作系统底层原理都大量使用了多线程。

​ 我们可以流畅的点击软件或者游戏中的各种按钮,其实,底层就是多线程的应用。UI界面的主线程绘制界面,如果有一个耗时的操作发生则启动新的线程,完全不影响主线程的工作。当这个线程工作完毕后,再更新到主界面上。

​ 我们可以上百人、上千人、上万人同时访问某个网站,其实,也是基于网站服务器的多线程原理,如果没有多线程,服务器处理速度会极大降低。

在学习多线程之前,我们先要了解几个关于多线程有关的概念。

1.1.1 程序

​ 程序(Program)是一个静态的概念,一般对应于操作系统中一个可执行文件,比如:我们要启动酷狗听音乐,则对应酷狗的可执行程序。当我们双击酷狗,则加载程序到内存中,开始执行该程序,于是产生了“进程”。

1.1.2 进程

​ 执行中的程序叫做进程(Process),是一个动态的概念。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

​ 现代的操作系统都可以同时启动多个进程。比如:我们在用酷狗听音乐、也可以使用IDEA写代码、也可以同时用浏览器查看网页。

多进程有什么意义呢?

单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。例如,一边玩游戏(游戏进程),一边听音乐(音乐进程)。

也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。并且,还可以提高CPU的使用率。

1.1.3 线程

​ 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,也可以有多个线程。

什么是多线程呢?即就是一个程序中有多个线程在同时执行,我们也称之为多线程程序。

紧接着,我们来区别单线程程序与多线程程序的不同:

  • 单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务才开始执行。

  • 多线程程序:即,若有多个任务可以“同时”执行。多个任务可以并发执行。

1.1.4 Java程序的运行原理

​ 由java命令启动JVM,JVM启动就相当于启动了一个进程,该线程在负责java程序的运行,而且这个线程运行的代码存在于main方法中,我们把这个线程称之为主线程。

​ JVM虚拟机的启动是单线程的还是多线程的?

​ 答案是多线程。原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,JVM的启动其实是多线程的。

1.2 并发和并行

​ 在学习多线程之前,我们必须先理解什么是并发,什么是并行,什么是并发编程,什么是并行编程。

1.2.1 并发(concurrency)

​ 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

 

如上图所示,并发就是只有一个CPU资源,程序(或线程)之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中,B、C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二阶段只有B在执行,第三阶段只有C在执行。其实,并发过程中,A、B、C并不是同时进行的(微观角度),但又是同时进行的(宏观角度)。

​ 在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,CPU使用抢占式调度模式在多个线程间进行着高速的切换,因此我们看起来的感觉就像是多线程一样,也就是看上去就是在同一时刻运行。

1.2.2 并行(parallellism)

​ 指在同一时刻,有多条指令在多个处理器上同时执行。

 

如图所示,在同一时刻,ABC都是同时执行(微观、宏观)  

1.2.3 并发编程和并行编程
  • 在CPU比较繁忙,资源不足的时候(开启了很多进程),操作系统只为一个含有多线程的进程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。

  • 在CPU资源比较充足的时候,一个进程内的多线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。

至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。

总结:单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。

不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源。

 

2. 线程的创建

​ 在Java中使用多线程非常简单,我们先学习如何创建和使用线程,然后结合案例再深入剖析线程的特性。

2.1 Thread 类介绍

​ 该如何创建线程呢?通过 API 中搜索,查到 Thread 类。通过阅读 Thread 类中的描述,知道 Thread 类用来描述线程,使其具备线程应该有功能。Java 虚拟机允许应用程序并发地运行多个执行线程。

 

2.1.1 构造方法

 

2.1.2 常用方法

 

继续阅读,发现创建新执行线程有两种方法。

  • 一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run 方法相当于其它线程的 main 方法。

  • 另一种方法是声明一个实现 Runnable 接口的类。该类然后重写 run 方法。然后创建Runnable 的子类对象,传入到某个线程的构造方法中,开启线程。

2.2 创建线程方式一:继承Thread类

  • 继承Thread类实现多线程的步骤:

    1) 定义一个类并继承于Thread类。

    2) 重写Thread类的run()方法,run()方法中包含了线程需要执行的任务。

    3) 调用的Thread的子类来创建线程对象。

    4) 通过调用start()方法开启线程,线程开启后会自动调用线程的run()方法。

【示例】继承Thread类实现多线程

public class MyThread01 extends Thread {
    // 继承父类的构造方法
    public MyThread01() {
    }

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

    @Override
    public void run() {
        // 定义线程任务代码: 打印0-9
        for (int i = 0; i < 10; i++) {
            System.out.println(super.getName() + "---" + i);
        }
    }
}
/*

创建线程的方式一
            1)定义一个类并继承于Thread类。
            2)重写Thread类的run()方法,run()方法中包含了线程需要执行的任务。
            3)调用的Thread的子类来创建线程对象。
            4)通过调用start()方法开启线程,线程开启后会自动调用线程的run()方法

*/

public class ThreadDemo2 {
    public static void main(String[] args) {
        // 创建两个线程
        // 创建线程A
        MyThread01 t1 = new MyThread01("线程A");
        // 创建线程B
        MyThread01 t2 = new MyThread01("线程B");

        // 开启两个线程
        t1.start();
        t2.start();

        // 定义主线程任务
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

 运行以上案例代码,输出结果如下:

2.3 创建线程深度剖析

思考1:线程对象调用run方法和调用start方法区别?

​ 线程对象调用run方法不开启线程,仅是对象调用方法并在主线程中执行。线程对象调用start开启线程,并让JVM调用run方法在开启的线程中执行。

思考2:我们为什么要继承 Thread 类,并调用其的 start 方法才能开启线程呢?

​ 继承 Thread 类:因为 Thread 类用来描述线程,具备线程应该有功能。那为什么不直接创建 Thread

类的对象呢?如下代码:

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread();
        Thread t2 = new Thread();
        
        /*
           此时JVM会开辟新线程,执行Thread类中的run(),但是由于线程任务target为null,所以这个run()无任何任务执行
           这个违反了线程的初衷,所以我们需要在Thread子类中重写Run() ,将Run()中代码作为线程的任务执行。
        
         */
        t1.start();
        
    }
}

 

 

以上代码语法上没有任何问题,但是该 start 调用的是 Thread 类中的 run 方法,而这个 run 方法没有做什么事情,更重要的是这个 run 方法中并没有定义我们需要让线程执行的代码。

​ 创建线程的目的就是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。对于之前所讲的主线程,它的任务定义在 main 函数中。自定义线程需要执行的任务都定义在 run方法中。

​ Thread 类 run 方法中的任务并不是我们所需要的,只有重写这个 run 方法。既然 Thread 类已经定义了线程任务的编写位置(run 方法),那么只要在编写位置(run 方法)中定义任务代码即可,所以进行了重写 run 方法动作。

思考3:多线程执行时,到底在内存中是如何运行的呢?

​ 多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

在多线程中,每个线程都有自己独立的栈内存,但是都是共享的同一个堆内存。在某个线程中程序执行出现了异常,那么对应线程执行终止,但是不影响别的线程程序执行。

​ main方法执行完毕之后,虚拟机有可能不会立即结束,只有等所有的线程都执行完毕之后,虚拟机才会结束!

思考4:开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?

查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象,还有个方法是获取当前线程对象的名称。

 

想要获取运行时线程名称,必须先要得到运行时线程对象(这里的线程对象和继承Thread子类对象是不一样的)。在线程类方法当中有一个方法,叫做currentThread(),返回thread类型,静态的,类名可以直接调用。

【示例】获取当前线程对象和线程名称

public class MyThread02 extends Thread {
    // 定义该线程任务类的name属性
    private String name;
    // 继承父类的构造方法
    public MyThread02() {
    }
    public MyThread02(String name) {
        this.name = name;
    }
    @Override
    public void run() {
       // 打印当前正在运行的线程对象的名称
        String threadName = Thread.currentThread().getName();
        System.out.println(name +" 线程名称为:" + threadName );
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) {
        MyThread02 t1 = new MyThread02("线程A");
        MyThread02 t2 = new MyThread02("线程B");
        t1.start();
        t2.start();

        String name = Thread.currentThread().getName();
        System.out.println("主线程名称:"+name);
    }
}

 运行以上案例代码,输出结果如下:

 

通过运行结果观察,发现主线程的名称为:main。自定义的线程名字默认为:Thread-加上编号,编号从0开始递增,th1线程对应的名称为:Thread-0,th2线程对应的名称为Thread-1。

​ 那么自定义线程的默认名字是怎么来的呢? 通过对Thread类的源码分析,我们发现调用Thread类的构造方法时,默认就给该线程对象定义了一个名字,格式为:Thread-加上编号。

 

 

由此,我们也可以得出一个结论:当我们创建线程子类对象的时候,它们在创建的同时已经完成了名称的定义。

【示例】获取线程对象的名称

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t1 = new Thread();
        Thread t2 = new Thread();
        System.out.println("t1线程对象的名称:" + t1.getName());
        System.out.println("t2线程对象的名称:" + t2.getName());
    }
}

 运行以上案例代码,输出结果如下:

思考5:可以手动的设置线程名称吗?

​ 自定义的线程名字默认为:Thread-加上编号,如果我们想要修改默认的线程名字,可以在创建线程对象的时候设置线程的名称,也可以使用Thread类提供的setName()方法来实现。

 

【示例】设置线程对象的名称

public class MyThread03 extends Thread {
    // 继承父类的构造方法
    public MyThread03() {
    }
    public MyThread03(String name) {
        super(name);
    }
    @Override
    public void run() {
       // 打印当前正在运行的线程对象的名称
        String threadName = Thread.currentThread().getName();
        System.out.println(" 线程名称为:" + threadName );
    }
}

public class ThreadDemo6 {
    public static void main(String[] args) {
        MyThread03 t1 = new MyThread03("窗口一");
        t1.start();

        MyThread03 t2 = new MyThread03();
        t2.setName("窗口二");
        t2.start();
    }
}

 

运行以上案例代码,输出结果如下:

2.4 创建线程方式二:实现 Runnable 接口

​ 使用继承Thread类的方式来创建线程有一个缺点,那就是自定义的类继承Thread类后就不能继承别的父类,如果还想继承别的父类那么可以选用第二种创建线程的方式。

​ 在开发中,我们更多的是通过Runnable接口实现多线程,使用这种方式避免了Java单继承的局限性,所以实现Runnable接口方式要通用一些。

​ 查看 Runnable 接口说明文档:Runnable 接口用来指定每个线程要执行的任务。包含了一个run的无参数抽象方法,需要由接口实现类重写该方法。

 

2.4.1 接口中的方法

 

2.4.2  Thread 类构造方法

 

  • 实现Runnable接口实现多线程的步骤:

    1) 定义一个类并实现Runnable接口。

    2) 重写Runnable接口的run()方法,在run()方法中包含线程需要执行的任务。

    3) 通过Thread类创建线程对象,并把Runnable接口的实现类对象作为参数传递。

    4) 调用线程对象的start()方法开启线程,并调用Runnable实现类的run()方法。

【示例】实现Runable接口实现多线程

public class MyTask01 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
        }
    }
}

/*线程的创建方式二
        Runnable接口: 指定了每个线程需要执行的任务,
              包含了run(),需要通过子类重写run()定义线程任务
        步骤:
           1. 定义线程任务类实现Runnable接口
           2. 重写Runnable接口中的run(),在run()定义了线程的任务代码
           3. 创建线程对象并将线程任务类对象作为:参数传入
           4. 调用线程对象的start()启动线程,执行线程任务类中的run()
*/
public class ThreadDemo7 {
    public static void main(String[] args) {
        // 1. 创建两个线程任务对象
        MyTask01 myTask1 = new MyTask01();
        MyTask01 myTask2 = new MyTask01();
        // 2. 创建两个线程
        Thread t1 = new Thread(myTask1, "线程A");
        Thread t2 = new Thread(myTask2, "线程B");
        // 3. 开启两个线程
        t1.start();
        t2.start();
        // 主线程执行任务
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
        }
    }
}

运行以上案例代码,输出结果如下:  

Thread 类用来描述线程,使其具备线程应该有功能,Runnable接口实现类用来封装线程任务,从而实现线程对象和线程任务进行解耦。

​ 实现Runnable接口的好处,不但避免了java单继承的局限性,而且还将线程任务从线程子类相分离,进行了单独的封装,按照面向对象的思想将任务封装成对象。

2.5 模拟Thread类start方法的实现

​ 我们知道,通过继承Thread类创建线程,线程任务是封装在Thread子类的run方法中;通过实现Runnable接口来创建线程,线程任务是封装在Runnable接口实现类的run方法中,那么调用start方法开启线程,在Thread内部是如何正确的执行线程任务的呢?

​ 接下来我们就来模拟Thread类,明确调用start方法开启线程调用之后是如何实现调用run方法来执行对应的线程任务。

【示例】模拟Thread类start方法的实现

class Thread {
	private Runnable target;
	public Thread() {}
	public Thread(Runnable r) {
		this.target = r;
	}
	public void start() {
		run(); // 注意此处是重点
	}
	public void run() {
		if(target != null)
			target.run();	
	}
}

 

模拟Thread类start方法的实现的核心:

​ 如果创建线程采用继承Thread类的方式,也就是通过Thread子类对象调用start方法,那么调用的就是Thread子类对象的run方法。

​ 如果创建线程采用实现Runnable接口的方式,也就是通过Thread对象调用start方法,那么调用的就是Thread的run方法,然后再去调用Runnable接口实现类的run。

​ 接下来,我们就基于两种创建线程的方式,来对我们模拟Thread类的start方法进行测试,看一下我们模拟实现是否成功!

【示例】基于两种创建线程方式的测试

public class MyTask01 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
        }
    }
}
/*
    @Author xiangge
    @Date 2023/8/1 
    @Description 定义子线程类
*/
public class MyThread01 extends Thread {
    public MyThread01() {
    }

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

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "正在执行" + i);
        }
    }
}
// 基于两种创建线程方式的测试
public class ThreadDemo8 {
    public static void main(String[] args) {
        // 1.使用创建线程方式一
        MyThread01 t1 = new MyThread01("线程A");
        // 2. 使用创建线程方式二
        MyTask01 task1 = new MyTask01();
        Thread t2 = new Thread(task1, "线程B");

        //3. 开启线程
        t1.start();
        t2.start();
    }
}

 

运行以上案例代码,输出结果如下:  

2.6 创建线程方式三:Lambda表达式创建线程

Lambda表达式起始于JDK8,此时可以代替匿名内部类创建,使用Lambda可以简化匿名内部类操作。

// 2. 使用创建线程方式二:匿名内部类方式
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                   System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
                }
            }
        }, "线程B");
//2. 使用创建线程方式二:Lambda表达式方式
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "线程正在执行:" + i);
            }
        }, "线程B");

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值