关于 Thread 的一些代码

哈哈哈哈哈哈哈!

这一篇文章参考《Java 并发编程之美》的第一章,实现一下里面的一些示例。

线程的创建方式

让一个类可以作为线程运行有三种方式——继承Thread类并重写run方法,实现Runnable接口并实现run方法,使用FutureTask方式。其中第二个方式需要通过Thread类的接受Runnable对象的构造器进行,它也可以使用 lambda 表达式或匿名实现类来进行,因为Runnable是一个函数式接口。

继承方法代码如下——

1
2
3
4
5
6
7
8
9
10
class ThreadFromExtends extends Thread {
@Override
public void run() {
System.out.println("Hello, Happy World!");
}
public static void main(String[] args) {
Thread instance = new ThreadFromExtends(); // 直接调用构造函数即可生成这样的一个线程的实例
instance.start();
}
}

通过实现Runnable接口的方法如下——

1
2
3
4
5
6
7
8
9
10
class ThreadFromImplements implements Runnable {
@Override
public void run() {
System.out.println("I was happy");
}
public static void main(String[] args) {
Thread instance = new Thread(new ThreadFromImplements()); // Thread 类接受一个 Runnable 函数式接口实例
instance.start();
}
}

还可以使用匿名实现类和 lambda 表达式——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) throws Exception {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通过 Runnable 接口的匿名实现类来实现创建 Thread");
}
});
Thread threadB = new Thread(()->{
System.out.println("有 lambda 表达式,方便很多!");
});
Thread threadC = new Thread(Main::method);
}
public static void method() {
System.out.println("也可以用方法作为参数,这和 lambda 是一致的");
}
}

FutureTask 方式等将来遇到再说。

synchronized,wait,notify

Java 中任何对象都有监视器锁(这是Object类所持有的,而任何对象都是Object对象)。其提供了waitnotifynotifyAll等方法用来进行某种线程间的通知和等待。这些方法都应当在同步代码块中进行,否则会抛出IllegalMonitorStateException异常。

当线程进入某个对象的同步代码块(使用synchronized关键字定义的代码块或方法),它就持有该对象的监视器锁。此时,任何其它线程都将被阻塞到这个对象的同步代码块之外,除非它们获得锁。

如果线程调用wait方法,它将被阻塞挂起,并释放锁。该线程遇到两个情况会恢复到 runnable 状态——其它线程调用notifynotifyAll方法将其唤醒,或该线程被 interrupted(也就是该线程的interrupt方法被调用)。

线程从挂起到可以运行可能在没有被 notify,没有被 interrupt,没有超时的时候也能发生,这称作虚假唤醒,为防虚假唤醒,应当用 while 包裹 wait 方法(当然,虚假唤醒仅仅是其中一个因素,如果该线程被唤醒后发现其执行的条件不满足,其应当必须再次放弃锁并挂起阻塞)。

1
2
3
4
5
6
// 防止虚假唤醒
synchronized (obj) {
while (条件不满足)
obj.wait();
...
}

下面给出一个简单的生产者消费者的例子,使用监视器锁来协调消费者线程和生产者线程的同步。

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 Main {
static Queue<Object> queue = new LinkedList<>();
static final int MAX_SIZE = 10; // 我居然试图给它加上 volatile,我在干什么?既然 final 是不可修改的,它就无所谓可见性了——不会被改变的东西,就无所谓对它的修改能不能让所有线程都及时看到
static final Object ele = new Object(); // 用来填充 queue

static void produce() { // 生产函数
synchronized (queue) { // 在生产时,不允许消费
while (queue.size() == MAX_SIZE)
try {
queue.wait(); // while 不能加到 try 里,因为还要再执行呢,即使被中断了,循环还是要继续做的
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.add(ele);
System.out.println("生产 1");
queue.notifyAll(); // 这里只有两个线程,所以用 notify 和 notifyAll 是没有区别的。
}
}

static void consume() { // 消费函数
synchronized (queue) {
while (queue.size() == 0)
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.remove();
System.out.println("消费 1");
queue.notifyAll();
}
}
public static void main(String[] args) throws Exception {
Thread producer = new Thread(()->{while (true) produce();});
Thread consumer = new Thread(()->{while (true) consume();});
producer.start();
consumer.start();
Thread.sleep(5);
System.exit(0);
}
}

调用 wait 时,线程会释放锁并阻塞,因为 wait 方法而阻塞的线程可以是多个的。如果有线程调用 notify,则其中一个线程会被唤醒,如果调用 notifyAll,则所有阻塞在 wait 方法的线程都会被唤醒。

调用 wait 方法时,只有该锁会被释放,该线程持有的其它锁是不会被释放的,下面用代码展示,线程 A 持有资源 1 后持有资源 2 并阻塞,线程 B 延迟执行并试图持有资源 2 然后持有资源 1——

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 static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
Thread threadA = new Thread(() -> {
synchronized (resource1) {
System.out.println("线程 A 持有资源 1");
System.out.println("线程 A 试图持有资源 2");
synchronized (resource2) {
System.out.println("线程 A 持有资源 2");
System.out.println("线程 A 释放资源 2 并阻塞");
try {
resource2.wait();
} catch (InterruptedException e) {
}
}
}
System.out.println("线程 A 释放资源 1");
});
Thread threadB = new Thread(() -> {
System.out.println("线程 B 试图获取资源 2");
synchronized (resource2) {
System.out.println("线程 B 获取资源 2\n 线程 B 试图获取资源 1");
synchronized(resource1) {}
}
});
threadA.start();
threadB.start();
}

/*
输出为——

线程 A 持有资源 1
线程 A 试图持有资源 2
线程 A 持有资源 2
线程 A 释放资源 2 并阻塞
线程 B 试图获取资源 2
线程 B 获取资源 2
线程 B 试图获取资源 1

*/

可以看到,线程 A 被阻塞后只释放了资源 2 的锁,仍持有资源 1 的锁。

yield

yield 是一个静态方法,它表示当前线程让出剩余的时间片,立即进行线程调度。yield 并不常用。线程调度器可以无条件忽略该请求。(如果被忽略,这剩余的时间里它都要阻塞吗?)

给两个例子——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void yieldTest() {
final Thread ME = Thread.currentThread();
for (int i = 0; i < 5; i++) {
if (i % 5 == 0) {
System.out.println(ME+" yield");
Thread.yield();
}
}
System.out.println(ME+" Over");
}
public static void main(String[] args) throws Exception {
new Thread(Main::yieldTest).start();
}
}

结果似乎同书中所讲的不一样……搁置。

中断

当线程阻塞在waitsleepjoin的时候如果被其它线程中断,将会抛出InterruptedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static synchronized void interruptTest() {
try {
System.out.println("a 启动并阻塞");
Main.class.wait(); // 对于静态方法,应当这样使用,对于实例方法,应该 this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("被中断啦");
return;
}
System.out.println("这一段不可能被执行,除非虚假唤醒了");
}
public static void main(String[] args) throws Exception {
Thread a = new Thread(Main::interruptTest);
a.start();
Thread.sleep(10);
a.interrupt();
a.join(); // join 方法等待线程结束
}
}

与查看和设置对象的中断标志位相关的方法有三个——interruptisInterruptedinterrupted。其中 interrupted 是 static 方法,它处理调用该方法的线程(这就是为什么它采取过去式的形式)。

interrupt方法中断线程,它实质是设置线程的中断标志位为 true,并非是直接“终结”线程。如果该线程被中断时阻塞在 wait,sleep 或 join 方法中,它会在这些地方抛出 InterruptedException。

isInterrupted方法检查线程是否被中断。

interrupted检查当前线程是否中断。如果中断,则返回 true,并清除中断标志位。如果未被中断,则返回 false。总之无论如何,调用该方法后中断标志位会被重置为 false。调用它应该使用 Thread.interrupted() 的形式。

一个不断检查中断并在被中断时优雅退出的例子是——

1
2
3
4
5
6
7
8
9
10
11
12
public void run() {
try {
// ...
while (!Thread.currentThread().isInterrupted() && workNotOver()) {
// 干活!
}
} catch (InterruptedException e) { // 如果中途被中断
// 在 sleep,wait 或 join 的时候被中断了,进行一些操作
} finally {
// 必要的清理
}
}

死锁

当所有线程都因为无法获得所需资源被阻塞,这种情况就称为死锁。一个典型的例子是,线程 A 独占资源 1, 线程 B 独占资源 2, 此时线程 A 请求资源 2 才能继续进行,线程 B 请求资源 1 才能继续进行,A 和 B 因此就都无法进行了。

HR:只要你能给我解释死锁,我就给你 Offer。
我:只要你给我 Offer,我就给你解释死锁。

死锁有四个条件,任意条件不满足,死锁就不会发生——

  • 互斥条件:线程对资源的访问是排他性的,即该资源只容许被一个线程占用,如果有其它线程尝试获取,则只能等待该线程释放资源。
  • 请求并持有(不如说持有并请求):一个线程已经持有至少一个资源,但又要请求其他资源,而该其它资源被其它线程占用。
  • 不可剥夺:线程占有的资源只能由自己释放,其它线程无法干涉。
  • 环路等待:发生死锁时必然存在线程——资源的环形链。定义这样的线程集合{T0,T1,T2,..,Tn},其中 T0 请求 T1 占有的资源,T1 请求 T2 占有的资源……Tn 请求 T0 占有的资源。

考虑之前提的死锁的例子——

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 Main {
static final Object resource1 = new Object();
static final Object resource2 = new Object();

public static void main(String[] args) throws Exception {
// 线程 A
new Thread(() -> {
synchronized (resource1) {
System.out.println("A 获取资源 1");

try {
Thread.sleep(100);// 确保 B 已经获取资源 2
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("A 尝试获取资源 2");
synchronized (resource2) {
System.out.println("A 获取资源 2(不可能发生!)");
}
}
}).start();
//线程 B
new Thread(()->{
synchronized (resource2) {
System.out.println("B 获取资源 2");

try {
Thread.sleep(100);// 确保 A 已经获取资源 1
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("B 尝试获取资源 1");
synchronized (resource1) {
System.out.println("B 获取资源 1(不可能发生!)");
}
}
}).start();
}
}

synchronized 满足互斥条件和不可剥夺条件——任意资源只能同时被一个线程占有,锁只能被持有线程释放。

请求并持有条件也是满足的,A 在持有 1 的情况下请求 2,B 在持有 2 的情况下请求 1。这里也是满足环路等待的。

避免死锁通过让死锁的必要条件不满足来实现,但是我们只能破坏环路等待和请求并持有条件。在这里,只需要让 A 和 B 申请资源的顺序一致即可,也就是确定资源申请的有序性

守护线程和用户线程

Java 中线程分为两类,守护线程(daemon 线程,这个词在 linux 里也有用到,所谓的 systemd,这里的 d 就是 daemon)和是用户线程(user 线程)。JVM 中很多线程,如垃圾回收线程,是 daemon 进程。一个典型的用户线程是 main 方法的线程。

JVM 只有在所有用户线程都结束的时候才会退出。见该实例——

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception {
new Thread(()->{
while(true);
}).start();
System.out.println("主线程完了");
}
/*
这时候调用 jps,发现该 JVM 进程还没有退出。

aoymykn@aoymykn-PC:~/public/blog$ jps
21332 Main
21383 Jps
*/

可以通过setDaemon方法设置该线程为 daemon 线程。该方法不能在线程执行的时候调用,否则会抛IllegalThreadStateException

ThreadLocal

ThreadLocal 类能够让每个线程对变量进行访问的时候,访问的是自己线程的变量(也就是说相当于把状态变量变成局部变量,给了它封闭性了)。

ThreadLocal 变量的行为是,让访问这个变量的每个线程都有该变量的一个本地副本,当线程对该变量进行操作的时候,实际上是操作自己本地内存(栈)里面的变量。(这样是不是就能避免本地缓存和主内存不一致的问题,从而保证可见性?它可以替代 volatile 吗?)

剩下的,之后再说。

并发真是个麻烦的东西……再次认识到,现在的编程语言对并发的抽象是差劲的,完全不能从编程语言本身来判断结果……必须要付诸到机器底层……好家伙,这更暴露出来了我的基本功不足。需要加强了!


思来想去,决定现在先搁置并发,对我来说,这样一个全新(而且庞大,晦涩)的领域如果一头扎进去,我基本上在毕业之前就找不到工作了。

当前需要专注的东西实在是非常明显的——Java EE,框架,设计模式,计网以及数据结构和算法!别再雨露均沾了!