Java多线程编程核心技术1——Thread类核心API

Thread类相关状态和方法示意图:

Thread类相关状态和方法示意图

一. Thread类核心API使用

1.进程和线程的区别

进程是一次程序的执行,可以理解成Windows任务管理器的一个exe程序;线程是进程中独立运行的子任务。

2. 实现多线程编程有两种方式:

2.1 继承Thread类,覆盖run()。(Thread类也实现了Runnaable接口)

优点:如需访问当前线程,无需使用Thread.currentThread()方法。

缺点:已继承Thread类,不能再继承其他父类。

2.2 实现Runnable接口来创建(必须封装到Thread类)。

优点:还可以继承其他父类,适合多个相同线程来处理同一份资源。

缺点:如需访问当前线程,必须使用Thread.currentThread()方法。

使用多线程时,代码的运行结果与代码执行顺序或调用顺序无关。

3.currentThread()方法

Thread.currentThread().getName():返回代码段正在被哪个线程调用的信息。

4.isAlive():判断线程是否处于活动状态。

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

package pgsthread;

public class TestAlive extends Thread{

public TestAlive(){

System.out.println("---TestAlive begins---");

System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());

System.out.println("Thread.currentThread().isAlive()="+Thread.currentThread().isAlive());

System.out.println("this.getName()="+this.getName());

System.out.println("this.isAlive()="+this.isAlive());

System.out.println("---TestAlive ends---");

}

@Override

public void run(){

System.out.println("---run begins---");

System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());

System.out.println("Thread.currentThread().isAlive()="+Thread.currentThread().isAlive());

System.out.println("this.getName()="+this.getName());

System.out.println("this.isAlive()="+this.isAlive());

System.out.println("---run ends---");

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package pgsthread;

public class Run {

public static void main(String[] args) {

TestAlive t = new TestAlive();

Thread t1 = new Thread(t);

//Thread(Runnable target):可以传入一个Thread类的对象,将t的run()交给t1执行。

System.out.println("main begin t1 isAlive="+t1.isAlive());

t1.setName("A");

t1.start();

System.out.println("main end t1 isAlive="+t1.isAlive());

}

}

运行结果如下:

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
---TestAlive begins---

Thread.currentThread().getName()=main

Thread.currentThread().isAlive()=true

this.getName()=Thread-0

this.isAlive()=false

---TestAlive ends---

main begin t1 isAlive=false

main end t1 isAlive=true

---run begins---

Thread.currentThread().getName()=A

Thread.currentThread().isAlive()=true

this.getName()=Thread-0

this.isAlive()=false

---run ends---

此处的Thread.currentThread()指向t1,this指向t。

5.判读线程是否是停止状态?

1
2
3
4
5
6
7
8
9
10
11
12
13
import pgsthread.TestAlive;
public class TestInterrupt {
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted()); //true
System.out.println(Thread.interrupted()); //false
TestAlive t = new TestAlive();
t.start();
t.interrupt();
System.out.println(t.isInterrupted()); //true
System.out.println(t.isInterrupted()); //true
}
}
Thread.currentThread().interrupt();中断的是当前线程,即main。

this.interrupted():具有将状态位清除为false的功能。

this.isInterrupted():不清除标志位。

## 6.主动行为:暂停线程使之阻塞

suspend():暂停线程;resume():恢复线程的执行。

废弃上述两个方法的原因:

(1) 具有独占性,若在同步代码块中调用suspend()而一直未调用resume(),会锁死该同步代码块。
1
2
3
4
5
6
7
8
9
10
11
12

public void println(long x){

synchronized (this){

print(x);

newline();

}

}

如果在进入println(long x)内部是调用suspend(),将锁定该方法,同步锁将不释放。

(2) 不同步,导致线程不安全。

会导致同方法suspend()后的代码得不到执行。

7.线程优先级

setPriority():优先级分为1~10级,默认为5。

(1)线程的优先级具有继承性。

(2)优先级具有规则性:高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。

(3) 优先级具有随机性,并不一定优先级高的线程就一定先执行完。ps,线程优先级与代码中出现的先后顺序无关。

8.守护线程 Thread.setDaemon(true)

Java线程分为两种,一种是用户线程,另一种是守护(Daemon)线程。当进程中没有用户线程时,守护线程也将自动销毁。

二. 线程间通信

1.等待/通知(wait/notify)机制

(1) 不使用等待/通知机制

使用sleep()结合while(true)死循环轮询检测,会浪费CPU资源。即两个线程主动式的读取一个共享变量,在花费读取时间的基础上,读到的值不一定是想要的。

(2) 等待/通知机制:

wait()使线程停止运行,notify()使停止的线程继续运行(随机挑选出一个呈wait状态的线程)。

在调用wait()和notify()前必须获得该对象的对象级别锁,即只能在同步方法或者代码块中调用wait()和notify()方法。

※要等到notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才释放锁,而不是notify()后马上唤醒wait状态的线程进行操作。

(3) sleep():不释放锁;notify():不立即释放锁;wait():释放锁。

(4) 不管线程是wait()还是sleep()状态,调用interrupt()都会抛出InterruptedException异常。只有运行的线程才可被interrupt()。

(5) 唤醒所有等待中的线程:

方法一:多次调用notify();

方法二:notifyAll();

(6) 等待wait的条件发生变化:

※用while去判断,while是轮询的,同步的,每一次都会进行判断;而if的话可能有两个线程进去同时进入if语句块就会产生非线程安全问题。

2.生产者/消费者模式实现

多个生产者与消费者:采用wait/notify机制,但不一定每一次唤醒的都是异类,连续唤醒同类就会产生“假死”,以下加粗的地方则为连续唤醒同类。

生产者1 +1

生产者1 wait

生产者2 wait

消费者1 1-1=0

消费者1 wait

消费者2 wait

生产者1 +1

生产者1 wait

生产者2 wait

解决方案1:改为notifyAll();

3. 通过管道进行线程间通信

一个线程发送数据到输出管道,另一个线程从输入管道中读数据。

3.1 字节流:PipedInputStream和PipedOutputStream

1
2
3
4
5
PipedInputStream in = new PipedInputStream();

PipedOutputStream out = new PipedOutputStream();

out.connect(in);或 in.connect(out);//使两个管道之间进行通信链接

3.2 字符流:PipedReader和PipedWriter,同上connect

4.实例:等待/通知の交叉备份

使线程具有有序性(设置一个volatile参数)

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
package threadservice;

public class DBTools {
volatile private boolean flag = false;
synchronized public void saveA(){
while(flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("save A!");
flag = true;
notifyAll();
}

synchronized public void saveB(){
while(!flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("save B!");
flag = false;
notifyAll();
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package threadservice;

public class ThreadA extends Thread{
private DBTools dbtools;

public ThreadA(DBTools dbtools){
this.dbtools = dbtools;
}

@Override
public void run() {
dbtools.saveA();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package threadservice;

public class ThreadB extends Thread{
private DBTools dbtools;

public ThreadB(DBTools dbtools){
this.dbtools = dbtools;
}

@Override
public void run() {
dbtools.saveB();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package threadservice;

public class Run {
public static void main(String[] args) {
DBTools dbtools = new DBTools();

for(int i = 0; i < 3;i++){
ThreadA tA = new ThreadA(dbtools);
tA.start();
ThreadB tB = new ThreadB(dbtools);
tB.start();
}
}
}

打印结果就是交叉打印的,循环等待和通知所有wait线程,但是只有flag正确的那个线程才能运行,即想要顺序执行的话,只要设置参数就可以。
1
2
3
4
5
6
save A!
save B!
save A!
save B!
save A!
save B!

5.join()

比如子线程要进行大量的耗时计算,主线程往往早于子线程结束。当主线程需要等待子线程完成之后再结束,就需要用到join()。

如在t1中调用t2.join();t1暂停,必须等到t2运行完才能继续运行。即等待线程对象销毁。

(1) join与synchronized区别:

join()在内部使用wait()方法进行等待,而synchronized使用的是“对象监视器”原理作为同步。

(2) 如果当前线程对象被中断,则当前线程出现异常(只影响当前线程)。

方法join()与interrupt()方法彼此遇到,会出现异常。但影响的是当前的线程,其他线程正常运行。

(3) join(long)和sleep(long)的区别

join(long):在内部使用的是wait(long),具有释放锁地特点。在内部执行wait(long)后,当前线程的锁被释放,其他线程就可以调用此线程中的同步方法了。

而sleep(long)不释放锁。

(4) 经过运行如下代码发现join后面的代码有可能提前运行,可能会出现以下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package jointhread;

public class ThreadA extends Thread {
private ThreadB b;

public ThreadA(ThreadB b) {
super();
this.b = b;
}

@Override
public void run() {
try {
synchronized (b) {
System.out.println("A begins at " + System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("A ends at " + System.currentTimeMillis());
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

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

public class ThreadB extends Thread{

@Override
synchronized public void run() {
try {
System.out.println("B begins at " + System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("B ends at " + System.currentTimeMillis());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

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

public class Run {
public static void main(String[] args) {
try {
ThreadB b = new ThreadB();
ThreadA a = new ThreadA(b);
a.start();
b.start();
b.join(2000);
System.out.println("main ends at " + System.currentTimeMillis());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

运行结果1:
1
2
3
4
5
A begins at 1486956475786
A ends at 1486956480825
main ends at 1486956480857
B begins at 1486956480872
B ends at 1486956485880

step1. b.join(2000)先抢到B锁,然后将B锁释放;

step2. ThreadA抢到B锁,打印 A begins 并且sleep(5000);

step3. ThreadA打印 A ends,并释放锁;

step4. 此时join(2000)和ThreadB争抢锁,而join(2000)再次抢到锁,发现时间已过,释放锁后打印main ends;

step5. ThreadB抢到锁打印B begins;

step6. 5秒之后再打印B ends。

运行结果2:

1
2
3
4
5
A begins at 1486956475786
A ends at 1486956480825
B begins at 1486956480872
B ends at 1486956485880
main ends at 1486956485880

step1. b.join(2000)先抢到B锁,然后将B锁释放;

step2. ThreadA抢到B锁,打印 A begins 并且sleep(5000);

step3. ThreadA打印 A ends,并释放锁;

step4. 此时join(2000)和ThreadB争抢锁,而ThreadB抢到锁后执行sleep(5000)后释放锁;

step5. main ends 在最后输出。

运行结果3:

1
2
3
4
5
A begins at 1486956475786
A ends at 1486956480825
B begins at 1486956480872
main ends at 1486956480872
B ends at 1486956485880

step1. b.join(2000)先抢到B锁,然后将B锁释放;

step2. ThreadA抢到B锁,打印 A begins 并且sleep(5000);

step3. ThreadA打印 A ends,并释放锁;

step4. 此时join(2000)和ThreadB争抢锁,而join(2000)再次抢到锁,发现时间已过,释放锁后打印main ends;

step5. ThreadB抢到锁打印B begins;

step6. 这时main ends 也异步输出;

step7. 打印B ends。

6.类ThreadLocal

变量值的共享:public static +变量名;

每一个线程都有自己的共享变量:采用类ThreadLocal,使每一个线程绑定自己的值,保证隔离性。

1
2
3
4
5
6
package localthread;

public class Tools {
public static ThreadLocalExt t1 = new ThreadLocalExt();

}

1
2
3
4
5
6
7
8
9
10
package localthread;

import java.util.Date;

public class ThreadLocalExt extends ThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
}

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

public class ThreadA extends Thread{
@Override
public void run() {
try {
for(int i = 0;i<10;i++){
System.out.println("ThreadA = " + Tools.t1.get());
Thread.sleep(100);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

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

public class Run {
public static void main(String[] args) {
try {
for(int i = 0;i<10;i++){
System.out.println("main =" + Tools.t1.get());
Thread.sleep(100);
}
Thread.sleep(5000);
ThreadA a = new ThreadA();
a.start();
} catch (InterruptedException e) {
// TODO: handle exception
}
}
}

运行结果为10个main打印的值相等,10个ThreadA 打印的值也相等,但main和ThreadA 的不相等。

说明ThreadLocal能够实现一个线程享有一个专属变量。

7.类InheritableThreadLocal

可以在子线程中取得父线程继承下来的值。

(1) 将上例中Tools.java的ThreadLocal改为InheritableThreadLocal,即可得到ThreadA 打印的值和main一样,可谓继承性。

(2) InheritableThreadLocal.childValue(Object object) : 可以修改子线程中的值。

(3) 如果子线程取得值的同时,主线程将InheritableThreadLocal中的值进行修改,子线程拿到的值还是旧值。

三. 线程组

1.作用:可以批量管理线程或线程组对象。

1
2
3
4
5
Thread aRunnable = new ThreadA();

ThreadGroup group = new ThreadGroup("小胖的线程组");

Thread aThread = new Thread(group, aRunnable);

2.在实例化一个线程组如果不指定所属的线程组,则该线程组自动归到当前线程对象所属的线程组中,也就是隐式地在一个线程组中添加了一个子线程组。

JVM的根线程组是system,再取其父线程组则出现异常。

3.递归与非递归取得组内对象。

1
2
3
enumerate(ThreadGroup, true);//递归(打印A->B)

enumerate(ThreadGroup, false);//非递归(只打印A)

4.在默认的情况下,线程组中的一个线程出错,不影响其他线程的运行。

博客迁移:年初发表在简书的Java多线程系列,原文链接:Java多线程编程核心技术1——Thread类核心API