写点什么

先巩固下 Java 线程这些基础操作,再开始多线程编程也不迟

作者:Java你猿哥
  • 2023-03-15
    湖南
  • 本文字数:8160 字

    阅读完需:约 27 分钟

这篇文章咱们总结一下 Java 线程的基础,打好基础,后面几篇再学多线程的同步控制中的各种锁、线程通信等方面的知识时就会觉得更容易些。


本文的大纲如下:


线程在计算机系统里每个进程(Process)都代表着一个运行着的程序,比如打开微信,系统就会为微信开一个进程--进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位。


一个进程下可以有很多个线程,还拿微信举例子,我们用微信的时候除了给好友收发消息,还可以在里面看公众号,看公众号的时候,也不影响我们的微信收到其他人发给我们的消息,这就以为着运行的微信的进程,还开启了多个线程来同时完成这些子任务。


线程是进程的子任务,是 CPU 调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发,线程同时也是操作系统可识别的最小执行和调度单位。


在 Java 里线程是程序执行的载体,我们写的代码就是由线程运行的。有的时候为了增加程序的执行效率,我们不得不使用多线程进行编程,虽然多线程能最大化程序利用 CPU 的效率,但是程序用多写成进行任务处理,也是 BUG 的高发地,主要原因还是多线程环境下有些问题一旦被疏忽就会造成执行结果不符合预期的 BUG。


平时我们写代码思考问题时的习惯思维是单线程的,写多线程的时候得刻意切换一下才行,这就要求我们要了解清楚线程在不同运行条件下所表现出来的行为才行。


首先我们来看一下在 Java 中是怎么表示线程的。


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 类的子类,在子类中覆盖 Thread 类的 run() 方法,在这个 run() 方法的方法体里的代码,就是指定给线程去执行的代码。第二种是,将实现 Runnable (java.lang.Runnable) 的对象传递给 Thread 构造方法,创建 Thread 实例。其实,还有第三种给线程指定执行代码的方法,不过细究下来算是第二种方法的特殊使用方式,下面我们看看这三种指定线程执行方法体的方式,以及它们之间的区别。


通过 Thread 子类指定要执行的代码通过继承 Thread 类创建线程的步骤:


定义 Thread 类的子类,并覆盖该类的 run() 方法。run() 方法的方法体就代表了线程要完成的任务,因此把 run() 方法称为执行体。创建 Thread 子类的实例,即创建了线程对象。调用线程对象的 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 接口指定要执行的代码 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 接口创建线程的步骤如下:


定义 Runnable 接口的实现,实现该接口的 run 方法。该 run 方法的方法体同样是线程的执行体。创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建 Thread 对象,该 Thread 对象才是真正的线程对象。调用线程对象的 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 的实现。


使用 Runnable 的匿名类作为参数创建 Thread 对象:Thread threadA = new Thread(new Runnable() {private int ticket = 5;@Overridepublic void run() {while (ticket > 0) {System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票");ticket--;}}}, "Runnable 线程-A");复制代码使用实现了 Runnable 的 Lambda 表达式作为参数创建 Thread 对象: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> {@Overridepublic 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 线程的使用陷阱在刚开始接触和学习 Java 线程相关的知识时,一个常见的错误是,在创建线程的线程里,调用 Thread 对象的 run() 方法而不是调用 start() 方法。


Runnable myRunnable = new Runnable() {@Overridepublic 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() 就能判断出线程被设置了中断状态,随后结束执行退出。


守护线程什么是守护线程?


守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程。当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。为什么需要守护线程?


守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。如何使用守护线程?


可以使用 isDaemon 方法判断线程是否为守护线程。可以使用 setDaemon 方法设置线程为守护线程。 正在运行的用户线程无法设置为守护线程,所以 setDaemon 必须在 thread.start 方法之前设置,否则会抛出 llegalThreadStateException 异常; 一个守护线程创建的子线程依然是守护线程。 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑就不能。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 线程的生命周期 java.lang.Thread.State 中定义了 6 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。 下图给出了这六种状态,注意中间的 Ready 和 Running 都属于 Runnable 就绪状态。


以下是各状态的说明,以及状态间的联系:


新建(New) - 尚未调用 start 方法的线程处于此状态。此状态意味着:线程创建了但尚未启动。就绪(Runnable) - 已经调用了 start 方法的线程处于此状态。此状态意味着:线程已经在 JVM 中运行。但是在操作系统层面,它可能处于运行状态,也可能等待资源调度(例如处理器资源),资源调度完成就进入运行状态。所以该状态的可运行是指可以被运行,具体有没有运行要看底层操作系统的资源调度。阻塞(Blocked) - 此状态意味着:线程处于被阻塞状态。表示线程在等待 synchronized 的隐式锁(Monitor lock)。被 synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,即处于阻塞状态。当占用 synchronized 隐式锁的线程释放锁,并且等待的线程获得 synchronized 隐式锁后,就又会从 BLOCKED 转换到 RUNNABLE 状态。等待(Waiting) - 此状态意味着:线程无限期等待,直到被其他线程显式地唤醒。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 synchronized 的隐式锁。而等待是主动的,线程通过调用 Object.wait 等方法进入。定时等待(Timed waiting) - 此状态意味着:**无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒,**线程通过调用设置了 Timeout 参数的 Object.wait 等方法进入。终止(Terminated) - 线程执行完 run 方法,或者因异常退出了 run 方法。此状态意味着:线程结束了生命周期。下面这张图更生动地展示了线程状态切换的时机和触发条件(图片来自网络,出处下方饮用链接 1)。

用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
先巩固下 Java 线程这些基础操作,再开始多线程编程也不迟_Java_Java你猿哥_InfoQ写作社区