重头戏,多线程.

绪论

线程与进程,并行与并发

首先要理解进程(Processor)和线程(Thread)的区别

进程:启动一个LOL.exe就叫一个进程。 接着又启动一个DOTA.exe,这叫两个进程。 

线程:线程是在进程内部同时做的事情,比如打开一个图片识别进程,先识别一张图片,再识别下一张这不是多线程,当同时进行时才是.

再补充点什么是并行和并发:

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。

吃饭的时候先接电话跟后接电话的比较更像是中断优先级高低的不同,并发应该是一手筷子,一手电话,说一句话,咽一口饭。 并行才是咽一口饭同时说一句话,而这光靠一张嘴是办不到的,至少两张嘴。

如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。

在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

摘自:《并发的艺术》 — 〔美〕布雷谢斯 (这一段是,上面两段不是)

可以看出,我们讨论多线程编程是在讨论并发编程.当然java也能做并行,但现在不讨论.

并发值得吗

换句话说,多线程能提高效率吗?

举个栗子:

两个小孩吃两碗饭,只有一双筷子,他两排队吃和轮流吃的时间是一样的(忽略交换筷子等细微时间),因为我不管你怎么吃,人就两个人,饭也是两碗饭,就只有一双筷子,最终时间都是一样的.从这个例子来看,仅仅从完成任务本身,并发没有多大意义.但是,

换个例子:

挖隧道,从两头向中间挖,只有一个工人,如果让他两头跑着挖显然没有从一头挖快,中间有赶路的时间耽搁.变一下,工人要干五个小时要休息一个小时(类比CPU等待I/O资源),把赶路的时间算在休息时间里面,这样就使得两头挖的时间短一点.(可能例子不太合适)

总之,多线程有没有提升效率要视情况而定,纯粹考虑计算效率没什么太大意义,就像下面我要用的例子,英雄加血和减血使用多线程的效率没准比不用还低,但是你不能不用.

创建多线程有3种方式,分别是

  • 继承线程类
  • 实现Runnable接口
  • 匿名类

一个线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

下图显示了一个线程完整的生命周期。

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 **start()**这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 **run()**,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait()方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join()发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join()等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态

线程的优先级

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

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

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

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

创建一个线程

继承线程类

先放出Hero类的代码:

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

import java.io.Serializable;

public class Hero{
public String name;
public float hp;

public int damage;

public void attackHero(Hero h) {
try {
//为了表示攻击需要时间,每次攻击暂停1000毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);

if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

使用多线程,就可以做到盖伦在攻击提莫的同时,赏金猎人也在攻击盲僧

设计一个类KillThread继承Thread,并且重写run方法

启动线程办法:实例化一个KillThread对象,并且调用其start方法

就可以观察到赏金猎人攻击盲僧的同时,盖伦也在攻击提莫.

设计一个KillThread,继承了Thread,重写了run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package multiplethread;

import charactor.Hero;

public class KillThread extends Thread{

private Hero h1;
private Hero h2;

public KillThread(Hero h1, Hero h2){
this.h1 = h1;
this.h2 = h2;
}

public void run(){
while(!h2.isDead()){
h1.attackHero(h2);
}
}
}

创建四个Hero,并实例化两个线程,第一个线程盖伦打提莫,第二个线程赏金猎人打盲僧:

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

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

KillThread killThread1 = new KillThread(gareen,teemo);
killThread1.start();
KillThread killThread2 = new KillThread(bh,leesin);
killThread2.start();

}

}

另一个例子:

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
class ThreadDemo extends Thread {
private Thread t;
private String threadName;

ThreadDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}

public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 让线程睡眠一会
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}

public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}

public class TestThread {

public static void main(String args[]) {
ThreadDemo T1 = new ThreadDemo( "Thread-1");
T1.start();

ThreadDemo T2 = new ThreadDemo( "Thread-2");
T2.start();
}
}

实现Runable接口

创建类Battle,实现Runnable接口

启动的时候,首先创建一个Battle对象,然后再根据该battle对象创建一个线程对象,并启动

1
2
Battle battle1 = new Battle(gareen,teemo);
new Thread(battle1).start();

battle1对象实现了Runnable接口,所以有run方法,但是直接调用run方法,并不会启动一个新的线程。

必须,借助一个线程对象的start()方法,才会启动一个新的线程。

所以,在创建Thread对象的时候,把battle1作为构造方法的参数传递进去,这个线程启动的时候,就会去执行battle1.run()方法了。

Battle类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package multiplethread;

import charactor.Hero;

public class Battle implements Runnable{

private Hero h1;
private Hero h2;

public Battle(Hero h1, Hero h2){
this.h1 = h1;
this.h2 = h2;
}

public void run(){
while(!h2.isDead()){
h1.attackHero(h2);
}
}
}
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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

Battle battle1 = new Battle(gareen,teemo);

new Thread(battle1).start();

Battle battle2 = new Battle(bh,leesin);
new Thread(battle2).start();

}

}

从本质上来说,通过继承Thread实现多线程也是实现Runnable接口,因为Thread实现了Runnable.而Runnable是一个函数式接口,可以使用匿名类来创建线程.

匿名类

使用匿名类,继承Thread,重写run方法,直接在run方法中写业务代码

匿名类的一个好处是可以很方便的访问外部的局部变量。 前提是外部的局部变量需要被声明为final。(JDK7以后就不需要了)

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

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

//匿名类
Thread t1= new Thread(){
public void run(){
//匿名类中用到外部的局部变量teemo,必须把teemo声明为final
//但是在JDK7以后,就不是必须加final的了
while(!teemo.isDead()){
gareen.attackHero(teemo);
}
}
};

t1.start();

Thread t2= new Thread(){
public void run(){
while(!leesin.isDead()){
bh.attackHero(leesin);
}
}
};
t2.start();

}

}

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

常见线程方法

当前线程暂停

Thread.sleep(1000); 表示当前线程暂停1000毫秒 ,其他线程不受影响  Thread.sleep(1000); 会抛出InterruptedException 中断异常,因为当前线程sleep的时候,有可能被停止,这时就会抛出InterruptedException

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

public class TestThread {

public static void main(String[] args) {

Thread t1= new Thread(){
public void run(){
int seconds =0;
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.printf("已经玩了LOL %d 秒%n", seconds++);
}
}
};
t1.start();

}

}

加入到当前线程中

首先解释一下主线程的概念

所有进程,至少会有一个线程即主线程,即main方法开始执行,就会有一个看不见的主线程存在。

在42行执行t.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
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

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

final Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

final Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

final Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

Thread t1= new Thread(() -> {
System.out.println(Thread.currentThread().getName()+" start");
while(!teemo.isDead()){
gareen.attackHero(teemo);
}
});

t1.start();
System.out.println(Thread.currentThread().getName()+" start");
//代码执行到这里,一直是main线程在运行
try {
//t1线程加入到main线程中来,只有t1线程运行结束,才会继续往下走
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" start");
Thread t2= new Thread(){
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+" start");
while(!leesin.isDead()){
bh.attackHero(leesin);
}
}
};
//会观察到盖伦把提莫杀掉后,才运行t2线程
t2.start();

}

}

另一个例子:

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
class Thread1 extends Thread {
private String name;

public Thread1(String name) {
super(name);
this.name = name;
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
for (int i = 0; i < 5; i++) {
System.out.println("子线程" + name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
}
}

public class TestThread2 {

public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "主线程运行开始!");
Thread1 mTh1 = new Thread1("A");
Thread1 mTh2 = new Thread1("B");
mTh1.start();
mTh2.start();
try {
mTh1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
mTh2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "主线程运行结束!");

}

}

**特别注意:join()不是表示子线程开始,而是表示主线程在这里等子线程!**可能在join之前子线程已经运行完了,join是用来确保主线程比子线程后结束.

线程优先级

当线程处于竞争关系的时候,优先级高的线程会有更大的几率获得CPU资源 .

1
2
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

临时暂停

当前线程,临时暂停,使得其他线程可以有更多的机会占用CPU资源.仅仅是给了一次机会,有可能暂停完了还是原来那个线程继续执行.并且相同或更高优先级更有可能得到这个机会,低优先级是没有机会的.

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
class ThreadYield extends Thread{  
public ThreadYield(String name) {
super(name);
}

@Override
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
if (i ==30) {
this.yield();
}
}

}
}

public class Main {

public static void main(String[] args) {

ThreadYield yt1 = new ThreadYield("张三");
ThreadYield yt2 = new ThreadYield("李四");
yt1.start();
yt2.start();
}

}

运行结果:

第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。

第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

sleep()和yield()的区别

sleep()和yield()的区别:sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态(看开始的第二张图),所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

sleep方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程.

另外,sleep 方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

守护线程

守护线程的概念是:当一个进程里,所有的线程都是守护线程的时候,结束当前进程。

就好像一个公司有销售部,生产部这些和业务挂钩的部门。除此之外,还有后勤,行政等这些支持部门。 如果一家公司销售部,生产部都解散了,那么只剩下后勤和行政,那么这家公司也可以解散了。

守护线程就相当于那些支持部门,如果一个进程只剩下守护线程,那么进程就会自动结束。 守护线程通常会被用来做日志,性能统计等工作。这么看来守护线程叫做支持线程也不错呢.

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

public class TestThread {

public static void main(String[] args) {

Thread t1= new Thread(){
public void run(){
int seconds =0;

while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.printf("已经玩了LOL %d 秒%n", seconds++);

}
}
};
t1.setDaemon(true);
t1.start();

}

}

这段代码不会有任何结果.

同步问题

原因分析

当多个线程同时修改一个数据时有可能产生同步问题.

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* @author YL
*/
public class TestThread3 {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 10000;

System.out.printf("盖伦的初始血量是 %.0f%n", gareen.hp);

//多线程同步问题指的是多个线程同时修改一个数据的时候,导致的问题

//假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击

//用JAVA代码来表示,就是有多个线程在减少盖伦的hp
//同时又有多个线程在恢复盖伦的hp

//n个线程增加盖伦的hp

int n = 10000;

Thread[] addThreads = new Thread[n];
Thread[] reduceThreads = new Thread[n];

for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
gareen.recover();
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
t.start();
addThreads[i] = t;
//System.out.println(Thread.currentThread().getName());
}

//n个线程减少盖伦的hp
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
gareen.hurt();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
t.start();
reduceThreads[i] = t;
//System.out.println(Thread.currentThread().getName());
}

//等待所有增加线程结束
for (Thread t : addThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//等待所有减少线程结束
for (Thread t : reduceThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

//代码执行到这里,所有增加和减少线程都结束了

//增加和减少线程的数量是一样的,每次都增加,减少1.
//那么所有线程都结束后,盖伦的hp应该还是初始值

//但是事实上观察到的是:

System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了 %.0f%n", n, n, gareen.hp);

}

}

10000个加血线程和10000个减血线程对hp操作,结果有可能不是10000.

考虑这种极端情况:增加线程加血还未完成,减血线程却读取了hp值1000,进行减血,这是加血线程将hp改为10001,然后减血线程改为9999.

对于上面的代码还引起了我一点思考,两个for循环明明有先后顺序,也就是先增加加血线程10000个,再增加减血线程1000个,为什么执行时却是两种线程交替呢?

经过仔细思考我终于后知后觉明白了多线程的意义,并且初步搞清了上面的问题,看下面:

再看六种线程状态

不同于文章开头的大致分法,关于线程状态的准确说法是下面六种(线程的状态是用枚举值来描述的,严格讲就6种枚举值,但是便于理解时几种状态的说法略有不同.):

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得cpu 时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIME_WAITING):该状态不同于WAITING,它可以在指定的时间内自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

另一个版本的状态图,实际没有差别:

回到上面的问题:受到那张图的影响,我以为20000个线程全是交替进行的.其实压根不是,我多多线程没理解.由于学艺不精,想不到什么查看线程信息的方法,就用了个笨办法,在recover()方法和hurt()方法中都加了一句System.out.println(Thread.currentThread().getName()+"recover");这样当每个线程执行时,就能知道大概的顺序.结果是在第一个减血线程执行之前,只存在加血线程,当出现了第一个减血线程后(大约在一般位置),加血逐渐减少,减血逐渐增多,然后只有减血.同步问题就出现在两者都存在的时候.

这样的情况应该才是正确的,第一个for循环中,我假定start()后部分加血线程立即开始执行,其他的肯定会有部分来不及执行,这时第二个for循环开始,减血线程加进来了,这就造成两种线程混在一起.

解决思路

解决同步问题的思路是:在加血线程操作hp时,禁止减血线程操作,如下图:

但是这又引起了我一个思考:既然是一个线程执行完加血操作后在进行减血线程,那么本质上线程是轮替工作的,既然如此还要多线程干嘛?

假定下面的情况:

情况一:十个线程各自操作各自的对象,没有干扰

在这种情况下,不存在同步问题,每个线程有十个任务.如果不进行多线程,就只能每个对象挨个来,就像银行只开一个窗口,所有人都要排队等,显然不合适,多线程在这里的意义是,先执行线程一的任务一,然后轮换给线程二的任务一,就像银行开了十个窗口,但是只有一个工作人员(CPU核数不足),这个工作人员在十个窗口间来回跑动,给十个对象服务.

我原来有个错误认知,以为多线程可以加快程序速度(同时运行),但实际只是将顺序执行变成了交替执行,它的目的是造成一种同时运行的假象.原本我画三张画,后面的人就是在干等,我用多线程的话,我就能先给后面的人一张草图,而最终两种方式的时间是一样的.

情况二:只有一个对象

用前面英雄加血回血的例子,复杂化,十个线程包括回血,减血,各种buff和debuff.显然不可以让它们依次执行,比如要减100的血,加150的血,加一个迟滞光环和加速光环,那么先减20的血,然后切换线程加buff,再切换线程加debuff,再切换线程加100的血,回到减血那减80,再加50.而且要避免加血和减血同时操作,要让hp是当前线程独占的.

synchronized同步对象概念

1
2
3
4
Object someObject =new Object();
synchronized (someObject){
//此处的代码只有占有了someObject后才可以执行
}

synchronized表示当前线程独占对象someObject

当前线程独占了对象someObject,如果有其他线程试图占有对象someObject,就会等待,直到当前线程释放对someObject的占用。someObject又叫同步对象,所有的对象,都可以作为同步对象

释放同步对象的方式:synchronized块自然结束,或者有异常抛出

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
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestThread {

private static String now() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void main(String[] args) {
final Object someObject = new Object();

Thread t1 = new Thread() {
@Override
public void run() {
try {
System.out.println(now() + " t1 线程已经运行");
System.out.println(now() + this.getName() + " 试图占有对象:someObject");
synchronized (someObject) {

System.out.println(now() + this.getName() + " 占有对象:someObject");
Thread.sleep(5000);
System.out.println(now() + this.getName() + " 释放对象:someObject");
}
System.out.println(now() + " t1 线程结束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t1.setName(" t1");
t1.start();
System.out.println(now() + " t1线程已初始化");
Thread t2 = new Thread() {

@Override
public void run() {
try {
System.out.println(now() + " t2 线程已经运行");
System.out.println(now() + this.getName() + " 试图占有对象:someObject");
synchronized (someObject) {
System.out.println(now() + this.getName() + " 占有对象:someObject");
Thread.sleep(5000);
System.out.println(now() + this.getName() + " 释放对象:someObject");
}
System.out.println(now() + " t2 线程结束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t2.setName(" t2");
t2.start();
System.out.println(now() + " t2线程已初始化");
}

}

结果是:

1
2
3
4
5
6
7
8
9
10
11
12
16:03:08 t1线程已初始化
16:03:08 t1 线程已经运行
16:03:08 t2线程已初始化
16:03:08 t2 线程已经运行
16:03:08 t2 试图占有对象:someObject
16:03:08 t1 试图占有对象:someObject
16:03:08 t2 占有对象:someObject
16:03:13 t2 释放对象:someObject
16:03:13 t2 线程结束
16:03:13 t1 占有对象:someObject
16:03:18 t1 释放对象:someObject
16:03:18 t1 线程结束

上面的结果符合预期,但是偶尔有几次发现第一行和第二行结果颠倒了,运行在初始化之前,仔细检查后发现,问题出在:

1
2
t1.start();
System.out.println(now() + " t1线程已初始化");

有可能出现t1线程start后开始执行run()方法,结果这是主线程的代码还没跑到第二行,所以应该把上面两行颠倒.

几种用法

使用synchronized解决同步问题

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class TestThread {

public static void main(String[] args) {

final Object someObject = new Object();

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 10000;

int n = 10000;

Thread[] addThreads = new Thread[n];
Thread[] reduceThreads = new Thread[n];

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){

//任何线程要修改hp的值,必须先占用someObject
synchronized (someObject) {
gareen.recover();
}

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
addThreads[i] = t;

}

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){
//任何线程要修改hp的值,必须先占用someObject
synchronized (someObject) {
gareen.hurt();
}

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
reduceThreads[i] = t;
}

for (Thread t : addThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (Thread t : reduceThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);

}

}

这种用法中,并不让加血和回血方法独占gareen,而是创建了一个someObject对象,让每个线程都要独占它,这样也做到了同一时间内,hp只能被一个线程修改.

使用hero对象作为同步对象

这种方法是索性让gareen来作为同步对象

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
64
65
66
67
68
69
70
71
72
73
74
75
76
public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 10000;

int n = 10000;

Thread[] addThreads = new Thread[n];
Thread[] reduceThreads = new Thread[n];

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){

//使用gareen作为synchronized
synchronized (gareen) {
gareen.recover();
}

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
addThreads[i] = t;

}

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){
//使用gareen作为synchronized
//在方法hurt中有synchronized(this)
gareen.hurt();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
reduceThreads[i] = t;
}

for (Thread t : addThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (Thread t : reduceThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);

}

}

也可以修改下Hero里的代码

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 charactor;

public class Hero{
public String name;
public float hp;

public int damage;

//回血
public void recover(){
hp=hp+1;
}

//掉血
public void hurt(){
//使用this作为同步对象
synchronized (this) {
hp=hp-1;
}
}

public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

在方法前,加上修饰符synchronized

这种方法也是使用Hero对象作为同步对象:

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

public class Hero{
public String name;
public float hp;

public int damage;

//回血
//直接在方法前加上修饰符synchronized
//其所对应的同步对象,就是this
//和hurt方法达到的效果一样
public synchronized void recover(){
hp=hp+1;
}

//掉血
public void hurt(){
//使用this作为同步对象
synchronized (this) {
hp=hp-1;
}
}

public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

和上面的

1
2
3
4
5
6
public void hurt(){
//使用this作为同步对象
synchronized (this) {
hp=hp-1;
}
}

是同样的效果,都是用的this,调用了哪个对象的hurt()方法,this就指代的是哪个对象.

线程安全的类

如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类

同一时间,只有一个线程能够进入这种类的一个实例去修改数据,进而保证了这个实例中的数据的安全(不会同时被多线程修改而变成脏数据)

比如StringBuffer和StringBuilder的区别:StringBuffer的方法都是有synchronized修饰的,StringBuffer就叫做线程安全的类,而StringBuilder就不是线程安全的类.

在集合那篇也提到了一些线程安全的类.Collections类还提供了用于同步控制的方法.

死锁

当用synchronized来解决同步问题时,又会产生死锁问题.原因非常简单,当线程A占有对象1后,试图占有对象二,而线程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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* @author YL
*/
public class TestThread4 {
public static void main(String[] args) {
final Hero a = new Hero();
a.name = "A";
final Hero b = new Hero();
b.name = "B";
final Hero c = new Hero();
c.name = "C";
Thread t1 = new Thread(() -> {
synchronized (a) {
System.out.println(" t1 hold A ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("t1 try to hold B, and wait B to release ...");
synchronized (b) {
System.out.println("t1 hold B !!!");
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (b) {
System.out.println(" t2 hold B ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("t2 try to hold C, and wait C to release ...");
synchronized (c) {
System.out.println("t2 hold C !!!");
}
}
});
t2.start();

Thread t3 = new Thread(() -> {
synchronized (c) {
System.out.println(" t3 hold C ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("t3 try to hold A, and wait A to release ...");
synchronized (a) {
System.out.println("t3 hold A !!!");
}
}
});
t3.start();
}
}

交互

线程之间有交互通知的需求,考虑如下情况:

有两个线程,处理同一个英雄。一个加血,一个减血。

减血的线程,发现血量=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
package charactor;

public class Hero{
public String name;
public float hp;

public int damage;

public synchronized void recover(){
hp=hp+1;
}

public synchronized void hurt(){
hp=hp-1;
}

public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true: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
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
import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;

Thread t1 = new Thread(){
public void run(){
while(true){

//因为减血更快,所以盖伦的血量迟早会到达1
//使用while循环判断是否是1,如果是1就不停的循环
//直到加血线程回复了血量
while(gareen.hp==1){
continue;
}

gareen.hurt();
System.out.printf("t1 为%s 减血1点,减少血后,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
};
t1.start();

Thread t2 = new Thread(){
public void run(){
while(true){
gareen.recover();
System.out.printf("t2 为%s 回血1点,增加血后,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
};
t2.start();

}

}

但这种方法不好.

使用wait()和notify()

在Hero类中:

hurt()减血方法:当hp=1的时候,执行this.wait().

this.wait()表示让占有this的线程等待,并临时释放占有.

进入hurt方法的线程必然是减血线程,this.wait()会让减血线程临时释放对this的占有。这样加血线程,就有机会进入recover()加血方法了。

recover()加血方法:增加了血量,执行this.notify();

this.notify()表示通知那些等待在this的线程,可以苏醒过来了.

等待在this的线程,恰恰就是减血线程.一旦recover()结束,加血线程释放了this,减血线程,就可以重新占有this,并执行后面的减血工作。

改动在Hero类中:

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

public class Hero {
public String name;
public float hp;

public int damage;

public synchronized void recover() {
hp = hp + 1;
System.out.printf("%s 回血1点,增加血后,%s的血量是%.0f%n", name, name, hp);
// 通知那些等待在this对象上的线程,可以醒过来了,如第20行,等待着的减血线程,苏醒过来
this.notify();
}

public synchronized void hurt() {
if (hp == 1) {
try {
// 让占有this的减血线程,暂时释放对this的占有,并等待
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

hp = hp - 1;
System.out.printf("%s 减血1点,减少血后,%s的血量是%.0f%n", name, name, hp);
}

public void attackHero(Hero h) {
h.hp -= damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
if (h.isDead())
System.out.println(h.name + "死了!");
}

public boolean isDead() {
return 0 >= hp ? true : false;
}

}

这里需要强调的是,wait方法和notify方法,并不是Thread线程上的方法,它们是Object上的方法。

因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。

wait()的意思是:让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。

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
public class TestThread5 {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;

for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
while (true) {
gareen.hurt();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

});
t.start();
}

for (int i = 0; i < 2; i++) {
Thread t = new Thread(() -> {
while (true) {
gareen.recover();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

});
t.start();
}
}

}

Hero类代码:

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

public class Hero {
String name;
float hp;

private int damage;

//回血
synchronized void recover() {
if (hp >= 1000) {
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
hp = hp + 1;
System.out.printf("%s 回血1点,增加血后,%s的血量是%.0f%n", name, name, hp);
this.notify();
}

//掉血
synchronized void hurt() {
if (hp <= 1) {
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
hp = hp - 1;
System.out.printf("%s 减血1点,减少血后,%s的血量是%.0f%n", name, name, hp);
this.notify();
}

public void attackHero(Hero h) {
h.hp -= damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
if (h.isDead()) {
System.out.println(h.name + "死了!");
}
}

private boolean isDead() {
return 0 >= hp ? true : false;
}

}

Hero类要求在hp为1时等待加血线程,hp为1000时等待减血线程.开启了五个减血线程和两个加血线程,当然数量也可以倒过来,注意:sleep都设置为100,结果发现hp值会突破定下的上界和下界.如果位置加血线程和减血线程数量相同,改为sleep事件不对等,也会出现这样的结果,原因在于:

当线程数不对等时,notify唤醒的线程就有较大可能是同类线程:一个减血线程发现hp为1,自己wait,notify别的线程,而此时不仅仅是加血线程在wait,也有更多的减血在wait.如果进入另一个减血线程,那么执行的是if块后面的代码,hp-1,不经过hp边界判断.

sleep数值不对等时也是类似的情况,sleep数值较大的造成了某类线程活跃的较少.

改进是:将if (hp >= 1000)if (hp <= 1000)改为while (hp >= 1000)while (hp >= 1000).改为while后wait()结束仍旧在hp边界判断中,这样避免了hp溢出.

线程池

在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。

自定义一个线程池

线程池的思路和生产者消费者模型是很接近的。

  1. 准备一个任务容器
  2. 一次性启动10个消费者线程
  3. 刚开始任务容器是空的,所以线程都wait在上面。
  4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
  5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
  6. 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。

在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程

线程池的代码:

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
64
65
66
67
68
import java.util.LinkedList;

/**
* @author YL
*/
public class ThreadPool {

// 线程池大小
int threadPoolSize;

// 任务容器
final LinkedList<Runnable> tasks = new LinkedList<>();

// 试图消费任务的线程

public ThreadPool() {
threadPoolSize = 10;

// 给予名字并启动10个任务消费者线程
synchronized (tasks) {
for (int i = 0; i < threadPoolSize; i++) {
new TaskConsumeThread("任务消费者线程 " + i).start();
}
}
}

public void add(Runnable r) {
//这里的synchronized保证添加任务的外部线程的线程安全
synchronized (tasks) {
tasks.add(r);
// 唤醒等待的任务消费者线程和添加任务线程
tasks.notifyAll();
}
}

class TaskConsumeThread extends Thread {
public TaskConsumeThread(String name) {
super(name);
}

Runnable task;

@Override
public void run() {
System.out.println("启动: " + this.getName());
while (true) {
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
task = tasks.removeLast();
// 允许添加任务的线程可以继续添加任务,也唤醒了添加任务线程
tasks.notifyAll();

}
//取出任务后就放弃了独占并开始执行
System.out.println(this.getName() + " 获取到任务,并执行");
task.run();
}
}
}

}

注意上面执行任务是直接使用run()方法,也就是下面的Lambda表达式位置,这样就是执行一个普通的类方法,只在当前线程执行.

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
/**
* @author YL
*/
public class TestThread7 {
public static void main(String[] args) {
ThreadPool pool = new ThreadPool();

for (int i = 0; i < 5; i++) {
Runnable task = () -> {
//System.out.println("执行任务");
//任务可能是打印一句话
//可能是访问文件
//可能是做排序
};

pool.add(task);

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
}

Java自带线程池

线程池类ThreadPoolExecutor在包java.util.concurrent下:

1
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
  • 第一个参数10 表示这个线程池初始化了10个线程在里面工作
  • 第二个参数15 表示如果10个线程不够用了,就会自动增加到最多15个线程
  • 第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过60秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就10个
  • 第四个参数TimeUnit.SECONDS 如上
  • 第五个参数 new LinkedBlockingQueue() 用来放任务的集合
  • execute方法用于添加新的任务
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
package multiplethread;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThread {

public static void main(String[] args) throws InterruptedException {

ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

threadPool.execute(new Runnable(){

@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("任务1");
}

});

}

}

练习

使用线程池同步查找特定文件中的内容.

创建一个大小为10的线程池,查找文件夹中的java文件,并找到里面有public的文件.

线程池还是那个线程池:

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
64
65
import java.util.LinkedList;

/**
* @author YL
*/
public class ThreadPool {

// 线程池大小
int threadPoolSize;

// 任务容器
final LinkedList<Runnable> tasks = new LinkedList<>();

// 试图消费任务的线程

public ThreadPool() {
threadPoolSize = 10;

// 启动10个任务消费者线程
synchronized (tasks) {
for (int i = 0; i < threadPoolSize; i++) {
new TaskConsumeThread("任务消费者线程 " + i).start();
}
}
}

public void add(Runnable r) {
synchronized (tasks) {
tasks.add(r);
// 唤醒等待的任务消费者线程
tasks.notifyAll();
}
}

class TaskConsumeThread extends Thread {
public TaskConsumeThread(String name) {
super(name);
}

Runnable task;

@Override
public void run() {
while (true) {
//这个synchronized保证线程池线程独占任务列表
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
task = tasks.removeLast();
// 允许添加任务的线程可以继续添加任务
tasks.notifyAll();
}
//取出任务后就放弃了独占并开始执行
task.run();
}
}
}

}

大体思路是,先递归找到java文件,然后将找到的文件打包为一个个任务交给任务容器,再由任务消费者线程获取任务并运行.

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

import java.io.File;

public class TestThread {

static ThreadPool pool= new ThreadPool();
public static void search(File file, String search) {

if (file.isFile()) {
if(file.getName().toLowerCase().endsWith(".java")){
SearchFileTask task = new SearchFileTask(file, search);
pool.add(task);
}
}
if (file.isDirectory()) {
File[] fs = file.listFiles();
for (File f : fs) {
search(f, search);
}
}
}

public static void main(String[] args) {
File folder =new File("e:\\project");
search(folder,"Magic");
}
}
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
package multiplethread;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class SearchFileTask implements Runnable{

private File file;
private String search;
public SearchFileTask(File file,String search) {
this.file = file;
this.search= search;
}

public void run(){

String fileContent = readFileConent(file);
if(fileContent.contains(search)){
System.out.printf( "线程: %s 找到子目标字符串%s,在文件:%s%n",Thread.currentThread().getName(), search,file);

}
}

public String readFileConent(File file){
try (FileReader fr = new FileReader(file)) {
char[] all = new char[(int) file.length()];
fr.read(all);
return new String(all);
} catch (IOException e) {
e.printStackTrace();
return null;
}

}

}

Lock

使用Lock

使用Lock也能类似的达到同步效果.

Lock是一个接口,为了使用一个Lock对象,需要用到:

1
Lock lock = new ReentrantLock();

与synchronized类似的是,调用Lock对象的lock()方法表示当前线程占有Lock对象,

不同的是,synchronized块结束后就释放对象,而要释放Lock对象要调用unlock()方法,为了保证释放的执行,往往会把unlock() 放在finally中进行。

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
64
65
66
67
68
69
70
71
72
73
74
75
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {

public static String now() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void log(String msg) {
System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
}

public static void main(String[] args) {
Lock lock = new ReentrantLock();

Thread t1 = new Thread() {
public void run() {
try {
log("线程启动");
log("试图占有对象:lock");

lock.lock();

log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);

} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log("释放对象:lock");
lock.unlock();
}
log("线程结束");
}
};
t1.setName("t1");
t1.start();
try {
//先让t1飞2秒
Thread.sleep(2000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Thread t2 = new Thread() {

public void run() {
try {
log("线程启动");
log("试图占有对象:lock");

lock.lock();

log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);

} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log("释放对象:lock");
lock.unlock();
}
log("线程结束");
}
};
t2.setName("t2");
t2.start();
}

}

使用Try-lock

使用synchronized有可能产生死锁问题,因为它是非常钻牛角尖的.回忆死锁部分,线程1占有对象A后还要占有B,线程2占有了B后还要占有A,两边都在等待.

而try-lock会在指定时间内试图占用,能占到就占,不能占到就等会,等的久了就不等了.

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {

public static String now() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void log(String msg) {
System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
}

public static void main(String[] args) {
Lock lock = new ReentrantLock();

Thread t1 = new Thread() {
public void run() {
boolean locked = false;
try {
log("线程启动");
log("试图占有对象:lock");

locked = lock.tryLock(1,TimeUnit.SECONDS);
if(locked){
log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);
}
else{
log("经过1秒钟的努力,还没有占有对象,放弃占有");
}

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

if(locked){
log("释放对象:lock");
lock.unlock();
}
}
log("线程结束");
}
};
t1.setName("t1");
t1.start();
try {
//先让t1飞2秒
Thread.sleep(2000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Thread t2 = new Thread() {

public void run() {
boolean locked = false;
try {
log("线程启动");
log("试图占有对象:lock");

locked = lock.tryLock(1,TimeUnit.SECONDS);
if(locked){
log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);
}
else{
log("经过1秒钟的努力,还没有占有对象,放弃占有");
}

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

if(locked){
log("释放对象:lock");
lock.unlock();
}
}
log("线程结束");
}
};
t2.setName("t2");
t2.start();
}

}

locked = lock.tryLock(1``,TimeUnit.SECONDS); ,tryLock()方法返回一个Boolean值,可以设定具体等待时间.这样的话trylock就不一定能占用对象,所以后面释放锁时一定要判断是否占用成功,如果没成功就释放会抛出异常.

总结

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。
  2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。
  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

另一种交互

前面使用wait(),notify(),notifyAll()和synchronized来交互,现在用Condition对象的await(),signal(),signalAll()来和Lock交互.

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

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyStack<T> {

LinkedList<T> values = new LinkedList<T>();

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

public void push(T t) {
try {
lock.lock();
while (values.size() >= 200) {
try {
condition.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
condition.signalAll();
values.addLast(t);
} finally {
lock.unlock();
}

}

public T pull() {
T t=null;
try {
lock.lock();
while (values.isEmpty()) {
try {
condition.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
condition.signalAll();
t= values.removeLast();
} finally {
lock.unlock();
}
return t;
}

public T peek() {
return values.getLast();
}
}

原子访问

原子性操作概念

所谓的原子性操作即不可中断的操作,比如赋值操作int i=7,而i++就不是,它需要先取i的值,然后i+1,然后将新值赋给i,这其中每一步都有可能被别的线程插入,导致同步问题.

AtomicInteger

JDK6以后,新增加了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicInteger。 而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的。换句话说,自增方法incrementAndGet是线程安全的,同一个时间,只有一个线程可以调用这个方法。

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

import java.util.concurrent.atomic.AtomicInteger;

public class TestThread {

public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicI =new AtomicInteger();
int i = atomicI.decrementAndGet();
int j = atomicI.incrementAndGet();
int k = atomicI.addAndGet(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
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
package multiplethread;

import java.util.concurrent.atomic.AtomicInteger;

public class TestThread {

private static int value = 0;
private static AtomicInteger atomicValue =new AtomicInteger();
public static void main(String[] args) {
int number = 100000;
Thread[] ts1 = new Thread[number];
for (int i = 0; i < number; i++) {
Thread t =new Thread(){
public void run(){
value++;
}
};
t.start();
ts1[i] = t;
}

//等待这些线程全部结束
for (Thread t : ts1) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

System.out.printf("%d个线程进行value++后,value的值变成:%d%n", number,value);
Thread[] ts2 = new Thread[number];
for (int i = 0; i < number; i++) {
Thread t =new Thread(){
public void run(){
atomicValue.incrementAndGet();
}
};
t.start();
ts2[i] = t;
}

//等待这些线程全部结束
for (Thread t : ts2) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.printf("%d个线程进行atomicValue.incrementAndGet();后,atomicValue的值变成:%d%n", number,atomicValue.intValue());
}

}

多线程部分到此结束,但是遗漏了非常多的内容没有学,时间比较赶,过段时间我会把知识点在梳理一遍.当然,多线程是个很复杂的问题,人家大佬能直接写本书,以后有空就学吧.