这篇文章咱们总结一下 Java线程的基础,打好基础,后面几篇再学多线程的同步控制中的各种锁、线程通信等方面的知识时就会觉得更容易些。
本文的大纲如下:
在计算机系统里每个进程(Process)都代表着一个运行着的程序,比如打开微信,系统就会为微信开一个进程--进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位。
一个进程下可以有很多个线程,还拿微信举例子,我们用微信的时候除了给好友收发消息,还可以在里面看公众号,看公众号的时候,也不影响我们的微信收到其他人发给我们的消息,这就以为着运行的微信的进程,还开启了多个线程来同时完成这些子任务。
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发,线程同时也是操作系统可识别的最小执行和调度单位。
在 Java 里线程是程序执行的载体,我们写的代码就是由线程运行的。有的时候为了增加程序的执行效率,我们不得不使用多线程进行编程,虽然多线程能最大化程序利用 CPU 的效率,但是程序用多写成进行任务处理,也是BUG的高发地,主要原因还是多线程环境下有些问题一旦被疏忽就会造成执行结果不符合预期的BUG。
平时我们写代码思考问题时的习惯思维是单线程的,写多线程的时候得刻意切换一下才行,这就要求我们要了解清楚线程在不同运行条件下所表现出来的行为才行。
首先我们来看一下在 Java 中是怎么表示线程的。
到目前为止,我们写的所有 Java 程序代码都是在由JVM给创建的主线程(Main Thread) 中执行的。Java 线程就像一个虚拟 CPU,可以在运行的 Java 应用程序中执行 Java 代码。当一个 Java 应用程序启动时,它的入口方法 main() 方法由主线程执行。主线程(Main Thread)是一个由 Java 虚拟机创建的运行应用程序的特殊线程。
我们在 Java 里万物皆对象,所以系统的线程在 Java 里也是用对象表示的,线程是类 java.lang.Thread 类或者其子类的实例。在 Java 应用程序内部, 我们可以通过线程对象创建和启动更多线程,这些线程可以与主线程并行执行应用程序的代码。
下面看一下怎么在 Java 程序里创建和启动线程。
在 Java 中创建一个线程,就是创建一个Thread类的实例
Thread thread = new Thread();
启动线程就是调用Thread对象的start()方法
thread.start();
当然,这个例子没有指定线程要执行的代码,所以线程将在启动后立即停止。 让线程执行逻辑,需要给线程对象指定执行体。
有两种方法可以给线程指定要执行的代码。
其实,还有第三种给线程指定执行代码的方法,不过细究下来算是第二种方法的特殊使用方式,下面我们看看这三种指定线程执行方法体的方式,以及它们之间的区别。
通过继承Thread类创建线程的步骤:
(1) 定义 Thread 类的子类,并覆盖该类的 run() 方法。run() 方法的方法体就代表了线程要完成的任务,因此把 run() 方法称为执行体。
(2) 创建 Thread 子类的实例,即创建了线程对象。
(3) 调用线程对象的 start() 方法来启动该线程。
package com.learnthread;public class ThreadFirstRunDemo { public static void main(String[] args) { // 实例化线程对象 MyThread threadA = new MyFirstThread("线程-A"); MyThread threadB = new MyFirstThread("线程-B"); // 启动线程 threadA.start(); threadB.start(); } static class MyFirstThread extends Thread { private int ticket = 5; MyThread(String name) { super(name); } @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); ticket--; } } }}
上面的程序,主线程启动调用A、B两个线程的start() 后,并没有通过调用wait() 等待他们执行结束。A、B两个线程的执行体,会并发地被系统执行,等线程都直接结束后,程序才会退出。
Runnable 接口的定义如下,只有一个 run() 方法的定义:
package java.lang;public interface Runnable { public abstract void run();}
其实,Thread 类实现的也是 Runnable 接口。 在 Thread 类的重载构造方法里,支持接收一个实现了 Runnale 接口的对象作为其 target 参数来初始化线程对象。
public class Thread implements Runnable { ... public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } ... public Thread(Runnable target, String name) { init(null, target, name, 0); } ...}
通过实现 Runnable 接口创建线程的步骤如下:
(1) 定义 Runnable 接口的实现,实现该接口的 run 方法。该 run 方法的方法体同样是线程的执行体。
(2) 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
(3) 调用线程对象的 start 方法来启动线程并执行。
package com.learnthread;public class RunnableThreadDemo { public static void main(String[] args) { // 实例化线程对象 Thread threadA = new Thread(new MyThread(), "Runnable 线程-A"); Thread threadB = new Thread(new MyThread(), "Runnable 线程-B"); // 启动线程 threadA.start(); threadB.start(); } static class MyThread implements Runnable { private int ticket = 5; @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); ticket--; } } }}
运行上面例程会有以下输出,同样程序会在所以线程执行完后退出。
Runnable 线程-B 卖出了第 5 张票Runnable 线程-B 卖出了第 4 张票Runnable 线程-B 卖出了第 3 张票Runnable 线程-B 卖出了第 2 张票Runnable 线程-B 卖出了第 1 张票Runnable 线程-A 卖出了第 5 张票Runnable 线程-A 卖出了第 4 张票Runnable 线程-A 卖出了第 3 张票Runnable 线程-A 卖出了第 2 张票Runnable 线程-A 卖出了第 1 张票Process finished with exit code 0
既然是给 Thread 传递 Runnable 接口的实现对象即可,那么除了普通的定义类实现接口的方式,我们还可以使用匿名类和 Lambda 表达式的方式来定义 Runnable 的实现。
Thread threadA = new Thread(new Runnable() { private int ticket = 5; @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); ticket--; } }}, "Runnable 线程-A");
Runnable runnable = () -> { System.out.println("Lambda Runnable running"); };Thread threadB = new Thread(runnable, "Runnable 线程-B");
因为,Lambda 是无状态的,定义不了内部属性,这里就举个简单的打印一行输出的例子了,理解一下这种用法即可。
上面两种方法虽然能指定线程执行体里要执行的任务,但是都没有返回值,如果想让线程的执行体方法有返回值,且能被外部创建它的父线程获取到返回值,就需要结合J.U.C 里提供的 Callable、Future 接口来实现线程的执行体方法才行。
J.U.C 是 java.util.concurrent 包的缩写,提供了很多并发编程的工具类,后面会详细学习。
Callable 接口只声明了一个方法,这个方法叫做 call():
package java.util.concurrent;public interface Callable<V> { V call() throws Exception;}
Future 就是对于具体的 Callable 任务的执行进行取消、查询是否完成、获取执行结果的。可以通过 get 方法获取 Callable 的 call 方法的执行结果,但是要注意该方法会阻塞直到任务返回结果。
public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;}
Java 的 J.U.C 里给出了 Future 接口的一个实现 FutureTask,它同时实现了 Future 和 Runnable 接口,所以,FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
下面是一个 Callable 实现类和 FutureTask 结合使用让主线程获取子线程执行结果的一个简单的示例:
package com.learnthread;import java.util.concurrent.Callable;import java.util.concurrent.FutureTask;public class CallableDemo implements Callable<Integer> { @Override public Integer call() { int i = 0; for (i = 0; i < 20; i++) { if (i == 5) { break; } System.out.println(Thread.currentThread().getName() + " " + i); } return i; } public static void main(String[] args) { CallableDemo tt = new CallableDemo(); FutureTask<Integer> ft = new FutureTask<>(tt); Thread t = new Thread(ft); t.start(); try { System.out.println(Thread.currentThread().getName() + " " + ft.get()); } catch (Exception e) { e.printStackTrace(); } }}
上面我们把 FutureTask 作为 Thread 构造方法的 Runnable 类型参数 target 的实参,在它的基础上创建线程, 执行逻辑。所以本质上 Callable + FutureTask 这种方式也是第二种通过实现 Runnable 接口给线程指定执行体的,只不过是由 FutureTask 包装了一层,由它的 run 方法再去调用 Callable 的 call 方法。例程运行后的输出如下:
Thread-0 0Thread-0 1Thread-0 2Thread-0 3Thread-0 4main 5
Callable 更常用的方式是结合线程池来使用,在线程池接口 ExecutorService 中定义了多个可接收 Callable 作为线程执行任务的方法 submit、invokeAny、invokeAll 等,这个等学到线程池了我们再去学习。
在刚开始接触和学习 Java 线程相关的知识时,一个常见的错误是,在创建线程的线程里,调用 Thread 对象的 run() 方法而不是调用 start() 方法。
Runnable myRunnable = new Runnable() { @Override public void run() { System.out.println("Anonymous Runnable running"); }};Thread newThread = new Thread(myRunnable);newThread.run(); // 应该调用 newThread.start();
起初你可能没有注意到这么干有啥错,因为 Runnable 的 run() 方法正常地被执行,输出了我们想要的结果。
但是,这么做 run() 不会由我们刚刚创建的新线程执行,而是由创建 newThread 对象的线程执行的 。要让新创建的线程--newThread 调用 myRunnable 实例的 run() 方法,必须调用 newThread.start() 方法才行。
Thread 线程常用的方法有以下这些:
方法 | 描述 |
run | 线程的执行实体,不需要我们主动调用,调用线程的start() 就会执行run() 方法里的执行体 |
start | 线程的启动方法。 |
Thread.currentThread | Thread 类提供的静态方法,返回对当前正在执行的线程对象的引用。 |
setName | 设置线程名称。 |
getName | 获取线程名称。 |
setPriority | 设置线程优先级。Java 中的线程优先级的范围是 [1,10],一般来说,高优先级的线程在运行时会具有优先权。可以通过 thread.setPriority(Thread.MAX_PRIORITY) 的方式设置,默认优先级为 5。 |
getPriority | 获取线程优先级。 |
setDaemon | 设置线程为守护线程。 |
isDaemon | 判断线程是否为守护线程。 |
isAlive | 判断线程是否启动。 |
interrupt | 中断线程的运行。 |
Thread.interrupted | 测试当前线程是否已被中断。 |
join | 可以使一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。 |
Thread.sleep | 静态方法。将当前正在执行的线程休眠。 |
Thread.yield | 静态方法。将当前正在执行的线程暂停,让出CPU,让其他线程执行。 |
使用 Thread.sleep 方法可以使得当前正在执行的线程进入休眠状态。 使用 Thread.sleep 需要向其传入一个整数值,这个值表示线程将要休眠的毫秒数。 Thread.sleep 方法可能会抛出 InterruptedException,因为异常不能跨线程传播回主线程中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
public class ThreadSleepDemo { public static void main(String[] args) { new Thread(new MyThread("线程A", 500)).start(); new Thread(new MyThread("线程B", 1000)).start(); new Thread(new MyThread("线程C", 1500)).start(); } static class MyThread implements Runnable { /** 线程名称 */ private String name; /** 休眠时间 */ private int time; private MyThread(String name, int time) { this.name = name; this.time = time; } @Override public void run() { try { // 休眠指定的时间 Thread.sleep(this.time); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.name + "休眠" + this.time + "毫秒。"); } }}
上面例程开启了3个线程,在各自的线程执行体里让各自线程休眠了 500、1000 和 1500 ms ,线程 C 休眠结束后,整个程序退出。
线程A休眠500毫秒。线程B休眠1000毫秒。线程C休眠1500毫秒。Process finished with exit code 0
当一个线程运行时,另一个线程可以直接通过 interrupt 方法中断其运行状态。
public class ThreadInterruptDemo { public static void main(String[] args) { MyThread mt = new MyThread(); // 实例化Runnable实现类的对象 Thread t = new Thread(mt, "线程"); // 实例化Thread对象 t.start(); // 启动线程 try { Thread.sleep(2000); // 主线程休眠2秒 } catch (InterruptedException e) { System.out.println("主线程休眠被终止"); } t.interrupt(); // 中断 mt 线程的执行 } static class MyThread implements Runnable { @Override public void run() { System.out.println("1、进入run()方法"); try { Thread.sleep(10000); // 线程休眠10秒 System.out.println("2、已经完成了休眠"); } catch (InterruptedException e) { System.out.println("3、MyThread线程休眠被终止"); return; // 返回调用处 } System.out.println("4、run()方法正常结束"); } }}
如果一个线程的 run 方法执行一个无限循环,并且没有执行 sleep 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt 方法就无法使线程提前结束。
不过调用 interrupt 方法会设置线程的中断标记,此时被设置中断标记的线程再调用 interrupted 方法会返回 true。因此可以在线程的执行体循环体中使用 interrupted 方法来判断当前线程是否处于中断状态,从而提前结束线程。
看下面这个,可以有效终止线程执行的示例:
package com.learnthread;import java.util.concurrent.TimeUnit;public class ThreadInterruptEffectivelyDemo { public static void main(String[] args) throws Exception { MyTask task = new MyTask(); Thread thread = new Thread(task, "线程-A"); thread.start(); TimeUnit.MILLISECONDS.sleep(50); thread.interrupt(); } private static class MyTask implements Runnable { private volatile long count = 0L; @Override public void run() { System.out.println(Thread.currentThread().getName() + " 线程启动"); // 通过 Thread.interrupted 和 interrupt 配合来控制线程终止 while (!Thread.interrupted()) { System.out.println(count++); } System.out.println(Thread.currentThread().getName() + " 线程终止"); } }}
主线程在启动线程-A后,主动休眠50毫秒,线程-A的执行体里会不断打印计数器的值,等休眠结束后主线程通过调用线程-A的 interrupt 方法设置了线程的中断标记,这时线程-A的执行体中通过 Thread.interrupted() 就能判断出线程被设置了中断状态,随后结束执行退出。
(1) 什么是守护线程?
(2) 为什么需要守护线程?
守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。
(3) 如何使用守护线程?
可以使用 isDaemon 方法判断线程是否为守护线程。
可以使用 setDaemon 方法设置线程为守护线程。
public class ThreadDaemonDemo { public static void main(String[] args) { Thread t = new Thread(new MyThread(), "线程"); t.setDaemon(true); // 此线程在后台运行 System.out.println("线程 t 是否是守护进程:" + t.isDaemon()); t.start(); // 启动线程 } static class MyThread implements Runnable { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + "在运行。"); } } }}
java.lang.Thread.State 中定义了 6 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。 下图给出了这六种状态,注意中间的 Ready 和 Running 都属于 Runnable 就绪状态。
以下是各状态的说明,以及状态间的联系:
下面这张图更生动地展示了线程状态切换的时机和触发条件(图片来自网络,出处下方饮用链接1)。
引用链接:
本文链接:http://www.28at.com/showinfo-26-51228-0.html老后端被借调去写Java了,含泪总结的Java多线程编程基础
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: PHP老矣,尚能饭否?