[TOC]

一. 多线程的实现.

1. Thread

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 cn.ngp.demo;
//线程操作主类
class MyThread extends Thread { //这就是一个多线程的操作类
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() { //覆写run()方法,作为线程的主体操作方法
for (int i = 0; i < 50; i++) {
System.out.println(this.title + "->" + i);
}
}
}
public class TestDemo { //主类
public static void main(String[] args) {
MyThread mt1 = new MyThread("线程1");
MyThread mt2 = new MyThread("线程2");
MyThread mt3 = new MyThread("线程3");
mt1.start();
mt2.start();
mt3.start();
}
}
/*
疑问: 为什么多线程必须使用start()而不是run()?
1 Thread类的方法里面存在有一个"IllegalThreadStateException"异常,并且属于RuntimeException.
如果一个线程重复启动就会抛出此异常
2 start()方法里面要调用start0()方法,而且此方法的结构与抽象方法类似,使用了native声明,在Java的
开发里面有一门技术成为JNI技术(JavaNative Interface),这么技术的特点: 是使Java调用本机操作系统提供的函数。
但是这样的技术有一个缺点,不能够离开特定的操作系统。
如果要想线程能够执行,需要操作系统来进行资源分配,所以此操作严格来讲主要是由JVM负责根据不同的操作的操作系统
而实现的。
即: 使用Thread类的start()方法不仅仅要启动多线程的执行代码,还要去根据不同的操作系统来进行资源的分配。
所以要用start()
*/

2. Runnanle

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 cn.ngp.demo;
//线程操作主类
class MyThread implements Runnable { //这就是一个多线程的操作类
private int ticket = 10 ;
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() { //覆写run()方法,作为线程的主体操作方法
// for (int i = 0; i < 50; i++) {
// System.out.println(this.title + "->" + i);
// }
for(int x = 0; x < 100; x ++) {
if(this.ticket > 0) {
System.out.println(this.title + " ticket = " + this.ticket -- );
}
}
}
}
public class TestDemo { //主类
public static void main(String[] args) {
// MyThread mt1 = new MyThread("线程1");
// MyThread mt2 = new MyThread("线程2");
// MyThread mt3 = new MyThread("线程3");
// new Thread(mt1).start();
// new Thread(mt2).start();
// new Thread(mt3).start();
MyThread mt = new MyThread("线程1");
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}

题: 请解释Thread类于Runnable接口实现多线程的区别?(请解释多线程两种实现方法的区别?)

  • Thread类是Runnable接口的子类,使用Runable接口实现多线程可以避免单继承局限
  • Runnable接口实现的多线程可以比Thread类实现的多线程更加清楚的描述数据共享的概念

Runnable是如何避免java单继承带来的局限性的?

  • (接口实现和类继承的角度来说)如果你想写一个类C,但这个类C已经继承了一个类A,此时,你又想让C实现多线程。用继承Thread类的方式不行了。(因为单继承的局限性)
    此时,只能用Runnable接口。
    Runnable接口就是为了解决这种情境出现的。

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
41
package cn.ngp.demo;

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

class MyThread implements Callable<String> {
private int ticket = 10 ;
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public String call() throws Exception{
for(int x = 0; x < 100; x ++) {
if(this.ticket > 0) {
System.out.println(this.title + " ,买票 ticket = " + this.ticket -- );
}
}
return "票已卖光";
}
}
public class TestDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyThread mt1 = new MyThread("线程1");
MyThread mt2 = new MyThread("线程2");
FutureTask<String> task1 = new FutureTask<String>(mt1);//目的是为了取得call()返回结果
FutureTask<String> task2 = new FutureTask<String>(mt2);
//FutureTask是Runnable接口子类,所以可以使用Thread类的构造来接受task对象
new Thread(task1).start();//启动多线程
new Thread(task2).start();
//多线程执行完毕之后可以取得内容,依靠Future中的get()方法完成
System.out.println("A线程的返回结果: " +task1.get());
System.out.println("B线程的返回结果: " +task2.get());
}
}
/*
* 总结:
* 1. 对于多线程的实现,重点在于Runnable接口与Thread类启动的配合上;
* 2. 对于JDK1.5新特性,了解就行,知道区别就在于返回结果上
* */

二. 线程的基本使用方法

1. 命名与取得

多线程的运行状态是不确定的,那么程序的开发之中为了可以获取到一些需要使用到是线程,就只能够依靠线程的名字来进行操作。所以线程的名字是一个至关重要的概念,这样在Thread类之中就提供有线程名称的处理:

  • 构造方法:public Thread(Runnable target,String name);
    设置名字:public final void setName(String name);
    取得名字:public final void getName();
    取得当前线程对象:public static Thread currendThread();

对于线程对象的获得是不可能只是依靠一个this来完成的,因为状态不可控,但是有一点是明确的,所有的线程对象一定要执行run()方法,那么这个时候可以考虑获取当前线程,在Thread类里面提供有获取当前线程的方法:

  • 获取当前线程: public static Thread currentThread()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.ngp.demo;
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class TestDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "线程A").start();
new Thread(mt).start(); //Thread-0
new Thread(mt, "线程B").start();
new Thread(mt).start(); //Thread-1
}
}

实例化Thread类对象的时候没有为其设置名字,会自动编号和命名,这种自动的属性命名主要是依靠了static属性完成的。Thread类里面定义有如下操作:

1
2
3
4
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}

范例: 观察一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.ngp.demo;
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class TestDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "线程对象").start();
mt.run(); //直接调用了run()方法
}
}
/*输出结果:
main
线程对象
**/

通过此时的代码可以发现当使用了 “mt.run()” 直接在主方法之中调用线程类对象中的run()方法所获得的线程对象的名字为 “main”,所以得出一个结论: **主方法也是一个线程。**那么问题来了,所有的线程都是在进程上划分,那么进程在哪里?

范例: 新建一个 Test.java 文件,黏贴以下代码,用cmd编译、运行。:

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
for(Long x = 0L; x < Interger.MAX_VALUE; x ++) {
System.out.println();
}
}
}

(Win10)任务管理器 → 详细信息 → 将会看到java.exe运行中,cmd中按下Ctrl+C,中断程序,java.exe即消失

(win7)任务管理器 → 进程 → 将会看到java.exe运行中,cmd中按下Ctrl+C,中断程序,java.exe即消失

每当使用java命令执行程序的时候就表示启动了一个JVM的进程,一台电脑上可以启动若干个JVM进程,所以每一个JVM的进程都会有各自的线程。

在任何开发之中,主线程可以创建若干个子线程,创建子线程的目的是可以将一些复杂逻辑或者比较耗时的逻辑交给子线程处理;

范例: 子线程处理

1
2
3
4
5
6
7
8
9
10
11
12
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
System.out.println("1. 执行操作任务一。");
int temp = 0 ;
for (int x = 0; x < Integer.MAX_VALUE; x++) {
temp += x ;
}
System.out.println("2. 执行操作任务二。");
System.out.println("n. 执行操作任务N。");
}
}

此时就会耽搁一下,此时就用lambda,用子线程处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
System.out.println("1. 执行操作任务一。");
new Thread(()-> { //子线程负责统计
int temp = 0 ;
for (int x = 0; x < Integer.MAX_VALUE; x++) {
temp += x ;
}
});
System.out.println("2. 执行操作任务二。");
System.out.println("n. 执行操作任务N。");
}
}

主线程负责处理整体流程,而子线程负责耗时操作。

问:** 每一个JVM进程启动的时候至少启动几个线程?

  • mian线程: 程序的主要执行,以及启动子线程;
  • gc线程: 负责垃圾收集

2. 线程的休眠

如果说现在希望某一个线程可以暂缓处理,那么久可以使用休眠的处理,在Thread类之中定义的休眠的方法如下:

  • 休眠: public static void sleep(long millis) throws InterruptedException
  • 休眠: public static void sleep(long millis,int nanos) throws InterruptedException

在进行休眠的时候,有可能会产生中断异常 “InterruptException”,中断异常属于Exception的子类,所以必须处理。

范例: 观察休眠处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
new Thread(()-> {
for(int x = 0 ; x < 10 ; x ++) {
System.out.println(Thread.currentThread().getName() + "、x = " + x);
try {
Thread.sleep(100); // 暂缓执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程对象").start();
}
}

休眠的主要特点是可以实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在有多个线程对象,那么休眠也是有先后顺序的。

**范例:**产生多个线程对象进行休眠处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
Runnable run = ()-> {
for(int x = 0 ; x < 10 ; x ++) {
System.out.println(Thread.currentThread().getName() + "、x = " + x);
try {
Thread.sleep(100); // 暂缓执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} ;
for (int num = 0; num < 5 ; num ++) {
new Thread(run,"线程对象 - " + num).start();
}
}
}

此时将产生五个线程对象,并且这五个线程对象执行的方法体是相同的。此时从程序执行的感觉上来讲,好像是若干个线程一起进行了休眠,而后一起进行了自动唤醒,但是实际上是有差别的。

2

p.s. 所有对象一起进入run方法,但真正执行的时候还是有先有后的。

提示文字也是有先有后

休眠操作也是有先有后

由于操作时间非常短,所以所有的操作看起来就是一起执行。当然不乏处理很快,有一起的情况出现

总结: 大部分而言,线程的操作方法里面,由于它执行的顺序或者说优先级有先后关系,所以它们执行的时候并不是同时休眠,也不是同时唤醒,它中间是有适当的延迟操作的。

这句话,将是继续后面同步分析的关键所在。

3. 线程中断

在之前发现线程的休眠提供有一个中断异常,实际上就证明线程的休眠是可以被打断的,而这种打断肯定是由其他线程完成的,在Thread类里面提供有这种中断执行的处理方法:

  • 判断线程是否被中断: public boolean isInterrupted()
  • 中断线程执行: public void interrupt()

范例: 观察线程的中断处理操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(()-> {
System.out.println("*** 连续战斗72个小时,我要睡觉。");
try {
Thread.sleep(10000); //预计准备休眠10秒
System.out.println("*** 睡足了,可以继续战斗了");
} catch (InterruptedException e) {
System.out.println("wo kao 才几点?");
}
}) ;
thread.start();
Thread.sleep(1000);
if(!thread.isInterrupted()) { //该线程中断了吗?
System.out.println("起床了,战斗,战斗,不要怂,起来嗨");
thread.interrupt(); //中断执行
}
}
}

所有正在执行的线程都是可以被中断的,中断线程必须进行异常的处理。

5.线程强制运行

所谓的线程的强制执行指的是当满足于某一些条件之后,某一个线程对象将可以一直独占资源,一直到该线程的程序执行结束。

范例: 观察一个没有强制执行的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
for (int x = 0; x < 100 ; x ++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行、 x = " + x);
}
},"玩耍的线程") ;
thread.start();
for (int x = 0; x < 100; x ++) {
Thread.sleep(100);
System.out.println("【霸道的main线程】 number = " + x);
}
}
}

这个时候主线程和子线程都在交替执行着,但是如果说现在你希望主线程独占执行。那么就可以利用Thread类中的方法:

  • 强制执行: public final void join() throws 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
26
27
28
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
Thread mainThread = Thread.currentThread() ; //获得主线程
Thread thread = new Thread(() -> {
for (int x = 0; x < 100 ; x ++) {
if(x == 3) { //现在霸道的线程来了
try {
mainThread.join(); //霸道的线程要先处理
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行、 x = " + x);
}
},"玩耍的线程") ;
thread.start();
for (int x = 0; x < 100; x ++) {
Thread.sleep(100);
System.out.println("【霸道的main线程】 number = " + x);
}
}
}

在进行线程强制执行的时候一定要获取强制执行的线程对象之后才可以执行Join的调用。

6. 线程礼让

线程的礼让指的是先将资源让出去让别的线程先执行。线程的礼让可以使用Thread中提供的方法:

  • 礼让: public static void yield()

范例: 使用礼让操作

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 cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
for (int x = 0; x < 100 ; x ++) {
if(x % 3 == 0) {
Thread.yield(); //线程礼让
System.out.println("### 玩耍的线程礼让执行 ###");
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行、 x = " + x);
}
},"玩耍的线程") ;
thread.start();
for (int x = 0; x < 100; x ++) {
Thread.sleep(100);
System.out.println("【霸道的main线程】 number = " + x);
}
}
}

礼让执行的时候每一次调用yield()方法都只会礼让一次当前的资源。

7. 线程优先级

从理论上来讲线程的优先级越高越有可能先执行(越有可能先抢占到资源)。在Thread类里面针对于优先级的操作提供有如下的两个处理方法:

  • 设置优先级: public final void setPriority(int newPriority)
  • 获取优先级: public final int getPriority()

在进行优先级定义的时候都是通过int型的数字来完成的,而对于此数字的选择在Thread类里面就定义有三个常量:

  • 最高优先级: MAX_PRIORITY、10
  • 中等优先级: NORM_PRIORITY、5
  • 最低优先级: MIN_PRIORITY、 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
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
Runnable run = () -> {
for(int x = 0 ; x < 10 ; x ++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行。");
}
} ;
Thread threadA = new Thread(run,"线程对象A");
Thread threadB = new Thread(run,"线程对象B");
Thread threadC = new Thread(run,"线程对象C");
threadA.setPriority(Thread.MIN_PRIORITY);
threadB.setPriority(Thread.MIN_PRIORITY);
threadC.setPriority(Thread.MAX_PRIORITY);
threadA.start();
threadB.start();
threadC.start();

}
}

主方法是一个主线程,那么主线程的优先级呢?

1
2
3
4
5
6
package cn.ngp.demo;
public class TestDemo {
public static void main(String[] args) throws Exception {
System.out.println(Thread.currentThread().getPriority());
}
}

主线程属于中等优先级,而默认创建的线程也是中等优先级。

p.s. 优先级高的只是有可能先执行,而不是绝对先执行

三. 线程的同步与死锁 笔记做到这了 ↑

再多线程的处理之中,可以利用Runnable描述多个线程操作的资源,而Thread描述每一个线程对象,于是当多个线程访问同一资源的时候如果处理不当就会产生数据的错误操作

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
package cn.ngp.demo;
class MyThread implements Runnable {
private int ticket = 10 ; //总票数为10张
@Override
public void run() {
while(true) {
if(this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票 = " + this.ticket -- );
}else {
System.out.println("***** 票已经卖光了 *****");
break;
}
}
}
}

public class TestDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "票贩子A").start();
new Thread(mt, "票贩子B").start();
new Thread(mt, "票贩子C").start();
}
}

此时的程序将穿件三个线程对象,并且这三个线程对象将进行10张票的出售。此时的程序在进行卖票处理的时候并没有任何的问题(假象),下面可以,模拟一下买票中的延迟操作。

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 cn.ngp.demo;
class MyThread implements Runnable {
private int ticket = 10 ; //总票数为10张
@Override
public void run() {
while(true) {
if(this.ticket > 0) {
try {
Thread.sleep(100); //模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票 = " + this.ticket -- );
}else {
System.out.println("***** 票已经卖光了 *****");
break;
}
}
}
}

public class TestDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "票贩子A").start();
new Thread(mt, "票贩子B").start();
new Thread(mt, "票贩子C").start();
}
}

这个时候追加了延迟,问题就暴露出来了,而实际上这个问题一直都在。

执行之后出现负数,这就叫不同步
2

2. 同步处理

2.1 同步代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package cn.ngp.demo;
class MyThread implements Runnable {
private int ticket = 5 ;
@Override
public void run() {
synchronized (this) { //同步代码块
for(int x = 0 ; x < 20 ; x ++) {
if(this.ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票 = " + this.ticket -- );
}
}
}
}
}

public class TestDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "票贩子A").start();
new Thread(mt, "票贩子B").start();
new Thread(mt, "票贩子C").start();
new Thread(mt, "票贩子D").start();
}
}
2.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
package cn.ngp.demo;
class MyThread implements Runnable {
private int ticket = 20 ;
@Override
public void run() {
for(int x = 0 ; x < 30 ; x ++) {
this.sale();
}
}
public synchronized void sale() { //同步方法
if(this.ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票 = " + this.ticket -- );
}
}
}

public class TestDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "票贩子A").start();
new Thread(mt, "票贩子B").start();
new Thread(mt, "票贩子C").start();
new Thread(mt, "票贩子D").start();
}
}

总结:

  • 同步操作与异步操作相比,异步操作的执行速度要高于同步操作,但是同步操作时数据的安全性较高,属于安全的线程操作。
  • synchronized(同步的)在Java中很常用。

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
package cn.ngp.demo;
class A{
public synchronized void say(B b) {
System.out.println("A说: 把你的本给我,我给你笔,否则不给!");
b.get();
}
public synchronized void get() {
System.out.println("A说: 得到了本,付出了笔,还是什么都干不了");
}
}
class B{
public synchronized void say(A a) {
System.out.println("B说: 把你的笔给我,我给你本,否则不给!");
a.get();
}
public synchronized void get() {
System.out.println("B说: 得到了笔,付出了本,还是什么都干不了");
}
}
public class TestDemo implements Runnable{ //主类
public static A a = new A();
public static B b = new B();
public static void main(String[] args) throws Exception {
new TestDemo();
}
public TestDemo() throws InterruptedException {
new Thread(this).start();
b.say(a);
}
@Override
public void run() {
a.say(b);
}
}

题: 请解释多个线程访问同一资源时需要考虑到需要考虑到哪些情况?有可能带来哪些问题?

  • 多个编程访问同一资源时一定要处理好同步,可以使用同步代码块或同步方法来解决。

    • 同步代码块: synchronized(锁定对象) {代码}
    • 同步方法: public synchronized 返回值 方法名称() {代码}
  • 但是过多的同步使用,有可能造成死锁

总结:

  • 最简单的理解同步和异步的操作那么就可以通过 synchronized 来实现;
  • 死锁是一种不定的状态.

四. 综合实战: 生产者与消费者

1. 问题引出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
package cn.ngp.demo;
class Info{
private String title;
private String content;
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
class Productor implements Runnable{
private Info info;
public Productor(Info info) {
this.info = info;
}
@Override
public void run() {
for(int x = 0 ; x < 100 ; x ++) {
if (x % 2 == 0) { //偶数
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.info.setTitle("我我我");
this.info.setContent("你你你");
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.info.setTitle("一一一一");
this.info.setContent("二二二二");
}
}
System.out.println(this.info.getTitle());
}
}
class Customer implements Runnable{
private Info info;
public Customer(Info info) {
this.info = info;
}
@Override
public void run() {
for(int x = 0 ; x < 100 ; x ++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.info.getTitle()+"-"+this.info.getContent());
}
}
}
public class TestDemo { //主类
public static void main(String[] args) throws Exception {
Info info = new Info();
new Thread(new Productor(info)).start();
new Thread(new Customer(info)).start();
}
}

现在实际上通过以上的代码可以发现两个严重的问题:

  • 数据错位, 发现不再是一个所需要的完整数据
  • 数据重复取,数据重复设置

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
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
package cn.ngp.demo;
class Info{
private String title;
private String content;
public synchronized void set(String title,String content) {
this.title = title;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.content = content;
}
public synchronized void get() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.title + "-" + this.content);
}

}
class Productor implements Runnable{
private Info info;
public Productor(Info info) {
this.info = info;
}
@Override
public void run() {
for(int x = 0 ; x < 100 ; x ++) {
if (x % 2 == 0) { //偶数
this.info.set("我我我","你你你");
}else {
this.info.set("一一一一","二二二二");
}
}
}
}
class Customer implements Runnable{
private Info info;
public Customer(Info info) {
this.info = info;
}
@Override
public void run() {
for(int x = 0 ; x < 100 ; x ++) {
this.info.get();
}
}
}
public class TestDemo { //主类
public static void main(String[] args) throws Exception {
Info info = new Info();
new Thread(new Productor(info)).start();
new Thread(new Customer(info)).start();
}
}

此时数据错位的问题得到了很好的解决,但是重复操作问题更加严重了。

3. 等待与唤醒机制(利用Object类解决重复操作)

要想解决重复操作问题,必须加入等待与唤醒机制,在Object类里面提供有专门的处理方法。

  • 等待: public final void wait() throws InterrupedException;
  • 唤醒第一个等待线程: public final void notify();
  • 唤醒全部等待线程,那个优先级高就先执行: public final void 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
package cn.ngp.demo;
class Info{
private String title;
private String content;
private boolean flag =true;
// flag = true : 表示可以生产,但是不可以取走
// flag = false: 表示可以取走,但是不可以生产
public synchronized void set(String title,String content) {
// 重复进入到了set()方法里面,发现不能够生产,所以要等待
if(this.flag == false) {
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.title = title;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.content = content;
this.flag = false ; //修改生产标记
super.notify(); //唤醒其他等待线程
}
public synchronized void get() {
if(this.flag == true) { //还没生产呢
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.title + "-" + this.content);
this.flag = true;
super.notify();
}

}
class Productor implements Runnable{
private Info info;
public Productor(Info info) {
this.info = info;
}
@Override
public void run() {
for(int x = 0 ; x < 100 ; x ++) {
if (x % 2 == 0) { //偶数
this.info.set("我我我","你你你");
}else {
this.info.set("一一一一","二二二二");
}
}
}
}
class Customer implements Runnable{
private Info info;
public Customer(Info info) {
this.info = info;
}
@Override
public void run() {
for(int x = 0 ; x < 100 ; x ++) {
this.info.get();
}
}
}
public class TestDemo { //主类
public static void main(String[] args) throws Exception {
Info info = new Info();
new Thread(new Productor(info)).start();
new Thread(new Customer(info)).start();
}
}

问题解决了,但是运行速度慢。

题: 请解释sleep()和wait()的区别?

  • sleep()是Thread类定义的方法,wait()是Object类定义的方法;
  • sleep()可以设置休眠时间,时间一到自动唤醒,而wait()需要等待notify()进行唤醒。

总结:

  • 这是一个非常经典的多线程处理模型。所以掌握它属于你个人能力的一个提升,同时可以更加理解Object类的作用。

参考

来自: 阿里云大学(笔记) → 零基础学Java10系列三:Java高级编程