1 线程的属性
线程有一些基本的属性,如id
、name
、以及 priority
:
id
:线程id
用于标识不同的线程,编号可能被后续创建的线程使用,编号是只读属性,不能修改;name
:线程的名称,默认值是Thread-(id)
;daemon
:是守护线程还是用户线程,可以通过setDaemon(true)
把线程设置为守护线程。守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC
线程就是一个守护线程。setDaemon()
要在线程启动前设置,否则JVM
会抛出非法线程状态异常,可被继承。priority
:线程调度器会根据这个值来决定优先运行哪个线程(不保证),优先级的取值范围为1~10
,默认值是5
,可被继承。Thread
中定义了下面三个优先级常量:- 最低优先级:
MIN_PRIORITY = 1
- 默认优先级:
NORM_PRIORITY = 5
- 最高优先级:
MAX_PRIORITY = 10
- 最低优先级:
daemon [ˈdiːmən] 守护进程;后台程序 priority [praɪˈɔːrəti] 优先,优先权
Daemon
线程被用作完成支持性工作,但是在Java
虚拟机退出时Daemon
线程中的finally
块并不一定会执行,示例如下:
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
thread.setDaemon(true);
thread.start();
}
static class DaemonRunner implements Runnable {
@Override
public void run() {
try {
SleepUtils.second(10);
} finally {
System.out.println("DaemonThread finally run.");
}
}
}
}
运行Daemon
程序,可以看到在终端或者命令提示符上没有任何输出。main
线程(非Daemon
线程)在启动了线程DaemonRunner
之后随着main
方法执行完毕而终止,而此时Java
虚拟机中已经没有非Daemon
线程,虚拟机需要退出,Java
虚拟机中的所有Daemon
线程都需要立即终止, 因此DaemonRunner
立即终止,但是DaemonRunner
中的finally
块并没有执行。
注意:在构建Daemon
线程时,不能依靠finally
块中的内容来确保执行关闭或清理资源的逻辑。
一个Java
程序从main()
方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java
天生就是多线程的, 因为执行main()
方法的是一个名称为main
的线程。
一个Java
程序的运行不仅仅是main()
方法的运行,而是main
线程和多个其他线程的同时运行。
2 在Java
中创建多线程
以下是Thread.java
中的注释:
/*
* There are two ways to create a new thread of execution.
* One is to declare a class to be a subclass of <code>Thread</code>.
* This subclass should override the <code>run</code> method of class <code>Thread</code>.
* The other way to create a thread is to declare a class that implements the
* <code>Runnable</code> interface.
* That class then implements the <code>run</code> method.
*/
public class Thread implements Runnable {
public Thread() { }
public Thread(Runnable target) { }
Thread(Runnable target, AccessControlContext acc) { }
public Thread(ThreadGroup group, Runnable target) { }
public Thread(String name) { }
public Thread(ThreadGroup group, String name) { }
Thread(ThreadGroup group, String name, int priority, boolean daemon) { }
public Thread(Runnable target, String name) { }
public Thread(ThreadGroup group, Runnable target, String name) { }
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { }
}
因此,线程的创建方式只有以下两种:
2.1 继承Thread
类
- 定义一个
Thread
类的子类,重写run()
方法,在run()
方法中实现线程要执行的相关逻辑 - 创建自定义的线程子类对象
- 调用子类实例的
start()
方法来重启线程
class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "run方法正在执行");
}
}
public class OnlyMain {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println(Thread.currentThread().getName() + "run方法正在执行");
}
}
// mainrun方法正在执行
// Thread-0run方法正在执行
2.2 实现Runnable
接口
- 定义
Runnable
接口实现类MyRunnable
,并重写run()
方法 - 创建
MyRunnable
的实例myRunnable
,以myRunnable
作为target
创建Thread
对象,Thread
才是真正的线程对象。 - 调用线程对象的
start()
方法
class MyRunnable implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "run方法正在执行");
}
}
public class OnlyMain {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println(Thread.currentThread().getName() + "run方法正在执行");
}
}
// mainrun方法正在执行
// Thread-0run方法正在执行
Thread
和Runnable
的区别:Thread
是对线程的抽象,Runnable
是对任务的抽象。Thread
是实现了Runnable
接口,是对Runnable
的扩展。 以下是Thead
的源码:
public class Thread implements Runnable { }
实现Runnable
和Thread
哪个相比更好:
- 从代码的角度来看,
Java
是单继承实现,所以Runnable
比较好 - 从扩展性角度来看:继承方式(
Thread
)—— 线程对象和任务对象耦合在一起;实现方式(Runnable
)——线程对象和任务对象是分离的,耦合性较低,扩展性较高。
为了避免Java
中单继承的限制,增强程序的健壮性,代码可以被多个线程共享。线程池只能放入实现Runnable
类线程,不能直接放入继承Thread
的类中—— 所以,Runnable
更好。
Callable
为什么不算呢?因为把Callable
交给Thread
去执行的时候,本质上是把Callable
包装成了一个FutureTask
,而FutureTask
实现了RunnableFuture
这个接口,而RunnableFuture
派生与Runnable
这个接口——所以,实际上是把Callable
包装成一个Runable
交给一个Thread
去执行。
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "-call方法正在运行");
return "Hello World";
}
}
public class OnlyMain {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(Thread.currentThread().getName() + "-run方法正在执行");
}
}
// main-run方法正在执行
// Thread-0-call方法正在运行
以下是Callable
、FutureTask
、RunnableFuture
相关源码:
public interface Callable<V> {
V call() throws Exception;
}
public class FutureTask<V> implements RunnableFuture<V> {
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
3 启动和终止线程
3.1 构造线程
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon
线程等信息。 以下是Thead
的源码:
public class Thread implements Runnable {
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 当前线程就是该线程的父线程
Thread parent = currentThread();
if (g == null) {
g = parent.getThreadGroup();
}
g.addUnstarted();
this.group = g;
// 将daemon、priority属性设置为父线程的对应属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
init2(parent);
this.stackSize = stackSize;
// 分配一个线程ID
tid = nextThreadID();
}
}
在上述过程中,一个新构造的线程对象是由其parent
线程来进行空间分配的,而child
线程继承了parent
是否为Daemo
n、优先级和加载资源的contextClassLoader
以及可继承的ThreadLocal
,同时还会分配一个唯一的ID
来标识这个child
线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。
3.2 启动线程
线程对象在初始化完成之后,调用start()
方法就可以启动这个线程。
线程start()
方法的含义是:当前线程(即parent
线程)同步告知Java
虚拟机,只要线程规划器空闲,应立即启动调用start()
方法的线程。注意:启动一个线程前,最好为这个线程设置线程名称。
Thread
中run()
与start()
的区别?
run()
和普通的成员方法一样,可以被重复调用。但是如果单独调用run
方法,则不是在子线程中执行。
start()
方法只能被调用一次。调用这个方法后程序会启动一个新的线程来执行run()
方法。注意:调用start()
方法后,线程处于就绪状态,一旦得到CPU
时 间片,就开始执行run()
方法。run()
方法结束后,线程则立即终止。
3.3 理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。通过调用该线程的interrupt()
方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,通过方法isInterrupted()
来进行判断是否被中断,也可以调用静态方法Thread.interrupted()
对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()
时依旧会返回false
。 以下是源码:
public class Thread implements Runnable {
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
@FastNative
public static native boolean interrupted();
@FastNative
public native boolean isInterrupted();
}
interrupt()
:安全中断,发送一个通知告知目标线程将要中断,是否中断由线程本身决定,设置标志位flag = true
(中断标志位表示当前线程已经中断)。 因为Java
中的线程是协作式的,不是抢占式的,线程通过检查自身的中断标志位是否被置为true
来进行响应。Thread.interrupted
:判断当前线程是否被中断,并清理中断状态,设置标志位flag = false
。isInterrupted
:判断当前线程是否被中断。
class UserThread extends Thread {
public UserThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " Thread = start = interrupt: " + isInterrupted());
// while (!isInterrupted()) {
while (!interrupted()) {
System.out.println(threadName + " Thread = while = interrupt: " + isInterrupted());
}
System.out.println(threadName + " Thread = end = interrupt: " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UserThread userThread = new UserThread("cah");
userThread.start();
userThread.sleep(2);
userThread.interrupt();
}
// isInterrupted:
// cah Thread = start = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = end = interrupt: true
// interrupted
// cah Thread = start = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = end = interrupt: false
注意:处于死锁状态的线程无法被中断
从Java
的API
中可以看到,许多声明抛出InterruptedException
的方法(例如Thread.sleep(long millis)
方法)。这些方法在抛出InterruptedException
之前,Java
虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException
,此时调用isInterrupted()
方法将会返回false
。
下面代码中,首先创建了两个线程,SleepThread
和BusyThread
,前者不停地睡眠,后者一直运行,然后对这两个线程分别进行中断操作,观察二者的中断标识位。
public class Interrupted {
public static void main(String[] args) throws InterruptedException {
// sleepThread不停的尝试睡眠
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
// busyThread不停的运行
Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠5秒,让sleepThread和busyThread充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
// 防止sleepThread和busyThread立刻退出
SleepUtils.second(2);
}
static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
SleepUtils.second(10);
}
}
}
static class BusyRunner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
}
// SleepThread interrupted is false
// BusyThread interrupted is true
// java.lang.InterruptedException: sleep interrupted
从结果可以看出,抛出InterruptedException
的线程SleepThread
,其中断标识位被清除了, 而一直忙碌运作的线程BusyThread
,中断标识位没有被清除。
3.4 过期的suspend()
、resume()
和stop()
suspend [səˈspend] 暂停,中止;
线程的暂停、恢复和停止对应在Thread
的API
就是suspend()
、resume()
和stop()
。但是这些API
都是过期的,不建议使用。 以下是源码:
public class Thread implements Runnable {
@Deprecated // 弃用
public final void stop() {
throw new UnsupportedOperationException();
}
@Deprecated
public final void suspend() {
throw new UnsupportedOperationException();
}
@Deprecated
public final void resume() {
throw new UnsupportedOperationException();
}
}
下面代码中,创建了一个线程PrintThread
,它以1
秒的频率进行打印,而主线程对其进行暂停、恢复和停止操作:
public class Deprecated {
public static void main(String[] args) throws InterruptedException {
DateFormat format = new SimpleDateFormat("HH:mm:ss");
Thread printThread = new Thread(new Runner(), "PrintThread");
printThread.setDaemon(true);
printThread.start();
TimeUnit.SECONDS.sleep(3);
printThread.suspend();
System.out.println("main suspend PrintThread at " + format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
printThread.resume();
System.out.println("main resume PrintThread at " + format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
printThread.stop();
System.out.println("main stop PrintThread at " + format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
}
static class Runner implements Runnable {
@Override
public void run() {
DateFormat format = new SimpleDateFormat("HH:mm:ss");
while (true) {
System.out.println(Thread.currentThread().getName() + " Run at " + format.format(new Date()));
SleepUtils.second(1);
}
}
}
}
//PrintThread Run at 09:19:38
//PrintThread Run at 09:19:39
//PrintThread Run at 09:19:40
//main suspend PrintThread at 09:19:41
//PrintThread Run at 09:19:44
//main resume PrintThread at 09:19:44
//PrintThread Run at 09:19:45
//PrintThread Run at 09:19:46
//main stop PrintThread at 09:19:47
在执行过程中,PrintThread
运行了3
秒,随后被暂停,3
秒后恢复,最后经过3
秒被终止。 通过示例的输出可以看到,suspend()
、resume()
和stop()
方法完成了线程的暂停、恢复和终止工作,而且非常“人性化”。
stop()
:即刻停止run()
方法中剩余的部分工作,包括在catch
或finally
语句中的操作,并抛出异常,因此可能导致一些清理性的工作得不到完成,如文件、数据库的关闭等;立即释放该线程所持有的资源(比如锁),导致数据得不到同步处理。 如下所示:
class TestObject {
private String first = "ja";
private String second = "va";
public synchronized void print(String first, String second) throws InterruptedException {
this.first = first;
Thread.sleep(10000);
this.second = second;
}
public String getFirst() {
return first;
}
public String getSecond() {
return second;
}
}
public class OnlyMain {
public static void main(String[] args) throws InterruptedException {
TestObject testObject = new TestObject();
Thread t1 = new Thread(() -> {
try {
testObject.print("1", "2");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
t1.stop();
System.out.println("first: " + testObject.getFirst() + " second: " + testObject.getSecond());
}
}
// first: 1 second: va
从程序的验证结果来看,stop()
确实是不安全的:释放该线程持有的所有锁,一般任何进行加锁的代码块都是为了保护数据的一致性,如果在调用stop()
后导致该线程所持有的锁突然释放,那么被保护的数据就有可能出现不一致的情况。
resume()
&suspend()
:这两个操作必须要成对出现,否则非常容易发生死锁。suspend()
在导致线程暂停(进入睡眠状态、挂起)的同时并不会释放任何资源,其他线程也无法访问被它占用的锁,直到对应的线程执行resume()
方法后,被挂起的线程才能继续。 代码如下所示:
class TestObject {
public synchronized void print() {
if (Thread.currentThread().getName().equals("A")) {
System.out.println("A线程独占该资源");
Thread.currentThread().suspend();
}
}
}
public class OnlyMain {
public static void main(String[] args) throws InterruptedException {
TestObject testObject = new TestObject();
Thread t1 = new Thread(() -> testObject.print());
t1.setName("A");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
System.out.println("B已启动,但无法调用print方法");
testObject.print();
});
t2.setName("B");
t1.start();
}
}
// A线程独占该资源
// Exception in thread "main" java.lang.IllegalThreadStateException
// at java.lang.Thread.start(Thread.java:708)
// at com.cah.kotlintest.OnlyMain.main(OnlyMain.java:70)
注意:正因为suspend()
、resume()
和stop()
方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用等待/通知机制来替代。
3.5 安全地终止线程
线程自然终止:run()
方法执行完成或者抛出一个未处理的异常导致线程提前结束。
中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean
变量来控制是否需要停止任务并终止该线程。
以下代码中创建了一个线程CountThread
,它不断地进行变量累加,而主线程尝试对其进行中断操作和停止操作:
public class Shutdown {
public static void main(String[] args) throws InterruptedException {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountTread");
countThread.start();
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
//Count i = 557192031
//Count i = 562538988
示例在执行过程中,main
线程通过中断操作和cancel()
方法均可使CountThread
得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。
线程控制就好比你控制了一个工人为你干活:
- 挂起:你主动对工人说:你去睡觉吧,等我用的着你的时候会去主动叫你的,然后再干活
- 睡眠:你主动对工人说:你去睡觉吧,等某时某刻过来接着干活
- 阻塞:你发现,你的工人没有经过你的允许去睡觉了,但是这不能怪他,因为你没有给他准备干活的工具,他只好去睡觉了。至于工具回来了之后,工人会不会知道,会不会继续干活,时不需要担心的,因为他一旦发现工具回来了,它会自己去干活的。
4 线程优先级
线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多处理器资源的线程属性。
在Java
线程中,通过一个整型成员变量priority
来控制优先级,优先级的范围从1~10
,在线程构建的时候可以通过setPriority(int)
方法来修改优先级,默认优先级是5
,优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时,针对频繁阻塞(休眠或者I/O
操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU
时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM
以及操作系统上,线程规划会存在差异, 有些操作系统甚至会忽略对线程优先级的设定,示例代码:
public class MultiThread {
private static volatile boolean notStart = true;
private static volatile boolean notEnd = true;
public static void main(String[] args) throws InterruptedException {
List<Job> jobs = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
Job job = new Job(priority);
jobs.add(job);
Thread thread = new Thread(job, "Thread" + i);
thread.setPriority(priority);
thread.start();
}
notStart = false;
TimeUnit.SECONDS.sleep(10);
notEnd = false;
for (Job job : jobs) {
System.out.println("Job Priority: " + job.priority + ", Count: " + job.jobCount);
}
}
static class Job implements Runnable {
private int priority;
private long jobCount;
public Job(int priority) {
this.priority = priority;
}
@Override
public void run() {
while (notStart) {
Thread.yield();
}
while (notEnd) {
Thread.yield();
jobCount++;
}
}
}
}
// Job Priority: 1, Count: 1203153
// Job Priority: 1, Count: 1203077
// Job Priority: 1, Count: 1203070
// Job Priority: 1, Count: 1202243
// Job Priority: 1, Count: 1202562
// Job Priority: 10, Count: 1201401
// Job Priority: 10, Count: 1201750
// Job Priority: 10, Count: 1203001
// Job Priority: 10, Count: 1202480
// Job Priority: 10, Count: 1202363
从输出可以看到线程优先级没有生效,优先级1
和优先级10
的Job
计数的结果非常相近,没有明显差距。这表示程序正确性不能依赖线程的优先级高低。
注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java
线程对于优先级的设定。
参考
https://www.zhihu.com/question/42962803
https://www.jianshu.com/p/7a123f212ca1
https://www.cnblogs.com/mjtabu/p/12694964.html
https://blog.csdn.net/qq_32907195/article/details/107379232?utm_medium=distribute.pc_relevant.none-task-blog-title-3&spm=1001.2101.3001.4242
https://blog.csdn.net/sdfs__sdfsd/article/details/103154221?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.not_use_machine_learn_pai&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.not_use_machine_learn_pai