线程-二

[TOC]

线程间通信

等待/通知机制

在前面线程方法中我们遇到了wait();notify();方法。接下来我们讲讲这些方法。

wait()

wait方法的作用是将当前执行代码的线程进行等待。但wait并不是Thread的方法而是Object的方法。至于为什么不放在Thread请看这里为什么wait方法定义在Object,而不是Thread。这篇文章讲的很清楚了。
wait方法是将当前线程放入“预执行队列”中。并且在wait所在的代码处停止执行。直到接到通知或中断为止。同时需要注意。在调用wait方法之前。线程需要获得该对象的对象对象级别锁。如果调用wait时没有获取到锁则抛出异常。

wait(long)

带参数的方法表示超过这个时间就会自动唤醒。也就是等待多久。

notify()/notifyAll()

notify();方法恰好与wait相对。当线程使用wait方法等待时可以使用notify()将其唤醒。同理notify也需要在同步方法或同步代码块中调用。
notifyAll()notify()的功能一样。只不过notify()是唤醒一个线程。而notifyAll()是唤醒所有线程。(针对同一个对象的所有线程)。

使用
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

public class ThreadTest4 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread1 t1 = new Thread1(o);
t1.start();
Thread.sleep(100);
synchronized (o){
System.out.println("开始解锁");
o.notifyAll();
System.out.println("结束解锁");
}


}
}

class Thread1 extends Thread {
private Object lock;
Thread1(Object lock) {
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("wait开始");
lock.wait();
System.out.println("wait结束:");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

在线程run之后进入等待。同时主线程休眠0.5秒。0.5秒之后调用notify唤醒线程。所以在调用wait方法之后wait后面的代码就会停止执行。直到notify执行完毕。当唤醒之后wait后面的代码继续执行
线程之间的状态切换
每个对象都有两个队列。一个是阻塞队列,另一个是就绪队列。当线程被唤醒之后会进入就绪队列。反之则进入阻塞队列。

wait()/notify()/锁

wait执行完后。锁会自动释放。但是执行完notify却不会自动释放锁。从之前的代码中我们知道。代码运行到wait的时候会进行等待。所以wait后面的代码就不执行了。刚开始的时候我以为代码停在这了。所以锁也没释放。其实当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

public class ThreadTest4 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread1 t1 = new Thread1(o);
Thread1 t2 = new Thread1(o);
t1.start();
t2.start();
Thread.sleep(100);
synchronized (o){
System.out.println("开始解锁");
o.notifyAll();
System.out.println("结束解锁");
}


}
}

class Thread1 extends Thread {
private Object lock;
Thread1(Object lock) {
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("wait开始");
lock.wait();
System.out.println("wait结束:");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

通过运行的结果表明,在调用wait之后锁就已经被释放掉了。所有线程都要等待notify的同步代码块执行完毕才会继续执行

生产者/消费者模式实现

等待/通知最经典的案例就是“生产者/消费者”模式。

一生产与一消费
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

class P {
private String lock;

P(String lock) {
this.lock = lock;
}

public void setVale() {
try {
synchronized (lock) {
if (!"".equals(ThreadTestCP.value)) {
lock.wait();
}
ThreadTestCP.value = System.currentTimeMillis() + "";
System.out.println("生产者:" + ThreadTestCP.value);
lock.notify();
}
} catch (Exception ignored) {
}
}
}

class C {
private String lock;

C(String lock) {
this.lock = lock;
}

public void getValue() {
try {
synchronized (lock) {
if ("".equals(ThreadTestCP.value)) {
lock.wait();
}
System.out.println("消费者:" + ThreadTestCP.value);
ThreadTestCP.value = "";
lock.notify();
}
} catch (Exception ignored) {
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadTestCP{
public static String value;
public static void main(String[] args){
String lock="aaa";
new Thread(()->{
P p = new P(lock);
while(true){
p.setValue();
}
}).start();
new Thread(()->{
C p = new C(lock);
while(true){
p.setValue();
}
}).start();

}
}

每次生产者生产完成之后唤醒等待线程。然后消费者进行消费。在这里如果出现多个消费者或多个生产者。那么线程就有可能出现假死状态。也就是所有线程都处于等待状态。可以将notify换成notifyAll解决假死状态。
同时这里的notify是没有区分生产者/消费者的。也就是说生产者唤醒的也有可能是生产者。
简单来说就是
消费者查看是否有数据。如果没有就等待。如果有就消费,随后唤醒生产者进行生产。
生产者被唤醒之后进行生产。生产之后唤醒消费者。数据载体可以是任何对象。只要生产者和消费者的数据载体是同一对象。

实战

题目要求: 创建二十个线程。线程分为两种,一种输出“★“,另一种输出”☆“ 两种线程交替运行。
代码如下:

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

public class ThreadReplaceRun {
public static void main(String[] args) throws InterruptedException {
Tools tools = new Tools();
for (int i = 0; i < 20; i++) {
new Thread(()->{
tools.MethodA();
}).start();
new Thread(()->{
tools.MethodB();
}).start();
}
}
}

class Tools {
private boolean flag = false;

public void MethodA() {
synchronized (this) {
try {
while (flag) {
wait();
}
System.out.print("★\t");
flag = !flag;
notify();
} catch (Exception ignored) {
}
}
}

public void MethodB() {
synchronized (this) {
try {
while (!flag) {
wait();
}
System.out.print("☆\t");
flag = !flag;
notify();
} catch (Exception ignored) {
}
}
}
}

通过一个Boolean变量使两个线程变成互斥运行。使其其中一个线程进入睡眠。

join方法

很多时候主线程创建并启动子线程,如果子线程中有很多耗时的操作。往往主线程比子线程更早结束。如果在主线程中需要使用到子线程中的数据,这个时候就需要join方法了。
join方法是使所属线程正常运行run方法。当前线程无限制等待。直到所属线程销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadReplaceRun{
public static void main(String agrs) {
Thread t=new Thread(()->{
try{
Thread.sleep(500);
System.out.println("子线程");
}catch(InterruptedException e){}
});
t.start();
t.join();
System.out.println("主线程");
}
}

在执行过程中,子线程会休眠一段时间,在休眠完成之后主线程开始执行。其实join方法的底层也只是使用了wait方法;只是更方便我们使用。
注意 join方法不能与interrupt方法一起使用。

join(long)与sleep(long)的区别

这两者在运行的效果上基本没什么区别。只是咋对待同步的处理上有点区别。由于join底层是使用wait进行等待的,所以具有释放锁的特点。

ThreadLocal

在前面我们都是使用的public static 变量的形式。所有线程都使用同一个public static变量。
ThreadLocal则是为每一个线程提供共享变量的功能。
ThreadLocal可以被视为给线程存放数据的箱子

ThreadLocal线程隔离

我们可以使用一个ThreadLocal类为每一个线程存入数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public class ThreadJoinAndSleep {
public static ThreadLocal tl = new ThreadLocal();

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
tl.set(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName()+":"+tl.get());
});
t.setName("" + i);
t.start();
}
System.out.println(Thread.currentThread().getName()+":"+tl.get());
}

}

不难发现,每个线程get的都只是那个线程存入的数据。不能获取其他线程的数据。

get为null

在没有进行set之前任何一个线程get返回的都是null。如果想设置一个默认值。我们可以继承ThreadLocal,并实现inittialValue()方法返回默认值