17Java多线程之线程创建

Thread类

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是,Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由 Java 去调用这样的东西,然后提供一些类(Thread 类)供我们使用。我们就可以实现多线程程序了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
构造方法:
public Thread()
public Thread(Runnable target)
...
成员方法:
public static Thread currentThread() // 返回对当前正在执行的线程对象的引用。

public final String getName() // 返回该线程的名称。
public final void setName(String name) // 改变线程名称,使之与参数 name 相同。
public final int getPriority() // 返回线程的优先级。
public final void setPriority(int newPriority) // 更改线程的优先级。
public final boolean isDaemon()
public final void setDaemon(boolean on) // 守护线程。将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。

public static void sleep(long millis) // 休眠线程。在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),
public final void join() // 等待线程。等待这个线程死亡。其实还是调用Object的wait()方法,this.wait()和thread.join()区别?
public static void yield() // 礼让线程。暂停当前正在执行的线程对象,并执行其他线程。


public final void stop() // 过时。强迫线程停止执行,毫无征兆,过于暴力,不建议。
/** 中断线程。
* 除非当前线程正在中断自己,这总是允许的,否则会调用该线程的 checkAccess 方法,这可能会导致抛出 SecurityException。
****** 如果该线程因调用 Object 类的 wait()、wait(long)、wait(long, int) 方法,或 Thread 类的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int) 方法而被阻塞,那么它的阻塞状态会被清除并且会收到一个 InterruptedException。
* 如果该线程在 InterruptibleChannel 上的 IO 操作中被阻塞,则通道将关闭,线程的中断状态将被设置,线程将收到 java.nio.channels.ClosedByInterruptException。
* 如果该线程在 java.nio.channels.Selector 中被阻塞,则该线程的中断状态将被设置,并且它将立即从选择操作返回,可能带有非零值,就像调用了选择器的 wakeup 方法一样。
****** 如果前面的条件都不成立,则将设置该线程的中断状态。
* 中断一个不活动的线程不需要有任何效果。
*/
public void interrupt() // 不能中断正在运行过程中的线程(在代码没有特殊处理中断状态的前提下),只能中断阻塞过程中的线程。
public boolean isInterrupted() // 测试此线程是否已被中断。线程的中断状态不受此方法的影响。由于在中断时线程不活动而被忽略的线程中断将通过此方法返回 false 来反映。

/**
* 注意此方法是 static 修饰
* 测试当前线程是否被中断。注意:通过该方法将会清除线程的中断状态,后续调用interrupted()、isInterrupted()将返回false。
* 换句话说,如果这个方法被连续调用两次,第二次调用将返回 false(除非当前线程再次被中断,在第一次调用清除其中断状态之后,第二次调用检查它之前)。
*
* 返回值: true - 清除中断状态成功,后续调用interrupted()、isInterrupted()将返回false。
*/
public static boolean interrupted()

public final native boolean isAlive() // 测试此线程是否存活。 如果线程已启动且尚未死亡,则该线程处于活动状态为true。
public static void dumpStack() // 将当前线程的堆栈跟踪打印到标准错误流。 此方法仅用于调试。
public static native boolean holdsLock(Object obj) // 当且仅当当前线程持有指定对象的监视器锁时,才返回 {@code true}。此方法旨在允许程序断言当前线程已持有指定的锁:assert Thread.holdsLock(obj);

通过查看构造方法,我们知道了有两种方式实现多线程程序。

方式一:继承Thread类

步骤

  1. 自定义类MyThread继承Thread类。
  2. MyThread类里面重写run()。
  3. 创建对象
  4. 启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.util.Date;
public class MyThreadDemo {
public static void main(String[] args) {
/********************************************************************************/

// 创建线程对象,无参构造+setXxx()
// MyThread my1 = new MyThread();
// MyThread my2 = new MyThread();
// my1.setName("周润发"); // 设置线程名称
// my2.setName("刘德华");
// my1.start();
// my2.start();
// my1.start(); // IllegalThreadStateException:非法的线程状态异常。// 为什么呢?因为这个相当于是my1线程被调用了两次。而不是启动两个线程。

/********************************************************************************/

// 带参构造方法给线程起名字
MyThread my1 = new MyThread("周润发AAA");
MyThread my2 = new MyThread("刘德华BBB");
MyThread my3 = new MyThread("梁朝伟CCC");

// 更改线程优先级,默认是5
// my1.setPriority(10); // 最大值,线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
// my2.setPriority(1); // 最小值
// 获取默认优先级
System.out.println(my1.getPriority());
System.out.println(my2.getPriority());
System.out.println(my3.getPriority());
// 获取main方法所在的线程对象的名称
System.out.println(Thread.currentThread().getName());

my1.start();
// try {
// my1.join(); // 等待该线程终止
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
my2.start();
my3.start();

/********************************************************************************/
}
}
class MyThread extends Thread {
public MyThread() {}
public MyThread(String name){
super(name);
}
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x + ",日期:" + new Date());

// try {
// Thread.sleep(1000);// 线程休眠
// } catch (InterruptedException e) {
// e.printStackTrace();
// }

// Thread.yield();// 暂停当前正在执行的线程对象,并执行其他线程。
}
}
}

方式二:实现Runnable接口

步骤:

  1. 自定义类Task实现Runnable接口

  2. 重写run()方法

  3. 创建Task类的对象

  4. 创建Thread类的对象,并把”步骤3”的对象作为构造参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TaskDemo {
public static void main(String[] args) {
// 创建Task类的对象
Task task = new Task();

// 创建Thread类的对象,并把3)步骤的对象作为构造参数传递
// Thread(Runnable target)
// Thread t1 = new Thread(task);
// Thread t2 = new Thread(task);
// t1.setName("周润发AAA");
// t2.setName("刘德华BBB");

// Thread(Runnable target, String name)
Thread t1 = new Thread(task, "周润发AAA");
Thread t2 = new Thread(task, "刘德华BBB");

t1.start();
t2.start();
}
}
class Task implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
// 由于实现接口的方式就不能直接使用Thread类的getName()方法了,但是可以间接的使用
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}

使用匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* 匿名内部类的格式:
* new 类名或者接口名() {
* 重写方法;
* };
* 本质:是该类或者接口的子类对象。
*/
public class ThreadDemo {
public static void main(String[] args) {
// 继承Thread类来实现多线程
new Thread() {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}.start();

// 实现Runnable接口来实现多线程
new Thread(new Runnable() {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}) {
}.start();

// 更有难度的。实际上是一个实现Runnable接口的线程被另一个类继承并重写了run方法
new Thread(new Runnable() {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println("hello" + ":" + x);
}
}
}) {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println("world" + ":" + x);
}
}
}.start();
}
}

方式三:Callable和Future创建线程

之前提到的不管是继承Thread类还是实现Runnable接口,都有两个问题,第一个是无法抛出更多异常,第二个是线程执行完毕后无法获得线程返回值

下面的实现方式就可以满足这种需求。

Future 模式,jdk5 开始,使用这种方式创建线程比上面两种方式要复杂一些,步骤如下:

  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体。这个接口类似于Runnable接口,但比Runnable接口强大,增加了异常和返回值。
  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。 public FutureTask(Callable<V> callable)
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。 public Thread(Runnable target)
  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。get() 方法:如有必要,等待计算完成,然后检索其结果。亦可以给定等待时间。

观察类图,FutureTask 重写了 run() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
* 带返回值的方式
*/
public class MyFutureDemo {
public static void main(String[] args) {
// 步骤一:创建 Callable 接口的实现类,并实现 call() 方法。创建 Callable 实现类的实例
Callable<Integer> call = () -> {
System.out.println("线程任务开始,call()方法执行....");

int a = RandomUtils.randomInt(1, 3);
int b = RandomUtils.randomInt(1, 3);
if (a != b) {
System.out.println("两个随机数不相等");
throw new Exception("两个随机数不相等");
} else {
System.out.println("两个随机数相等");
}

return a;
};
// 步骤二:使用 FutureTask 类来包装 Callable 对象
FutureTask<Integer> task = new FutureTask<>(call); // 提供了检查任务执行情况和检索结果的方法,具体查看Future接口。
System.out.println("task.isDone(): " + task.isDone());
// 步骤三:使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
new Thread(task).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task.isDone(): " + task.isDone());

// --------------- 这里是在线程启动之后,线程结果返回之前 ---------------
System.out.println("-------------- 这里可以为所欲为 --------------");
// --------------- 这里是在线程启动之后,线程结果返回之前 ---------------

// 步骤四:为所欲为完毕后,调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值
// Integer result = task.get(1, TimeUnit.SECONDS);
Integer result = null;
while (result == null || !task.isDone()) {
try {
// 可以多次调用
result = task.get();
} catch (InterruptedException e) {
e.printStackTrace();
result = 0;
} catch (ExecutionException e) {
e.printStackTrace();
result = 0;
}
}
System.out.println("task.isDone(): " + task.isDone());
System.out.println("主线程中拿到异步任务执行的结果为:" + result);
}
}

执行结果:

1
2
3
-------------- 这里可以为所欲为 --------------
线程任务开始,call()方法执行....
主线程中拿到异步任务执行的结果为:1

获取当前正在执行的线程

1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println(thread.toString()); // Thread[#1,main,5,main],分别是线程的 id、name、priority、线程组名称
}
}

三种线程创建方式对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package java.lang;
public class Thread implements Runnable {
@Override
public void run() {
Runnable task = holder.task;
if (task != null) {
Object bindings = scopedValueBindings();
runWith(bindings, task);
}
}
}

package java.lang;
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

package java.util.concurrent;
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}

继承Thread 实现Runnable接口 实现Callable接口
线程方法 run() run() call()
返回值 No No Yes
接口抛异常 No No Yes
java.lang; java.lang; java.util.concurrent;
启动线程 支持 不支持,需借助Thread 不支持,需借助Thread
获取当前线程(Thread对象) this Thread.currentThread() Thread.currentThread()
重复利用 多个 Thread 可以共享同一个Runnable对象,适合多线程处理同一份资源,注意线程安全。
体现了面向对象思想。
多个 Thread 可以共享同一个Callable对象,适合多线程处理同一份资源,注意线程安全。
体现了面向对象思想。
使用场景 单继承 需要继承其他类 需要继承其他类
与FutureTask关系 new Thread(Runnable task) new FutureTask(Runnable runnable, V result),利用Future对象可获取线程执行状态、取消任务、获取结果。 new FutureTask(Callable<V> callable),利用Future对象可获取线程执行状态、取消任务、获取结果。

run()和start()区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Thread implements Runnable {
@Override
public void run() {
Runnable task = holder.task;
if (task != null) {
Object bindings = scopedValueBindings();
runWith(bindings, task);
}
}

public void start() {
synchronized (this) {
// zero status corresponds to state "NEW".
if (holder.threadStatus != 0)
throw new IllegalThreadStateException();
start0();
}
}
}

run() start()
位置 Thread类是重写Runnable接口的run() 位于Thread类
类型 非同步方法 同步方法
作用 用来包含被线程执行的代码 启动线程,之后JVM会调用run()
是否会产生新线程 不会 产生一个新线程
调用次数 允许调用无数次(普通方法调用) 只能调用一次,因为线程不可重复启动

线程的停止与中断

Thread 的两个方法 stop()interrupt()

stop()

暴力停止,甚至都没有释放资源的机会,已过时。

如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 释放锁,那其他线程就再也没机会获得 ReentrantLock 锁。

interrupt()

从线程的生命周期了解到,一个线程要中断,必须要从 runnable 状态到 terminated。而线程可能处于阻塞状态,非静态方法 interrupt() 可以将 任何阻塞状态 的线程转为 runnable 状态,并设置或清除中断状态,可能会抛出异常

针对不同阻塞方式抛出的异常不同(来自方法源码注释):

  • 若该线程处于 等待阻塞(WAITING、TIMED_WAITING),则其中断状态将被清除,并且将收到 InterruptedException在实际开发中,捕捉到 InterruptedException 终止任务即可,若想监视系统状态可以调用 interrupt() 方法重新设置中断状态。
  • 如果该线程在 InterruptibleChannel 上的 I/ O 操作中被阻塞,则通道将被关闭,线程的中断状态将被设置,并且线程将收到 java. nio. channels. ClosedByInterruptException
  • 如果该线程在java. nio. channels. Selector中被阻塞,则该线程的中断状态将被设置,并且它将立即从选择操作返回,可能返回一个非零值,就像调用选择器的wakeup方法一样。
  • 如果前面的条件都不成立(非waiting状态),则该线程的中断状态将被设置。

注意:响应中断指令或捕获到 InterruptedException 的任务才有可能终止。具体还要看代码怎么写。

获取中断状态

测试线程是否已被中断,有两种方式:

  • Thread.currentThread().isInterrupted()非静态方法,通过 Thread 对象调用,测试指定线程。不改变线程的中断状态。推荐
  • Thread.interrupted()静态方法,通过 Thread 类调用,测试当前线程。通过该方法清除线程的中断状态。换句话说,如果连续调用此方法两次,第二次调用将返回 false(除非当前线程在第一次调用清除其中断状态之后,在第二次检查中断状态之前,再次被中断)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ThreadStopDemo {
public static void main(String[] args) throws InterruptedException {
ThreadStop ts = new ThreadStop();
ts.start();

System.out.println("外部获取线程状态1: " + ts.getState() + "\n"); // RUNNABLE
TimeUnit.SECONDS.sleep(3);
ts.interrupt(); // 中断该线程
TimeUnit.SECONDS.sleep(3); // 为了等待线程被中断
System.out.println("\n" + "外部获取线程状态2: " + ts.getState()); // TERMINATED
}
}

class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("Begin...");
while (!Thread.currentThread().isInterrupted()) {
Thread.yield();
}
System.out.println("是否中断1: " + Thread.currentThread().isInterrupted()); // true
System.out.println("是否中断2: " + Thread.interrupted()); // true,该方法会清除线程的中断状态,源码注释很清晰
System.out.println("是否中断3: " + Thread.interrupted()); // false
System.out.println("线程被中断了");
// 在run方法中获取当前线程状态一定是RUNNABLE,因为执行到这个方法时它就在运行,一般在其他线程获取,用于监视系统状态。
System.out.println("内部获取线程状态1: " + Thread.currentThread().getState()); // 线程状态: RUNNABLE
System.out.println("End...");
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
外部获取线程状态1: RUNNABLE

Begin...
是否中断1: true
是否中断2: true
是否中断3: false
线程被中断了
内部获取线程状态1: RUNNABLE
End...

外部获取线程状态2: TERMINATED

中断状态被清除的示例,修改 ThreadStop 类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("Begin...");
while (!Thread.currentThread().isInterrupted()) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
// 抛出InterruptedException,中断状态被清除。
System.out.println("是否中断1: " + Thread.currentThread().isInterrupted()); // false
break;
}
}
System.out.println("End...");
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
外部获取线程状态1: RUNNABLE

Begin...
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep0(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:558)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
at org.example.ThreadStop.run(ThreadStopDemo.java:36)
是否中断1: false
End...

外部获取线程状态2: TERMINATED

线程优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统和JVM确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

守护线程

Java分为两种线程:用户线程和守护线程。所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

守护线程和用户线程的没啥本质的区别,唯一的不同之处就在于虚拟机的离开。如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 没有了被守护者,守护者也就不需要了。

1
2
3
Thread thread = new Thread();
thread.setDaemon(true); // 标记为守护线程或非守护线程
thread.isDaemon(); // 判断是否为守护线程

在使用守护线程时需要注意一下几点:

  • thread.setDaemon(true) 必须在 thread.start() 之前设置,否则会抛出一个 IllegalThreadStateException 异常。不能把正在运行的常规线程设置为守护线程。
  • 守护线程中产生的新线程也是守护线程。
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new PrintTask());
System.out.println("设置前: " + thread.isDaemon()); // 是否为守护线程
thread.setDaemon(true); // 标记为守护线程或非守护线程
System.out.println("设置后: " + thread.isDaemon()); // 是否为守护线程
thread.start();
TimeUnit.SECONDS.sleep(1);
}
}

class PrintTask implements Runnable {
@Override
public void run() {
System.out.println("任务开始运行");
while (true) {
Thread.yield();
}
}
}

判断线程是否存活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Task());
System.out.println("启动前: " + thread.isAlive()); // 测试该线程是否存活
thread.start();
System.out.println("启动后: " + thread.isAlive()); // 测试该线程是否存活

TimeUnit.SECONDS.sleep(1);
System.out.println("1秒后: " + thread.isAlive()); // 测试该线程是否存活
}
}

class Task implements Runnable {
@Override
public void run() {
}
}

输出:

1
2
3
启动前: false
启动后: true
1秒后: false

sleep(0)和sleep(1)区别

Thread.sleep(0) :挂起线程,线程将暂时放弃 CPU 的执行权,相当于一个让位动作。思考下面这两个问题:

1、假设现在是 1970-01-01 12:00:00.000,如果我调用一下 Thread.sleep(1000) ,在 1970-01-01 12:00:01.000 的时候,这个线程会不会被执行?

2、Thread.sleep(0); 这行代码是否有必要存在?

回顾操作系统原理:

在操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。

CPU 调度,就是根据一定的算法(优先级,FIFO等。。。),从就绪队列中选择一个线程来分配 CPU 时间片。

所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。因此可以看出,在抢占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU 。

在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经很长时间没有使用过 CPU 了),给他们算出一个总的优先级来。操作系统就会把 CPU 交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他。

sleep(long millis) 函数就是告诉操作系统“在未来的多少毫秒内我不参与 CPU 竞争”。

对于第一个问题,答案是:不一定。因为你只是告诉操作系统:在未来的 1000 毫秒内我不想再参与到 CPU 竞争。那么 1000 毫秒过去后,这时也许另外一个线程正在使用 CPU,那么这时操作系统是不会重新分配 CPU 的,直到那个线程挂起或结束;况且,即使这个时候恰巧轮到操作系统进行 CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU 还是可能被其他线程抢占去。

对于第二个问题,答案是:非常有必要。Thread.sleep(0) 的作用,就是“触发操作系统立刻重新进行一次 CPU 竞争”。竞争的结果可能是当前线程继续获得 CPU 执行权,也可能换成其他线程获得 CPU 执行权。这也是为什么在大循环里面经常会写一句 Thread.sleep(0) ,因为这样就给了其他线程获得 CPU 执行权的机会,界面就不会假死在那里。

线程没终止前,大致有三个状态:就绪状态,运行状态,阻塞状态。sleep(n) 之所以在 n 毫秒内不会参与 CPU 竞争,是因为当线程调用 sleep(n) 时,线程是由运行状态转为定时等待阻塞状态,线程被放入定时等待队列中,当 n 毫秒内发生中断或等待 n 毫秒后,线程才重新由定时等待阻塞状态转为就绪状态,被放入就绪队列中。等待队列中的线程是不参与 CPU 竞争的,只有就绪队列中的线程才会参与 CPU 竞争

Thread.sleep(0) CPU 占用 100%线程将很快被放入就绪队列,重新参与 CPU 竞争。是否会路过等待队列呢?

Thread.sleep(1) CPU 占用 1% ,测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
public static void main(String[] args) {
Task task = new Task();
// cpu核心数
int processors = Runtime.getRuntime().availableProcessors();
for (int i = 0; i < processors; i++) {
new Thread(task).start();
}
}
}

class Task implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

yield()==sleep(0)

Thread.yield()Thread.sleep(0) 语义实现取决于具体的 JVM 虚拟机,某些 JVM 可能什么都不做,大多数虚拟机会让线程放弃剩余的 CPU 时间片,立刻从 runnable 中的 运行状态 转为 就绪状态,放入就绪队列 。

join()

join() 方法是等待调用的线程终止,然后当前线程才能继续运行。主要起同步作用,使线程之间的执行从“并行”变成“串行”。也就是说,当我们在线程A中调用了线程B的 join() 方法时,线程A必须等待线程B终止才会继续执行。用于在多个线程之间添加排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Task(1, 3, null));
Thread thread2 = new Thread(new Task(2, 2, thread1));
Thread thread3 = new Thread(new Task(3, 1, thread2));
System.out.println(thread3.getState()); // 此时为新建状态 NEW

thread3.start();
thread2.start();
thread1.start();
}
}

class Task implements Runnable {
private Integer id;
private int sleepSeconds;
/**
* 等待的线程
*/
private Thread waitThread;

public Task(Integer id, int sleepSeconds, Thread waitThread) {
this.id = id;
this.sleepSeconds = sleepSeconds;
this.waitThread = waitThread;
}

@Override
public void run() {
try {
System.out.println("开始: " + id);
Thread.sleep(sleepSeconds * 1000);
if (waitThread != null) {
// waitThread.join(); // 观察使用join和不使用join的输出顺序
}
System.out.println("结束: " + id);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

wait()

wait() 方法的作用是让调用的线程进入等待状态,wait() 会与 notify() 和 notifyAll() 方法一起使用。notify() 和 notifyAll() 方法的作用是唤醒等待中的线程,notify()方法:唤醒单个线程,notifyAll()方法:唤醒所有线程。用于线程间通信,后面章节有介绍。

sleep、yield、join、wait

yield sleep join wait await
所在位置 Thread类 Thread类 Thread类 Object类 Condition类
方法类型 静态 静态 非静态 非静态
调用方式 Thread.yield() Thread.sleep(0) Thread对象.join() 锁对象.wait()
是否需要当前线程拥有锁 不需要 不需要 不需要 需要
何时恢复运行 立刻,等于sleep(0) 待睡眠时间结束 待等待时间结束,或 join 线程结束 待等待时间结束,或调用 notify() 唤醒 唤醒后进入就绪态
是否支持中断 不支持 调用 interrupt() 调用 interrupt() 调用 interrupt()
是否释放同步锁 不释放 不释放 不释放 释放 释放
线程状态 RUNNABLE TIME_WAITING WAITING、TIMED_WAITING WAITING、TIMED_WAITING
调用位置 任意位置 任意位置 任意位置 同步代码块中 同步代码块中
线程状态

测试是否释放锁。若只有一个任务 id 被打印,则说明不释放锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Task(1, null));
Thread thread2 = new Thread(new Task(2, thread1));
Thread thread3 = new Thread(new Task(3, thread2));

thread3.start();
thread2.start();
thread1.start();
}
}

class Task implements Runnable {
private Integer id;
/**
* 等待的线程
*/
private Thread waitThread;

public Task(Integer id, Thread waitThread) {
this.id = id;
this.waitThread = waitThread;
}

@Override
public void run() {
synchronized (this.getClass()) {
try {
System.out.println("开始: " + id);
if (waitThread != null) {
Thread.sleep(1 * 1000 * 1000 * 1000); // sleep不释放锁
// this.getClass().wait(1 * 1000 * 1000 * 1000); // wait释放锁
// waitThread.join(1 * 1000 * 1000 * 1000); // join不释放锁
}
System.out.println("结束: " + id);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

线程组

实际开发中很少用到线程组。

Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

Java多线程16:线程组

Java并发编程与技术内幕:ThreadGroup线程组应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Test {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("新建线程组1");

Task task = new Task();
Thread thread1 = new Thread(group, task, "thread1");
Thread thread2 = new Thread(group, task, "thread2");

thread1.start();
thread2.start();

try {
Thread.sleep(3000);

System.out.println("此线程组的名称:" + group.getName());

System.out.println("此线程组及其子组中活动线程数的估计值:" + group.activeCount());

System.out.println("将此线程组及其子组中的每个活动线程复制到指定的数组中:");
Thread[] arrary = new Thread[group.activeCount()];
int num = group.enumerate(arrary);
System.out.println("放入数组的线程数:" + num);

System.out.println("设置组的最大优先级,线程组中已经具有更高优先级的线程不受影响:");
group.setMaxPriority(5);

System.out.println("返回此线程组的父级:" + group.getParent());

System.out.println("中断此线程组中的所有线程:");
group.interrupt();
// 留出中断时间
Thread.sleep(3000);

System.out.println("测试此线程组是否已被销毁:" + group.isDestroyed());
System.out.println("销毁此线程组及其所有子组。此线程组必须为空,表示此线程组中的所有线程都已停止:");
group.destroy();
System.out.println("测试此线程组是否已被销毁:" + group.isDestroyed());

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class Task implements Runnable {
@Override
public void run() {
if (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getThreadGroup().getName() + " " + Thread.currentThread().getName());
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(1);
}
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println(e.getMessage());
}
}
System.out.println(Thread.currentThread().getName() + " 线程已中断 ");
}
}

注意事项

多线程中注意事项

有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。

通过对多线程的使用,可以编写出非常高效的程序。如果创建太多的线程,程序执行的效率实际上是降低了,而不是提升了,因为 CPU 花费在上下文的切换的时间多于程序执行的时间。