Java·多线程

线程和进程

进程是资源分配的最小单位,线程是CPU调度的最小单位
做个简单的比喻:

  1. 进程=火车,线程=车厢线程在进程下行进(单纯的车厢无法运行)
  2. 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  3. 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  4. 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  5. 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  6. 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  7. 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  8. 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁”
  9. 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

创建多线程

方法一:继承Thread类

1 创建一个继承Thread类的子类

2 重写Thread类的run方法

3 创建该类的对象

4 通过该对象调用start方法

我们来写一个遍历0~100的数的例子:

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 com.xzc;

// 1. 创建一个继承Thread类的子类
class MyThread extends Thread{
// 2. 重写Thread类的run方法
@Override
public void run(){
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class Test {

public static void main(String[] args) {
//3. 创建该类的对象
MyThread t1 = new MyThread();
//4. 通过该对象调用start方法
t1.start();
for (int i = 0; i < 100; i++) {
if(i % 2 == 1){
System.out.println(i);
}
}
}
}

输出结果
在这里插入图片描述

可以看到在main线程输出到43的时候,我们输出偶数的线程开始输出,当然,每次执行结果可能会不一样。start方法有两个作用:1.启动当前线程。2.调用当前线程的run方法。
注意两个问题:
1.我们不能通过run方法去执行,因为这样不会启动线程。
2.我们不能让一个已经start的对象再次start,会报异常。

线程有关方法

void start()

启动线程,并执行对象的run方法。

void run()

线程被执行时进行的操作。

string getName()

返回线程的名称。

void setName(string name)

设置线程的名称。

static Thread currentThread()

返回当前线程。在Thread子类中就是this,通常用于主线程和runnable实现类。

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 com.xzc;

// 1. 创建一个继承Thread类的子类
class MyThread extends Thread{
// 2. 重写Thread类的run方法
@Override
public void run(){
for (int i = 0; i < 10; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class Test {

public static void main(String[] args) {
//3. 创建该类的对象
MyThread t1 = new MyThread();
t1.setName("Thread One");
//4. 通过该对象调用start方法
t1.start();
Thread.currentThread().setName("Thread Main");
for (int i = 0; i < 10; i++) {
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

输出结果:

Thread Main:1
Thread One:0
Thread Main:3
Thread Main:5
Thread Main:7
Thread One:2
Thread Main:9
Thread One:4
Thread One:6
Thread One:8

我们也可以通过构造器去起名。

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
package com.xzc;

// 1. 创建一个继承Thread类的子类
class MyThread extends Thread{
public MyThread(){

}
public MyThread(String name){
super(name);
}
// 2. 重写Thread类的run方法
@Override
public void run(){
for (int i = 0; i < 10; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class Test {

public static void main(String[] args) {
//3. 创建该类的对象
MyThread t1 = new MyThread("Thread One");
//4. 通过该对象调用start方法
t1.start();
Thread.currentThread().setName("Thread Main");
for (int i = 0; i < 10; i++) {
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

输出结果:

Thread One:0
Thread Main:1
Thread One:2
Thread Main:3
Thread One:4
Thread Main:5
Thread One:6
Thread Main:7
Thread One:8
Thread Main:9

static void yield()

释放cpu的执行权。释放之后,执行权可能会被另一个线程所拿到。

void join()

在线程A中调用线程B的join方法,此时线程A就进入阻塞状态,直到线程B执行完成,线程A才可以执行。此方法会抛出异常。

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
package com.xzc;

// 1. 创建一个继承Thread类的子类
class MyThread extends Thread{
public MyThread(){

}
public MyThread(String name){
super(name);
}
// 2. 重写Thread类的run方法
@Override
public void run(){
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}

}
}
}
public class Test {

public static void main(String[] args) {
//3. 创建该类的对象
MyThread t1 = new MyThread("Thread One");
//4. 通过该对象调用start方法
t1.start();
Thread.currentThread().setName("Thread Main");
for (int i = 0; i < 50; i++) {
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName()+":"+i);
}
if(i == 19){
try {
t1.join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}

我们这里分别两个线程输出0~50的偶数和奇数,看到设置main线程执行到19的时候,调用t1的join方法,之后一直执行t1,直到t1结束,才执行了main线程。
在这里插入图片描述

static void sleep(long millis)

线程休眠millis毫秒。当前线程进入阻塞状态。

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
package com.xzc;

// 1. 创建一个继承Thread类的子类
class MyThread extends Thread{
public MyThread(){

}
public MyThread(String name){
super(name);
}
// 2. 重写Thread类的run方法
@Override
public void run(){
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}

}
}
}
public class Test {

public static void main(String[] args) {
//3. 创建该类的对象
MyThread t1 = new MyThread("Thread One");
//4. 通过该对象调用start方法
t1.start();
Thread.currentThread().setName("Thread Main");
for (int i = 0; i < 50; i++) {
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName()+":"+i);
}
if(i == 19){
try {
t1.join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}

我们这里设置线程1,是偶数的时候休眠1秒,然后主线程直接输出到19,然后t1又join进来了,然后1秒1个t1的输出,直到t1输出完,主线程才能继续执行。
在这里插入图片描述

boolean isAlive()

判断当前线程是否存活。

线程的调度

Java的调度方法:
1.同优先级线程组成先进先出队列,使用时间片策略。
2.对高优先级,使用优先调度的抢占式策略。

线程的优先级等级

MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5

涉及的方法

getPriority():返回线程的优先级。
setPriority(int newPriority):设置线程的优先级。

说明

线程创建时继承父线程的优先级。
低优先级只是获得调度的概率低,不一定真的比高优先级调用后再调用。

从卖票入手

先看多线程的一个经典例子:创建三个窗口卖票,总票数是100张。
先看这个代码:

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 com.xzc;

class Window extends Thread{
public Window(){

}
public Window(String name){
super(name);
}
private int ticket = 100;

@Override
public void run() {
while (true){
if(ticket > 0){
System.out.println(getName()+":卖票,票号为:"+ticket--);
}else {
break;
}
}
}
}
public class Test {
public static void main(String[] args) {
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
}

}

输出结果:
在这里插入图片描述
出现了同一张票,被多个窗口卖出的情况。这是因为我们提供的ticket是实例变量,属于每一个对象,每一个线程,这个资源不被共享。如果我们把ticket设为静态变量:

java private static int ticket = 100;
输出结果:
在这里插入图片描述
大致是共享了资源,但是还会出现一些问题!实际上这里就有线程安全问题。这里输出是因为要有一定延迟,所以从输出结果看并不是依次递减。

创建多线程

方法二:实现Runnable接口

1.创建一个实现了Runnable接口的类
2.实现Runnable类的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器,创建Thread类的对象
5.通过Thread类对象调用start方法。

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

class MyThread implements Runnable{

@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}

public class Test {
public static void main(String[] args) {
new Thread(new MyThread(),"线程一").start();
}

}

输出结果:
在这里插入图片描述
遍历了0~100的偶数,这里就是创建一个实现了runnable类接口的类的对象,把他作为参数传入Thread类的构造器,其实该构造器还可以有一个参数,就是线程名,我们这里直接用。这里的start有两个作用:1.启动线程。2.调用Runnable类型的target的run方法。我们可以重用这个类,再启动一个线程。

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

class MyThread implements Runnable{

@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}

public class Test {
public static void main(String[] args) {
new Thread(new MyThread(),"线程一").start();
new Thread(new MyThread(),"线程二").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
package com.xzc;

class Window implements Runnable{
public Window(){}
private int ticket;
public Window(int ticket){
this.ticket = ticket;
}
@Override
public void run() {
while (true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+ticket--);
}else {
break;
}
}
}
}

public class Test {
public static void main(String[] args) {
Window w = new Window(100);
new Thread(w,"窗口一").start();
new Thread(w,"窗口二").start();
new Thread(w,"窗口三").start();
}

}

输出结果:
在这里插入图片描述
可以发现,已经能比较好地实现资源共享了,但是这种实现方式也有线程安全问题,也会有重票的问题。但是,我们没有加static关键字,就有100张票,这是因为虽然ticket是实例变量,但是我们只有一个window对象放到构造器当参数,自然而然就是同一个ticket。

两种创建多线程方式

开发中,优先选择实现Runnable接口的方式,原因:
1.实现的方式没有类的单继承的局限性,可以让类继承一些特定的父类。
2.实现的方式更适合来处理多个线程共享数据的情况。
联系:Thread本身就实现了Runnable接口。
相同点:两种方式都要重写run方法。

线程的生命周期

在这里插入图片描述
在这里插入图片描述

线程安全

如果我们让实现runnable接口的车票程序的run方法里加个sleep

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
package com.xzc;

class Window implements Runnable{
public Window(){}
private int ticket;
public Window(int ticket){
this.ticket = ticket;
}
@Override
public void run() {
while (true){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+ticket--);
}else {
break;
}
}
}
}

public class Test {
public static void main(String[] args) {
Window w = new Window(100);
new Thread(w,"窗口一").start();
new Thread(w,"窗口二").start();
new Thread(w,"窗口三").start();
}

}

输出结果:
在这里插入图片描述
输出0和-1的概率大大提升。这是因为有一张票的时候,我们t1线程进来,被阻塞,还没输出,t2进来,也被阻塞,同理t3,然后就出现了如下情况:
在这里插入图片描述
不管是重票还是错票,问题出现的原因都是当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。如何解决?当一个线程在操作ticket的时候,其他线程不能参与进来,直到该线程完成操作。这种情况,即使该线程被阻塞,也不能被改变。

同步代码块解决实现Runnable接口的线程安全问题

关键字:synchronized

1
2
3
4
5
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
package com.xzc;

class Window implements Runnable{
public Window(){}
private int ticket;
public Window(int ticket){
this.ticket = ticket;
}
Object obj = new Object();
@Override
public void run() {

while (true){
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--);
} else {
break;
}
}
}
}
}

public class Test {
public static void main(String[] args) {
Window w = new Window(100);
new Thread(w,"窗口一").start();
new Thread(w,"窗口二").start();
new Thread(w,"窗口三").start();
}

}

输出结果:
在这里插入图片描述
一定注意要求多个线程,共用同一把锁,这里我们一个window对象,就一个对象obj,保证了所有线程共用一个锁。不会出现线程安全问题。
同步的好处时解决了线程安全问题,但是操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程过程,效率比较低。

同步代码块解决实现继承Thread类的线程安全问题

因为此时是不一样的window对象,如果和之前用一样的方法,肯定不能共用一个锁,因为每一个window对象的obj实例不一样,导致无法共用一个锁。把obj设为static即可

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 com.xzc;

class Window2 extends Thread{
public Window2(){

}
public Window2(String name){
super(name);
}
private static int ticket = 100;
private static final Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket--);
} else {
break;
}
}
}
}
}
public class Test2 {
public static void main(String[] args) {
Window2 w1 = new Window2("窗口一");
Window2 w2 = new Window2("窗口二");
Window2 w3 = new Window2("窗口三");
w1.start();
w2.start();
w3.start();
}

}

输出结果:
在这里插入图片描述
在实现Runnable接口的锁,其实直接用this关键字即可。synchronized (this){},因为这里this指的是Window的一个对象,他是实现Runnable接口的类的对象,唯一,所以为了方便,不用new一个object,直接用this即可。但是在继承thread类的对象中,就不能用this了,因为我们new了三个对象出来,不是唯一的。有什么简便方法呢,用反射。

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 com.xzc;

class Window2 extends Thread{
public Window2(){

}
public Window2(String name){
super(name);
}
private static int ticket = 100;
private static final Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (Window2.class) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket--);
} else {
break;
}
}
}
}
}
public class Test2 {
public static void main(String[] args) {
Window2 w1 = new Window2("窗口一");
Window2 w2 = new Window2("窗口二");
Window2 w3 = new Window2("窗口三");
w1.start();
w2.start();
w3.start();
}

}

我们在synchronized参数传入Window2的Class对象即可。synchronized (Window2.class)

同步方法解决实现Runnable的线程安全问题

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。

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
package com.xzc;

class Window3 implements Runnable {
public Window3() {
}

private int ticket;

public Window3(int ticket) {
this.ticket = ticket;
}

@Override
public void run() {
while (true) {
show();
}
}

private synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--);
}
}
}

public class Test3 {
public static void main(String[] args) {
Window3 w = new Window3(100);
new Thread(w, "窗口一").start();
new Thread(w, "窗口二").start();
new Thread(w, "窗口三").start();
}

}

输出结果:
在这里插入图片描述
注意这里:private synchronized void show()就是同步方法处理。
这里的锁没有显示出来,但是也是有锁,在show方法里,锁就是this

同步方法解决继承Thread的线程安全问题

此时不能像之前一样,直接把show方法定义为synchronized的,因为锁的不唯一。如果非想用同步方法解决,就要在synchronized前面加一个static声明为静态方法。

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 com.xzc;

class Window4 extends Thread {
public Window4() {

}

public Window4(String name) {
super(name);
}

private static int ticket = 100;

@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket--);
}
}
}

public class Test4 {
public static void main(String[] args) {
Window4 w1 = new Window4("窗口一");
Window4 w2 = new Window4("窗口二");
Window4 w3 = new Window4("窗口三");
w1.start();
w2.start();
w3.start();
}

}

输出结果:
在这里插入图片描述
此时,我们的锁是当前类Window4.class

同步方法总结

同步方法仍然涉及到锁,只是不需要我们显示声明

非静态的同步方法,同步监视器是this

静态的同步方法,同步监视器是当前类本身(即Class类对象)

线程安全的单例模式之懒汉式

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

public class BankTest {

}

//懒汉式
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
if (instance == null){
instance = new Bank();
}
return instance;
}
}

这样是不线程安全的,如果我们有多个线程run方法调用getInstance方法,其中一个线程进来,首次instance肯定是null,然后刚进if语句,可能会被阻塞,即使不被阻塞,cpu也可能切换到线程2,另外一个线程进来,那么instance会先后两次赋值,显然是不对的,因为instance是静态Bank类型变量。instance相当于是共享数据了。下面用同步方式修改为线程安全的:

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

public class BankTest {

}

//懒汉式
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
/*synchronized(Bank.class) {
if (instance == null) {
instance = new Bank();
}
return instance;
}*/
//方式二
if (instance == null){
synchronized (Bank.class){
if(instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}

我们先看方式一,return如果在synchronized同步代码块里面,为什么效率会低,这是因为不管instance是不是null,都进入同步代码块,如果线程非常多,就会产生效率低下问题,假设线程一拿到锁,产生一个instance返回,后面的所有线程还要在同步代码块前面等着,事实上这是无意义的,因为我们已经有instance了,后面的线程拿着instance返回就好了。而方式二就修正了这个问题,我们在同步代码块面前先判断instance是不是空,并且把return instance提出来,这样线程一二三可能在同步代码块中抢一下锁,卡一会,但是后面的所有线程进来,直接就判断instance不是null了,然后直接返回instance就好了。
这样我们就把单例模式的懒汉式修改为线程安全的了。

线程死锁

在这里插入图片描述
当我们有两个锁嵌套的时候,就比较容易出现死锁问题:

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

public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();

new Thread(() -> {
synchronized (s2) {
s1.append("c");
s2.append("3");

synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}

输出结果
在这里插入图片描述
这段代码其实就有死锁隐患,这是线程顺次执行完了,我们用sleep阻塞一下:

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

public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();

new Thread(() -> {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}

当我们在嵌套的分解处加入sleep进行阻塞后,就出现了死锁问题,这段代码执行起来,就和死循环一样,不会终止,但是也不会报错,这是因为我们第一个线程执行,拿到s1这把锁,执行sleep阻塞,这时候第二个线程很大概率也被调度,拿到s2这把锁,然后执行sleep阻塞,这时候第一个线程手里有s1这把锁,醒过来后发现没有s2这把锁,没法进去下面的同步代码段,这是因为s2被第二个线程拿了,同理s2醒过来后也拿不到s1,就僵持住了,发生了死锁。
也就是发生了:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成死锁。

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

import java.util.concurrent.locks.ReentrantLock;

//Lock锁解决线程安全问题-JDK5.0新增
class Window implements Runnable{

private int ticket = 100;
//定义reentrantlock对象
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true){
try {
//调用lock方法,锁住
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
}
else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
new Thread(w,"Window - 1").start();
new Thread(w,"Window - 2").start();
new Thread(w,"Window - 3").start();
}

}

输出结果:
在这里插入图片描述
可以看到我们使用Reentrantlock类可以实现同步的方式,但是需要我们加锁解锁,synchronized是自动执行,加锁解锁一般就如上使用try-finally执行,一般就是先定义Reentrantlock对象,然后try里面锁住,finally里面解锁。Reentrantlock构造器还可以有一个参数:boolean fair,如果为true,则实现公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。

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

import java.util.concurrent.locks.ReentrantLock;

//Lock锁解决线程安全问题-JDK5.0新增
class Window implements Runnable{

private int ticket = 100;
//定义reentrantlock对象
private ReentrantLock lock = new ReentrantLock(true);

@Override
public void run() {
while (true){
try {
//调用lock方法,锁住
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
}
else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
new Thread(w,"Window - 1").start();
new Thread(w,"Window - 2").start();
new Thread(w,"Window - 3").start();
}

}

这里我们用了公平锁,结果:
在这里插入图片描述
我们从这可以看到,如果三个线程一开始的等待时间确定了,后面一定是一样的,1进拿锁,3进,2进则3一定等的比2长,3拿锁,1执行完了后2一定比1等的长,2拿锁,后面同理,所以公平锁可以看做先进先出。

synchronized与Lock的异同

(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。

ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。

一般优先顺序(实际上都一样):
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
package demo01;

import java.util.concurrent.locks.ReentrantLock;

class User implements Runnable {
private int money;
public User() {
}
public User(int initMoney) {
money = initMoney;
}
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
lock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money += 1000;
System.out.println(Thread.currentThread().getName() + "存了1000元," + "现有资金:" + money);
} finally {
lock.unlock();
}
}
}
}
public class Test {
public static void main(String[] args) {
User u = new User(0);
new Thread(u, "Human - 1").start();
new Thread(u, "Human - 2").start();
}
}

输出结果:
在这里插入图片描述
这里注意我们用的lock方式,因为采用实现runnable接口的方式来多线程,所以我们的锁是同一个,如果我们采用继承thread类方式,我们要把lock设为static的,以保证锁的唯一。

线程的通信

例题:使用两个线程打印1-100。线程和线程2交替打印。

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
package demo02;
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while (true){
synchronized (this) {
notify();
if (number <= 100){
System.out.println(Thread.currentThread().getName()+":"+number++);
try {
//使得调用如下wait方法得到线程进入阻塞状态
//一旦执行wait,会释放锁,这里和sleep不一样,sleep不会释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
public class Communication {
public static void main(String[] args) {
Number number = new Number();
new Thread(number,"线程1").start();
new Thread(number,"线程2").start();
}
}

输出结果:
在这里插入图片描述
这里我们用了wait和notify两个方法进行线程通信,注意notify是按优先级唤醒某个线程,notifyAll是唤醒所有线程,在这里我们只有两个线程,一个线程执行notify和notifyAll效果是一样的。而调用wait方法得到线程进入阻塞状态,一旦执行wait,会释放锁,这里和sleep不一样,sleep不会释放锁。所以逻辑就很清晰了,线程1先进来拿到锁,唤醒2(这里其实刚开始2没有wait),然后打印1,1进入wait状态,释放锁,线程2拿到锁,进来,唤醒1,1没有锁也进不来,线程2打印2,然后进入wait,周而复始。
wait,notify,notifyAll三个方法只能够出现在同步代码块或同步方法中,用lock都不行
这三个方法的调用者,必须是同步代码块或同步方法中的同步监视器,也即锁,所以用lock实现锁不能用这三个方法
所以我们上述代码中的wait();也就是this.wait();,因为锁用了this,而this又可以省略,如果用了别的锁,则不能省略。
这三个方法,是定义在java.lang.Object中的,所以任意对象可以当锁,可以调用wait等方法。

sleep和wait的异同

一旦执行方法,都会使得当前的线程进入阻塞状态。
区别:
1、来自不同的类:sleep是Thread的静态类方法,wait是Object类的方法。
2、有没有释放锁(释放资源):sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。
3、sleep可以在任何需要的场景下调用,而wait方法必须在同步代码块或同步方法中使用。
4、一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
5、sleep必须捕获异常,而wait,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
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 demo02;

class Producer implements Runnable{
private Clerk clerk;
public Producer(){}
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始生产产品……");
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer implements Runnable{
private Clerk clerk;
public Consumer(){}
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始消费产品……");
while (true){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
class Clerk{
public Clerk(){}
public Clerk(int productCount){
this.productCount = productCount;
}
//已经有的产品数量
private int productCount;
//生产产品给到店员
public synchronized void produceProduct() {//锁是Clerk的实例对象
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");
//只要有一个产品存在,就可以唤醒消费者来消费了
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费者从店员处消费产品
public synchronized void consumeProduct() {//锁是Clerk的实例对象
if(productCount > 0){
System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品");
productCount--;
//只要消费了,就必小于20了,可以唤醒生产者生产了
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Product {
public static void main(String[] args) {
Clerk clerk = new Clerk(0);
Producer p1 = new Producer(clerk);
Thread t1 = new Thread(p1,"生产者1");
Consumer c1 = new Consumer(clerk);
Consumer c2 = new Consumer(clerk);
Thread t2 = new Thread(c1,"消费者1");
Thread t3 = new Thread(c2,"消费者2");
t1.start();
t2.start();
t3.start();
}
}


执行结果:
在这里插入图片描述

JDK5.0新增线程创建方式

实现Callable接口

在这里插入图片描述

在这里插入图片描述
FutureTask同时实现Runnable接口和Future接口,所以可以放进Thread类构造器。
1.创建一个实现Callable的实现类。
2.覆写call方法,将此线程需要执行的操作写在call中,注意有返回值。
3.创建Callable接口实现类的对象。
4.将此Callable接口实现类的对象作为参数传递到FutureTask的构造器,创建FutureTask的对象。
5.将该FutureTask对象传入Thread构造器,创建Thread对象,并调用start方法。
如果对call返回值感兴趣,调用get方法获取返回值。

如何理解实现Callable接口的方式比实现Runnable接口方式更强大?
1.call可以有返回值。
2.call可以抛出异常,被外部捕获,获取异常的信息。
3.Callable支持泛型。

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

//实现Callable接口

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class NumThread implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
if(i % 2 == 0){
sum += i;
System.out.println(i);
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask(numThread);
//FutureTask实现了Runnable接口 所以可以放
new Thread(futureTask).start();
try {
//get方法的返回值即为FutureTask构造器参数Callable实现类重写的call方法的返回值。
//必须线程启动了之后才能get 要不然什么也get不到还无法结束程序
Object o = futureTask.get();
System.out.println("总和为"+o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

结果
在这里插入图片描述
使用泛型:

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

//实现Callable接口

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class NumThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
if(i % 2 == 0){
sum += i;
System.out.println(i);
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
NumThread numThread = new NumThread();
FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread);
//FutureTask实现了Runnable接口 所以可以放
new Thread(futureTask).start();
try {
//get方法的返回值即为FutureTask构造器参数Callable实现类重写的call方法的返回值。
//必须线程启动了之后才能get 要不然什么也get不到还无法结束程序
Integer o = futureTask.get();
System.out.println("总和为"+o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

使用线程池

在这里插入图片描述
在这里插入图片描述
下面只看创建一个可重用固定线程数的线程池

java ExecutorService service = Executors.newFixedThreadPool(n);
我们声明的ExecutorService类的service有两个方法,一个是execute方法,适用于Runnable接口的实现类,一个是submit方法,适用于Callable接口实现类。执行了后,用shutdown方法结束线程池。

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

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class NumThread1 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class NumThread2 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new NumThread2());
service.execute(new NumThread1());
service.shutdown();
}
}

结果:
在这里插入图片描述
这个过程大致如下:
1.提供指定线程数量的线程池。
2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口的类的对象。
3.关闭线程池。
开发中一般都用线程池,因为有如下好处:
在这里插入图片描述
线程池的管理,因为ExecutorService是接口,我们没法设置属性,因为接口有属性也是常量,所以我们要找他的实现类,通过反射可以找到为ThreadPoolExecutor,此时我们把service强制转换为ThreadPoolExecutor类,就可以进行属性设置了
在这里插入图片描述

在这里插入图片描述

守护线程

java中的线程分为两种:守护线程(Daemon)用户线程(User)

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

两者的区别:

唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的线程;比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开

优雅地停止线程

Thread类中的stop方法已经被舍弃了。
除了stop方法,还有几个方法也被禁用了:销毁多线程(destroy),挂起多线程(suspend),恢复挂起(resume),之所以废除这些方法是因为有可能导致线程死锁。
范例:实现线程柔和地停止。

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


public class ThreadDemo {
private static boolean flag = true;
public static void main(String[] args) throws Exception{
new Thread(()->{
long num = 0;
while (flag){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在执行,num="+num++);
}
},"执行线程").start();
System.out.println(Thread.currentThread().getName());
Thread.sleep(200);
flag = false;
}
}

输出结果:

main
执行线程正在执行,num=0
执行线程正在执行,num=1
执行线程正在执行,num=2
执行线程正在执行,num=3

就是线程main先休眠200ms后,将flag设为false,这样“执行线程”每50ms输出一次,输出4次后run方法里的while循环就结束了,线程就停止了。
万一现在有其他线程去控制这个flag的内容,那么对于这个执行线程的停止也不是说停就立刻停,而是会在执行中判断flag内容完成。这样就能解决线程死锁。

守护线程例子

还没设守护线程:

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


public class ThreadDemo {

public static void main(String[] args) throws Exception{
Thread userThread = new Thread(()->{
for (int i = 0 ; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i);
}
},"用户线程");//完成核心业务。
Thread daemonThread = new Thread(()->{
for (int i = 0 ; i < Integer.MAX_VALUE; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i);
}
},"守护线程");
userThread.start();
daemonThread.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
package demo02;


public class ThreadDemo {

public static void main(String[] args) throws Exception{
Thread userThread = new Thread(()->{
for (int i = 0 ; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i);
}
},"用户线程");//完成核心业务。
Thread daemonThread = new Thread(()->{
for (int i = 0 ; i < Integer.MAX_VALUE; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i);
}
},"守护线程");
daemonThread.setDaemon(true);
userThread.start();
daemonThread.start();
}
}

在这里插入图片描述

volatile关键字

在多线程的定义中,volatile关键字主要是在属性定义上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理。在正常进行变量处理的时候往往会经历如下一个步骤:
1.获取变量原有的数据内容副本
2.利用副本为变量进行数学计算
3.将计算后的变量,保存到原始空间之中
而如果一个属性上追加了volatile关键字,表示就是不使用副本,而是直接操作原变量,相当于节约了拷贝副本,重新保存的步骤。
在这里插入图片描述
用了volatile关键字,该加同步还是要加,只不过volatile是直接内存操作,没有拷贝与赋值环节了。

volatile和synchronized区别

在这里插入图片描述
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package demo02;

//两个线程操作的资源
class Resource{
Resource(){

}
Resource(int num,boolean flag){
this.num = num;
this.flag = flag;
}
//操作的数字
private int num;
//true表示执行加法 false表示执行减法
private boolean flag;
//加法操作
public synchronized void inc() throws InterruptedException {
if(!flag){
wait();
}
this.num++;
System.out.println("[加法操作]->num:"+this.num);
flag = false;
notifyAll();
}
//减法操作
public synchronized void sub() throws InterruptedException {
if (flag){
wait();
}
this.num--;
System.out.println("[减法操作]->num:"+this.num);
flag = true;
notifyAll();
}
}
//加法线程
class IncThread implements Runnable{
Resource r;
IncThread(){}
IncThread(Resource r){
this.r = r;
}

@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 50; i++) {
try {
r.inc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//减法线程
class SubThread implements Runnable{
Resource r;
SubThread(){}
SubThread(Resource r){
this.r = r;
}

@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 50; i++) {
try {
r.sub();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestOne {
public static void main(String[] args) {
Resource r = new Resource(0,true);
IncThread i1 = new IncThread(r);
IncThread i2 = new IncThread(r);
SubThread s1 = new SubThread(r);
SubThread s2 = new SubThread(r);
new Thread(i1,"加法线程1").start();
new Thread(i2,"加法线程2").start();
new Thread(s1,"减法线程1").start();
new Thread(s2,"减法线程2").start();
}

}

这也是一个很经典的多线程问题,主要是要考虑到加减要交错,该加的时候就不能减,要控制方向,用一个flag实现线程的通信。

案例分析二:生产电脑

设计一个生产电脑和搬运电脑类,要求生产出一台电脑就搬走一台电脑,如果没有新的电脑生产出来,则搬运工需要等待新电脑生产出,如果生产出的电脑没有搬走,则要等待电脑搬走后再生产,并统计电脑数量。
本质还是生产者消费者模型。

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

class Produce implements Runnable{
Res r;
Produce(Res r){
this.r = r;
}

@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
r.make();
System.out.println(Thread.currentThread().getName()+"生产了一台电脑");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
class Get implements Runnable{
Res r;
Get(Res r){
this.r = r;
}

@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
r.fetch();
System.out.println(Thread.currentThread().getName()+"搬走了一台电脑");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}


class Res{
private Computer computer;
public synchronized void make() throws Exception{
if(this.computer != null){//还没有取走
wait();
}
Thread.sleep(200);
this.computer = new Computer("HP",8000);//生产一个电脑
System.out.println(this.computer);
notifyAll();
}
public synchronized void fetch() throws Exception{
if(this.computer == null){//还没有生产
wait();
}
Thread.sleep(100);
this.computer = null;//已经取走了
notifyAll();
}

}
class Computer{
//生产个数
private static int count = 0;
private String name;
private double price;
public Computer(String name,double price){
this.name = name;
this.price = price;
count++;
}

@Override
public String toString() {
return "第"+count+"台电脑{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
public class TestTwo {
public static void main(String[] args) {
Res r = new Res();
Produce p = new Produce(r);
Get g = new Get(r);
new Thread(p,"生产者").start();
new Thread(g,"搬运工").start();
}
}


在这里插入图片描述

案例分析三:竞争抢答

实现一个竞拍抢答程序,设计三个抢答者,同时发出抢答指令,抢答成功者给出成功提示,未能抢答成功给出失败提示。
对于这个多线程牵扯到数据返回问题,那么使用实现Callable接口的方式最好。

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


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyThread1 implements Callable<String>{
private boolean flag = false;

@Override
public String call() throws Exception {
synchronized (this){
if(!flag){//拿到了锁进入了同步代码块 此时flag还是false 意味着没人抢答
flag = true;
return Thread.currentThread().getName()+"抢答成功";
}else{
return Thread.currentThread().getName()+"抢答失败";
}
}
}
}
public class TestThree {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread1 m = new MyThread1();
FutureTask<String> futureTask1 = new FutureTask<String>(m);
FutureTask<String> futureTask2 = new FutureTask<String>(m);
FutureTask<String> futureTask3 = new FutureTask<String>(m);
new Thread(futureTask1,"抢答者1").start();
new Thread(futureTask2,"抢答者2").start();
new Thread(futureTask3,"抢答者3").start();
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
System.out.println(futureTask3.get());
}
}


在这里插入图片描述


Java·多线程
http://example.com/2021/05/05/Java%C2%B7%E5%A4%9A%E7%BA%BF%E7%A8%8B%C2%B7%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89/
作者
AHawkeye
发布于
2021年5月5日
许可协议