一、多线程
1.1 基本概念
线程其实是程序中的一条执行路径。
多线程(Multithread)是指在同一个程序中同时存在几个执行体,按几条不同的执行路径共同工作的情况。
先来区分几个概念:
程序(Program):程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,程序是静态的代码。 进程(Process):进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。 多任务(Multi task):多任务是指在一个系统中可以同时运行多个程序,即有多个独立运行的任务,每一个任务对应一个进程。 线程(Thread):线程是一个比进程更小的执行单位。一个进程在其执行过程中可以产生多个线程,形成多条执行线路。
我们之前写过的程序,其实都是单线程程序,如下图代码,如果前面的for循环没有执行完,for循环下面的代码是不会执行的。
怎样的程序才是多线程程序呢? 如下图所示,12306网站就是支持多线程的,因为同时可以有很多人一起进入网站购票,而且每一个人互不影响。再比如百度网盘,可以同时下载或者上传多个文件。这些程序中其实就有多条执行路径,每一条执行执行路径就是一条线程,所以这样的程序就是多线程程序。
1.2 线程的状态与生命周期
接下来,我们学习最后一个有关线程的知识点,叫做线程的生命周期。所谓生命周期就是线程从生到死的过程中间有哪些状态,以及这些状态之间是怎么切换的。
为了让大家同好的理解线程的生命周期,先用人的生命周期举个例子,人从生到死有下面的几个过程。在人的生命周期过程中,各种状态之间可能会有切换,线程也是一样的。
接下来就来学习线程的生命周期。在Thread类中有一个嵌套的枚举类叫Thread.Status,这里面定义了线程的6中状态。如下图所示
NEW: 新建状态,线程还没有启动 RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态 BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态 WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态 TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态 TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。
这几种状态之间切换关系如下图所示
1.3 线程的调度与优先级
调度:指在各个线程之间分配CPU资源。线程调度有两种模型:分时模型和抢占模型。
优先级:决定了线程被CPU执行的优先顺序。
Java语言中线程的优先级从低到高以整数1~10表示,共分为10级。Thread类有三个关于线程优先级的静态变量,MIN_PRIORITY表示最小优先级,通常为1;MAX_PRIORITY表示最高优先级,通常为10;NORM_PRIORITY表示普通优先级,缺省值为5。
对应一个新建的线程,系统会遵循如下的原则为其指定优先级:
(1)新建线程将继承创建它的父线程的优先级。父线程是指执行创建新线程对象语句所在的线程,它可能是程序的主线程,也可能是某一个用户自定义的线程。
(2)一般情况下,主线程具有普通优先级。
1.4 线程创建方式1
Java语言中实现多线程的方法有两种,一种是继承java.lang包中的Thread类,另一种是用户在定义自己的类中实现Runnable接口。但不管采用哪种方法,都要用到Java语言类库中的Thread类以及相关的方法。
Java为开发者提供了一个类叫做Thread,此类的对象用来表示线程。创建线程并执行线程的步骤如下
1.定义一个子类继承Thread类,并重写run方法 2.创建Thread的子类对象 3.调用start方法启动线程(启动线程后,会自动执行run方法中的代码)
代码如下
public class MyThread extends Thread{ // 2、必须重写Thread类的run方法 @Override public void run() { // 描述线程的执行任务。 for (int i = 1; i <= 5; i++) { System.out.println("子线程MyThread输出:" + i); } } }
再定义一个测试类,在测试类中创建MyThread线程对象,并启动线程
public class ThreadTest1 { // main方法是由一条默认的主线程负责执行。 public static void main(String[] args) { // 3、创建MyThread线程类的对象代表一个线程 Thread t = new MyThread(); // 4、启动线程(自动执行run方法的) t.start(); for (int i = 1; i <= 5; i++) { System.out.println("主线程main输出:" + i); } } }
打印结果如下图所示,我们会发现MyThread和main线程在相互抢夺CPU的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前我们是无法控制的,每次输出结果都会不一样)
最后我们还需要注意一点:不能直接去调用run方法,如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。此时执行结果是这样的。
1.5 线程创建方式2
接下来我们学习线程的第二种创建方式。Java为开发者提供了一个Runnable接口,该接口中只有一个run方法,意思就是通过Runnable接口的实现类对象专门来表示线程要执行的任务。具体步骤如下
1.先写一个Runnable接口的实现类,重写run方法(这里面就是线程要执行的代码) 2.再创建一个Runnable实现类的对象 3.创建一个Thread对象,把Runnable实现类的对象传递给Thread 4.调用Thread对象的start()方法启动线程(启动后会自动执行Runnable里面的run方法)
代码如下:先准备一个Runnable接口的实现类
/** * 1、定义一个任务类,实现Runnable接口 */ public class MyRunnable implements Runnable{ // 2、重写runnable的run方法 @Override public void run() { // 线程要执行的任务。 for (int i = 1; i <= 5; i++) { System.out.println("子线程输出 ===》" + i); } } }
再写一个测试类,在测试类中创建线程对象,并执行线程
public class ThreadTest2 { public static void main(String[] args) { // 3、创建任务对象。 Runnable target = new MyRunnable(); // 4、把任务对象交给一个线程对象处理。 // public Thread(Runnable target) new Thread(target).start(); for (int i = 1; i <= 5; i++) { System.out.println("主线程main输出 ===》" + i); } } }
运行上面代码,结果如下图所示**(注意:没有出现下面交替执行的效果,也是正常的)**
主线程main输出 ===》1 主线程main输出 ===》2 主线程main输出 ===》3 子线程输出 ===》1 子线程输出 ===》2 子线程输出 ===》3 子线程输出 ===》4 子线程输出 ===》5 主线程main输出 ===》4 主线程main输出 ===》5
1.6 线程创建方式2—匿名内部类
同学们注意了,现在这种写法不是新知识。只是将前面第二种方式用匿名内部类改写一下。因为同学们在看别人写的代码时,有可能会看到这种写法。你知道是怎么回事就可以了。
刚刚我们学习的第二种线程的创建方式,需要写一个Runnable接口的实现类,然后再把Runnable实现类的对象传递给Thread对象。
现在我不想写Runnable实现类,于是可以直接创建Runnable接口的匿名内部类对象,传递给Thread对象。
代码如下
public class ThreadTest2_2 { public static void main(String[] args) { // 1、直接创建Runnable接口的匿名内部类形式(任务对象) Runnable target = new Runnable() { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println("子线程1输出:" + i); } } }; new Thread(target).start(); // 简化形式1: new Thread(new Runnable() { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println("子线程2输出:" + i); } } }).start(); // 简化形式2: new Thread(() -> { for (int i = 1; i <= 5; i++) { System.out.println("子线程3输出:" + i); } }).start(); for (int i = 1; i <= 5; i++) { System.out.println("主线程main输出:" + i); } } }
1.7 线程的创建方式3
接下来,我们学习线程的第三种创建方式。已经有两种了为什么还有要第三种呢? 这样,我们先分析一下前面两种都存在的一个问题。然后再引出第三种可以解决这个问题。
- 假设线程执行完毕之后有一些数据需要返回,前面两种方式重写的run方法均没有返回结果。
public void run(){ ...线程执行的代码... }
- JDK5提供了Callable接口和FutureTask类来创建线程,它最大的优点就是有返回值。
在Callable接口中有一个call方法,重写call方法就是线程要执行的代码,它是有返回值的
public T call(){ ...线程执行的代码... return 结果; }
第三种创建线程的方式,步骤如下
1.先定义一个Callable接口的实现类,重写call方法 2.创建Callable实现类的对象 3.创建FutureTask类的对象,将Callable对象传递给FutureTask 4.创建Thread对象,将Future对象传递给Thread 5.调用Thread的start()方法启动线程(启动后会自动执行call方法) 等call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中 6.调用FutrueTask对的get()方法获取返回结果
代码如下:先准备一个Callable接口的实现类
/** * 1、让子类继承Thread线程类。 */ public class MyThread extends Thread{ // 2、必须重写Thread类的run方法 @Override public void run() { // 描述线程的执行任务。 for (int i = 1; i <= 5; i++) { System.out.println("子线程MyThread输出:" + i); } } }
再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果
public class ThreadTest3 { public static void main(String[] args) throws Exception { // 3、创建一个Callable的对象 Callable<String> call = new MyCallable(100); // 4、把Callable的对象封装成一个FutureTask对象(任务对象) // 未来任务对象的作用? // 1、是一个任务对象,实现了Runnable对象. // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。 FutureTask<String> f1 = new FutureTask<>(call); // 5、把任务对象交给一个Thread对象 new Thread(f1).start(); Callable<String> call2 = new MyCallable(200); FutureTask<String> f2 = new FutureTask<>(call2); new Thread(f2).start(); // 6、获取线程执行完毕后返回的结果。 // 注意:如果执行到这儿,假如上面的线程还没有执行完毕 // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。 String rs = f1.get(); System.out.println(rs); String rs2 = f2.get(); System.out.println(rs2); } }
二、多线程常用方法
下面我们演示一下getName()
、setName(String name)
、currentThread()
、sleep(long time)
这些方法的使用效果。
public class MyThread extends Thread{ public MyThread(String name){ super(name); //1.执行父类Thread(String name)构造器,为当前线程设置名字了 } @Override public void run() { //2.currentThread() 哪个线程执行它,它就会得到哪个线程对象。 Thread t = Thread.currentThread(); for (int i = 1; i <= 3; i++) { //3.getName() 获取线程名称 System.out.println(t.getName() + "输出:" + i); } } }
再测试类中,创建线程对象,并启动线程
public class ThreadTest1 { public static void main(String[] args) { Thread t1 = new MyThread(); t1.setName(String name) //设置线程名称; t1.start(); System.out.println(t1.getName()); //Thread-0 Thread t2 = new MyThread("2号线程"); // t2.setName("2号线程"); t2.start(); System.out.println(t2.getName()); // Thread-1 // 主线程对象的名字 // 哪个线程执行它,它就会得到哪个线程对象。 Thread m = Thread.currentThread(); m.setName("最牛的线程"); System.out.println(m.getName()); // main for (int i = 1; i <= 5; i++) { System.out.println(m.getName() + "线程输出:" + i); } } }
执行上面代码,效果如下图所示,我们发现每一条线程都有自己了名字了。
最后再演示一下join这个方法是什么效果。
public class ThreadTest2 { public static void main(String[] args) throws Exception { // join方法作用:让当前调用这个方法的线程先执行完。 Thread t1 = new MyThread("1号线程"); t1.start(); t1.join(); Thread t2 = new MyThread("2号线程"); t2.start(); t2.join(); Thread t3 = new MyThread("3号线程"); t3.start(); t3.join(); } }
执行效果是1号线程先执行完,再执行2号线程;2号线程执行完,再执行3号线程;3号线程执行完就结束了。
最新Java基础系列课程--Day14-多线程编程(二)https://developer.aliyun.com/article/1423551