本人学习多线程时做的一些笔记,以备以后回顾复习。

第一章、多线程

1.1 并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。

此时有两个任务,一个任务1,一个任务2,我要在某个时间内让它完成两个任务,一个任务1,一个任务2,我们用并发的方式完成,我们执行任务需要cpu来执行它,比如此时我的电脑是单核心的,它会先执行任务1再执行任务2,然后执行任务1比如(QQ,微信,浏览器,交替执行,但是交替速度很快)。这个指的就是并发即一段时间内,cpu在多个任务之间交替执行

  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

我们现在电脑的cpu都是多核心多线程的了对吧。这个cpu可以执行任务1,还能再来一个cpu让它执行任务2.所以这个并行它叫同时执行

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

1.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
/**
* 程序:一堆命令的集合,进程静止的状态,包括了可执行文件、配置文件、
* 数据文件等等软件文件集合,即计算机要执行的任务。
*
*
* 进程: 即具有一定功能的程序 关于某个数据集合上的一次运行活动,
* 是cpu进行资源分配和调用的 一个独立单元。
*
* 线程: 有时候可以看成轻量级的进程,是程序执行流的最小单元,
* 一个进程可以看做是有n个线程组成的
*
* 在单个程序运行的过程中,可以同时执行多个线程完成不同的工作,
* 称之为多线程
*
* 以腾讯管家为例,打开腾讯管家,就会进入到内存中,就是一个进程
* 它里面有很多功能,例如病毒查杀,清理垃圾,电脑加速,他们都是一个线程
* 此时点击病毒查杀,清理垃圾,电脑加速让它执行,它就会开启一条应用程序到cpu的执行路径
* cpu就可以通过路径就可以执行功能,这个路径有个名字叫线程
* 腾讯电脑管家实际上是个多线程的路径
*
* 注意:线程是属于进程的,是进程中的一个执行单元,负责程序的执行
*
* 并发:
* 官方解释 :并发,在操作系统中,是指一个时间段中有几个程序都处
* 于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,
* 但任一个时刻点上只有一个程序在处理机上运行。
* 理解:
* 线程总是并发运行的,操作系统会将时间分成若干个片段(时间片)
* 尽可能的将这些时间片分配均匀的分配给每一个线程,
* 当线程得到时间片以后,就可能被cpu所执行,
* 随着cpu高速的运行,从宏观的角度来看, 所有任务都在同时被执行,
* 但是从微观的角度看,每个任务都是走走停停的,此种现象被称为并发。
*
*
* 并行: 多个任务是被多个cpu真的同时运行。
*/

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 设置线程的优先级

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们工作一边使用编辑器,同时还开着微信QQ,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

    实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
    其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

1.3 创建线程类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的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
/*
创建多线程程序的第一种方式:创建Thread类的子类
java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类

实现步骤:
1.创建一个Thread类的子类
2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
3.创建Thread类的子类对象
4.调用Thread类中的方法start方法,开启新的线程,执行run方法
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
java程序属于抢占式调度,那个线程的优先级高,那个线程优先执行;同一个优先级,随机选择一个执行
*/
public class Thread01 {
public static void main(String[] args) {
// 3.创建Thread类的子类对象
MyThread mt = new MyThread();
// 4.调用Thread类中的方法start方法,开启新的线程,执行run方法
/*
* 注: 启动一个线程,是通过调用其start()来实现的。
*/
mt.start();

for (int i = 0; i < 20; i++) {
System.out.println("main:" + i);
}
}

}

自定义线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}

运行结果:

第二章、多线程执行原理

JVM执行main方法,找os开辟一条main方法通向CPU的路径,这个路径叫做main线程(主线程)

cpu通过这个线程,这个路径可以执行main方法

当我们在这里new MyThread(); 等于它重新开辟了一个指向CPU的路径

这条路径是执行run方法的

现在对于CPU而言,我是不是有两条执行路径了,cpu就有了选择的权限。

注意:我们控制不了CPU,CPU喜欢谁它就会执行哪条路径

所以就有了程序的随机打印结果

即两个线程,一个main线程,一个thread线程一起抢夺cpu的执行权(cpu的执行时间)

谁抢到了谁执行对应的代码。

2.1、多线程原理

有一个main方法,还有一个run方法。run方法里面有个for循环,

那我们来看看它的内存图解

首先它有一个栈内存

程序怎么执行的呢,首先执行程序的入口也就是main方法。它会压栈执行,从栈内存进来跑到栈内存最下面来

然后一行行执行代码,首先创建对象,对象创建在那里?是不是就是在堆内存中,这里new的对象是在堆内存中的, 它有个内存空间,这个对象它有自己的地址值,然后会把地址赋值给变量,

我们今天重点研究的不是堆内存而是栈内存,这里我要执行一行代码即(mt.run)它这里就是调用这个run方法

它也会压栈执行,但是这么执行,程序就是单线程的程序了。他会先执行完run方法,在执行主方法里的其他代码。

那什么是多线程程序呢

就是我们在这里调用了mt.start方法。调用mt.start方法,它就不一样了。它会开辟一个新的栈空间。它调用的是mt.start方法,执行run方法

此时对于cpu而言,我可以选择执行哪个栈空间里的代码,即cpu有了选择的权利,可以执行main方法也可以执行run方法。这个就是多线程的内存空间

2.2、多线程的好处

多个线程之间互不影响(在不同的栈空间中)

第三章、Thread类

3.1、thread类的常见方法

3.1.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
/**
* 线程常见的方法
*
* 1、获取线程名称:
* 1.1、使用Thread类中的方法getName() String getName() 返回该线程的名称
*
* 1.2、可以先获取当前正在执行的线程,使用线程中的方法getName()获取线程名
*
* static Thread currentThread()返回对当前正在执行的线程对象引用
* 获取当前的Id getId()
* 获取当前线程的优先级 getPriority()
* 获取当前线程的状态 getState()
* 判断当前线程是否还处于活动状态 isAlive()
* 判断线程是否为守护线程 isDaemon()
*/

//定义一个Thread的子类
class MyThreads extends Thread {
// 重写Thread类中的run方法,设置线程任务
@Override
public void run() {
// 获取线程名
String name2 = getName();
System.out.println(name2);

//获取当前执行线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread);

//链式编程
System.out.println(Thread.currentThread().getName());
}
}

public class Thread02 {
public static void main(String[] args) {
MyThreads mt = new MyThreads();
//调用start方法,开启新线程,执行run方法
mt.run();
new MyThreads().start();
//链式编程
System.out.println(Thread.currentThread().getName());
}
}

3.1.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
/*
设置线程名称:
1、使用Thread类中方法setName(名字)
void setName(String name)
改变线程名称,使之与参数name相同

2、创建带参数的构造方法,参数传递线程的名称
调用父类的带参构造方法,把线程名称传递给父类
让父类Thread()给子线程起一个名字
Thread(String name) 分配新的Thread对象
*/
public class Thread03 {
public static void main(String[] args) {
// 开启多线程
Mythread1 th1 = new Mythread1();
th1.setName("线程1");
th1.start();

//开启多线程
Mythread1 th2 = new Mythread1("线程2");
th2.start();
}
}

class Mythread1 extends Thread {
public Mythread1() {

}

public Mythread1(String name) {
//把线程名给父类,让父类Thread给子线程起个名字
super(name);
}

@Override
public void run() {
// 获取线程名
System.out.println(Thread.currentThread().getName());

}
}

3.1.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
/**
* 守护线程: 守护线程也叫做后台线程,正常创建出来的线程又叫做前台线程 或者 主线程
*
* 主线程可以通过setDemeon()设置成后台线程(守护线程)
*
* 当一个程序中,所有的主线程都结束后,此时如果程序中还有守护线程在执行任务 则不管守护线程是否执行完任务,整个程序结束。
*
* 当一个程序中,所有的守护线程任务结束,当此时如果程序中还有主线程在执行任务 则整个程序继续执行
*
*/
public class Demo04 {
public static void main(String[] args) {
Thread th1 = new Thread(){
public void run(){
for (int i = 0; i < 1000; i++) {
System.out.println("主线程在执行任务 " + i);
try {
//线程睡眠
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
Thread th2 = new Thread(){
public void run(){
for (int i = 0; i < 1000; i++) {
System.out.println("守护线程在执行任务 " + i);

try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//将线程2设置成守护线程
th2.setDaemon(true);
th1.start();
th2.start();
}
}

3.1.4、线程睡眠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* sleep(int value) 让当前线程陷入沉睡,沉睡时间为指定的时间,单位是毫秒
* sleep为静态方法
*/
public class Demo05 {
public static void main(String[] args) {
Thread th = new Thread() {
public void run() {
for (int i = 5; i >= 0; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
th.start();
}
}

3.1.5、设置线程优先级

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
/**
* 线程的优先级:
*
* 我们不能强制干涉cpu将时间片分配给某个线程
*
* 但是我们可以控制线程的优先级,从而提高该线程获取时间片的概率
*
* 数据越大,优先级越高。
*
* 优先级:1-10级,数字越大,优先级越高
*
* 优先级默认等级为5
*/
public class Demo03 {
public static void main(String[] args) {
Thread th1 = new Thread("th1") {
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("th1正在执行");
}
}
};
Thread th2 = new Thread("th2") {
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("th2正在执行");
}
}
};
/**
* setPriority(int value)
*
* 设置当前线程的优先级,需要在线程启动之前调用,之后调用就没有意义
*/
th1.setPriority(2);
th2.setPriority(8);

th1.start();
th2.start();
}
}

3.1.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
/**
* yield : 线程让步,当前线程让出自己的cpu资源让其他线程优先执行
*
* 注: 1、让出的时间不确定;
* 2、在让出cpu资源时,会先检查是否有相同优先级且处于可运行状态的线程,如果有则让出,否则不让
*/
public class Demo07 {
public static void main(String[] args) {
Thread th1 = new Thread() {

public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("老太太排队买票 " + i);
try {
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
Thread th2 = new Thread() {

public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("小朋友在排队买票 " + i);
try {
if (i == 4) {
System.out.println("开始让出cpu");
/**
* 让出cpu资源,让出的时间不确定,若要把cpu全让出,可用线程插队
*/
Thread.yield();

}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
th1.start();
th2.start();
}
}

3.1.7、线程插队

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
/**
* jion() 该方法表示线程插队,当当前线程插队时,其他线程陷入阻塞状态,直到当前线程插队将任务执行完毕
*
*/
public class Demo06 {
public static void main(String[] args) {
Thread th1 = new Thread() {

public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("老太太排队买票 " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
Thread th2 = new Thread() {

public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("小朋友在排队买票 " + i);
try {
Thread.sleep(1000);

if (i == 3) {
System.out.println("老太太又事情,先插个队");
/**
* 让th1线程插队
*/
th1.join();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
th1.start();
th2.start();
}
}

第四章、实现多线程的第二种方法

4.1、实现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
42
43
44
45
46
47
48
49
50
51
/**
* 在java中使用多线程方式2(推荐):
* 通过实现Runnable接口, 重写run() ,在其中定义要执行的任务。
* 然后将该任务子类对象添加到线程中执行。
*
* 好处在于将线程 和 线程要执行的任务分开,避免的线程和任务的强耦合,
* 当前这个线程在完成某个任务以后,可以再去执行别的任务。
*
* 构造方法:
* Thread(Runnable target)分配新的Thread对象
* Thread(Runnable target,String name)分配新的name对象
*
* 实现步骤:
* 1、创建一个Runnable接口的实现类
* 2、在实现类中重写Runnable接口中的run方法
* 3、创建一个Runnable接口实现类对象
* 4、创建Thread类对象,构造方法中传递Runnable接口的实现类对象
* 5、调用Thread类中的start方法开启新的线程,执行run方法
*/
public class Thread06 {
public static void main(String[] args) {
//Runnable 可以看作任务类接口
Runnable1 run1 = new Runnable1();
Runnable2 run2 = new Runnable2();

// 将任务对象 添加线程中执行
Thread th1 = new Thread(run1);
Thread th2 = new Thread(run2);

th1.start();
th2.start();
}
}

class Runnable1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("runnable1接口实现多线程");
}
}
}

class Runnable2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("runnable2接口实现多线程");
}
}
}

第五章、Thread和Runnable接口的区别

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

总结:

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

  1. 适合多个相同的程序代码的线程去共享一个资源

  2. 可以避免Java中的单继承的局限性

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立

  4. 线程池只能放入实现Runnable或者Callable类线程。不能直接放入继承Thread的类

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

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
/**
实现Runnable接口创建多线程程序的好处

1、避免了单继承的局限性
一个类只能继承一个类(一个人只能有一个亲爹)
类继承了Thread类就不能继承其它类
实现了Runnable接口,还可以继承其它类,实现其它接口
2、增强程序的扩展性,降低了程序的耦合性(解耦)
实现Runnable接口的方式,把设置线程任务和开启新线程进行分离(解耦)
实现类中,重写了run方法,用来设置线程任务
创建Thread类对象,调用start方法,用来开启新线程

*/
public class Thread08 {
public static void main(String[] args) {
Runnable3 run3 = new Runnable3();
Runnable4 run4 = new Runnable4();

Thread th1 = new Thread(run3);
Thread th2 = new Thread(run4);

th1.start();
th2.start();
}
}

class Runnable3 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("Hello,Runnable" + (i + 1));
}
}
}

class Runnable4 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("HelloWorld" + (i + 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
/**
* 匿名内部类方式
*
* 匿名:没有名字 内部类:写在其他类内部的类
*
* 匿名内部类的作用:简化代码 把子类继承父类,重写父类的方法创建子类对象可以一步完成
* 把实现实现类接口,重写接口中的方法,创建实现类对象一步完成
*
* 匿名内部类的最终产物:子类/实现类对象,而这个类它没有名字
*
* 格式:
* new 父类/接口(){
* 重写父类/接口中的方法
* };
*
*/
public class Thread09 {
public static void main(String[] args) {
// 线程的父类是Thread
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " " + (i + 1));
}
}
};
// thread.start();

// 实现线程接口Runnable
Runnable r = new Runnable() {

@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "Runnable" + (i + 1));

}
}
};
Thread th2 = new Thread(r) {
};
th2.start();
}
}

第七章、线程安全问题

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个 (本场电影只能卖100张票)。

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票) 需要窗口,采用线程对象来模拟;需要票,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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
线程安全问题:

案例:此时有个电影院开始售票,它一共贩卖100张

现在只能让它现场买票,这个就相当于单线程程序(不会出现线程安全问题)

那此时,一个窗口卖票速度太慢了,变成了三个窗口

1号窗口贩卖(1-33) 2号窗口贩卖(34-67) 3号窗口贩卖(68-100)

三个窗口一起卖票,但是卖的票不同,也不会出问题(多线程程序没有访问共享数据)

现在假如有这么一个情景,还是开设了三个窗口,但是每个窗口都是从1号票开始卖

这就会出现问题了,是不是就会出现混乱的情况了啊!是不是就会出现不存在的票啊!

多线程访问了共享的数据会产生安全问题,这个问题是可以解决的
*/
/**
* 线程并发带来的安全问题 : 当多个线程操作同一个资源的时候就会引发安全问题
* 当多个线程访问同一份临界资源时,由于cpu切换线程的不确定性,会引发多个线程抢资源的现象,导致逻辑上的伦乱,引发数据安全问题。
*
* Java中一般引发安全问题的前提:
* 1、多个线程同时访问同一个对象
* 2、操作同一个全局变量
* 解决办法 : 通过添加synchronized锁
*/
public class Demo01 {
public static void main(String[] args) {
//模拟卖票案例
Runnable1 run1 = new Runnable1();

Thread th1 = new Thread(run1);
Thread th2 = new Thread(run1);
Thread th3 = new Thread(run1);

th1.start();
th2.start();
th3.start();
}
}

class Runnable1 implements Runnable {
// 定义一个多线程共享的票源
private int ticket = 100;

// 设置线程任务,卖票
@Override
public void run() {
// 使用死循环让卖票重复执行
while (true) {
// 先判断票是否存在
if (ticket > 0) {
System.out.println("还有余票" + Thread.currentThread().getName() + "正在售卖第" + ticket + "张票");
ticket = ticket - 1;
}
}
}
}

发现程序出现了两个问题:

  1. 相同的票数,比如100这张票被卖了两回。

  2. 不存在的票,比如0票与-1票,是不存在的。

这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写

操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,

否则的话就可能影响线程安全。

7.1 线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。

1
2
3
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。
也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,
完成对应的操作,保证了数据的同步性,解决了线程不安全的现象

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。

  2. 同步方法。

  3. 锁机制。

7.2、同步代码块

  • 同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式

1
2
3
synchronized(同步锁){
需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。

  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着

(BLOCKED)。

使用同步代码块解决代码

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
/**
* 注意:
* 线程安全问题不能产生,我们可以让一个线程在访问共享数据
* 的时候,无论是否丢失了cpu的执行权,让其他的线程只能等待
* 等待当前线程卖完票,其它线程才能进行卖票。
*
* 保证:始终只有一个线程在卖票
*
* 同步代码块:
* synchronized(同步锁){
* 可能会出现线程安全的代码
* }
*
* 注意:
* 1、同步代码块的锁对象,可以是任意对象
* 2、但是必须保证多个线程使用的锁对象是同一个
* 3、锁对象作用:
* 把同步代码块锁住,只能一个线程在同步代码块中执行
*/
public class Demo02 {
public static void main(String[] args) {
Runnable2 run1 = new Runnable2();


Thread th1 =new Thread(run1);
Thread th2 =new Thread(run1);
Thread th3 =new Thread(run1);

th1.start();
th2.start();
th3.start();
}
}

class Runnable2 implements Runnable {

private int ticket = 100;

// 创建一个锁对象
Object obj = new Object();

@Override
public void run() {
while (true) {

// 创建同步代码块
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖票ticket
System.out.println(Thread.currentThread().getName() + "正在出售第 " + ticket + "张票");

ticket--;
}
}
}
}
}

7.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
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 同步技术原理:
* 使用了一个锁对象,这个锁对象叫做同步锁,也叫对象锁
* 也叫对象监视器,
*
* 流程:
* 两个线程一起抢夺cpu执行权,谁抢到了谁执行run方法进行卖票
*
* t0抢到了cpu的资源执行run方法,遇到synchronized同步代码块
* 此时t0检查同步代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行
*
* t1抢到了cpu的执行权,执行run方法,遇到synchronized同步代码块
* 此时t1检查同步代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行
*
* t1发现没有锁对象,就会进入到阻塞状态,会一直等待。t0线程归还锁对象
* 一直到t0线程执行完同步中的代码,就会把锁对象归还给同步代码块
* t1才能获取到锁对象,进入到同步中执行
*
* 总结:
* 同步中的线程,没有执行完,不会释放锁,同步外的线程没有锁进不去
*
* 注意:
* 同步保证了只能有一个线程在同步中执行共享数据,保证了安全
* 程序频繁的判断锁,获取锁,释放锁,程序的效率会变低
*/
public class Demo03 {
public static void main(String[] args) {
Runnable3 run3 = new Runnable3();

Thread th1 = new Thread(run3) ;
Thread th2 = new Thread(run3);
th1.start();
th2.start();

}
}

class Account{
private String name;
private double money;

public Account(String name, double money) {
super();
this.name = name;
this.money = money;
}

public void takeMoney(double money) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (this) {
this.money -= money;
System.out.println(Thread.currentThread().getName()
+ "取款后剩余" + this.money);
}
}
}
class Runnable3 implements Runnable{
Account account = new Account("llw", 10000);
@Override
public void run() {
account.takeMoney(1000);
}
}

7.4、同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

1
2
3
public synchronized void method(){
可能会产生线程安全问题的代码
}

同步锁是谁?

对于非static方法,同步锁就是this。

对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

使用同步方法代码如下:

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
/*
在方法上加个synchronized关键字

案例:
卖票案例出现线程安全问题
卖出了重复的票和不存在的票

解决线程问题的方案:使用同步方法来解决

使用步骤
1、把访问共享数据的代码抽取出来,方法方法中
2、在方法上添加修饰符synchronized

格式:
定义方法的格式

修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码
}
*/
/**
* synchronized锁 实际上锁的不是某段代码,某个方法,而是锁的是此方法,或者同步块所指定的对象
* 当某个线程去访问这些加了synchronized锁的代码时,对应的对象就被上锁。
*
* 同步锁的互斥:当我们使用线程访问了某个对象中的带synchronized的方法(代码段)
* 那么该对象中的其他synchronized方法(代码段)
* 也都不可以被其他线程同时访问,但是非synchronized方法不受限制
*/
public class Demo04 {
public static void main(String[] args) {
Runnable4 run4 = new Runnable4();

Thread th1 = new Thread(run4);
Thread th2 = new Thread(run4);
Thread th3 = new Thread(run4);

th1.start();
th2.start();
th3.start();

}
}

class Runnable4 implements Runnable{
private int ticket = 100;

//卖票
@Override
public void run() {
while(true) {
payTicket();
}
}
/*
定义一个同步方法
同步方法也会把方法内部的代码锁住
只让一个线程执行
同步方法锁的对象是谁?
就是实现类对象,即new Runnable4() 也就是this
即谁调用这个方法,对象就是谁
*/
public synchronized void payTicket() {
if(ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在ticket--
System.out.println(Thread.currentThread().getName()
+"正在贩卖" + ticket +"张票");
ticket--;
}
}
}

7.5、静态锁

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
/**
* 静态锁: 在静态方法上加锁, 此时锁的不是方法所在的对象(因为静态方法不是某个对象的,而是被该类的全体对象所共有的),
* 此时实际上锁的是当前类对应的Class(存放类信息的类,jvm加载的每一个类,都有一个对应的Class实例)对象。
* 所以此时,调用上锁的静态方法时,是否同步执行,和是哪个对象无关。
*
* 注意:静态的同步方法,锁对象是谁? 不是this,this是创建对象之后产生的
* 静态方法优先于对象
* 静态方法的锁对象是本来的class属性 --> class文件对象(反射)
*/
public class Demo05 {
public static void main(String[] args) {
Runnalbe5 run5 = new Runnalbe5();

Thread th1 = new Thread(run5);
Thread th2 = new Thread(run5);
Thread th3 = new Thread(run5);

th1.start();
th2.start();
th3.start();
}
}
class Runnalbe5 implements Runnable {
private static int ticket = 100;

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

public static synchronized void payStaticTicket() {


/**
* 这样也可以保证锁对象是唯一的
*/
/*
* synchronized (Runnalbe5.class) {
*
* }
*/
if(ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在ticket--
System.out.println(Thread.currentThread().getName()
+"正在贩卖" + ticket +"张票");
ticket--;
}
}
}

7.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
/*
* 线程脏读问题:
* 此时可能就会出现脏读:即一个线程进行写数据操作的时候,执行到一半时间片耗尽,
* 此时有另一个线程去执行读数据的操作,但是读操作是没有加锁的可以被并发访问
* 此时可能读到的是另一个线程处理了一般的数据,此时可能读到的是另一个线程处理了一半的数据
*/
public class Demo06 {
public static void main(String[] args) {
Accounts accounts = new Accounts("llw",1000);

Thread th1 = new Thread("th1") {
@Override
public void run() {
accounts.setMoney(500);
System.out.println("th1 :" + accounts.getMoney());
}
};

Thread th2 = new Thread("th2") {
@Override
public void run() {
System.out.println("th2 :" + accounts.getMoney());
}
};

th1.start();
th2.start();
}
}
class Accounts{
String userName;
double money;

public Accounts(String userName,double money) {
super();
this.userName = userName;
this.money = money;
}

public /*synchronized*/void setMoney(double money) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.money -= money;
}

public/*synchronized*/ double getMoney() {
return money;
}
}

7.7、内存的可见性问题 和 volatile关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
/**
内存的可见性问题 和 volatile关键字

在下述案例中我们期望通过线程2修改status变量的值,去停止线程1 但是实际上并没有成功

那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

为什么发生这样的结果:和jvm的内存模型相关,在jvm中全局共享变量存放在主存中(堆中) 而每个线程都有属于自己的私有的本地内存,当程序运行起来以后,
CPU为了避免频繁的去主存中读取数据,而导致性能降低,所以会先将主存中的数据复制一份到线程的本地内存中,
然后所有对数据的操作都是对本地内存中的副本进行操作,操作完毕后,会在合适的时机将数据刷新到主存中去 此时如果多个线程去访问同一份数据都是先拷贝一份到本地中
而某个线程刷新了主存中的数据以后,其他的线程并不一定能感知到主存中的数据已经发生了改变 此时就导致了线程之间的可见性问题。

解决的办法:
1、对数据操作时使用线程锁 synchronized 或者 Lock
2、在对操作的变量前加volatile关键字

volatile关键字: 是Java提供的一种轻量级的同步机制(并不能解决线程安全问题!!!!)

当一个变量被volatile关键字修饰以后,某个线程对此数据修改,刷新到主存中后,jvm会通知其他线程次数据已经发生改变

此时数据已经发生了变化,其他线程就会重新载入这个数据

线程的可见性:指的是某条线程修改了多个线程的共享的全局变量数据,新的值对其他线程 来说也是可以立刻得知的

*/
public class Demo07 {
// boolean status = true;
volatile boolean status = true;

public static void main(String[] args) {
Demo07 d7 = new Demo07();

Thread th1 = new Thread("th1") {
@Override
public void run() {
// TODO Auto-generated method stub
String thName = Thread.currentThread().getName();
System.out.println(thName + "开始执行..");

while (d7.status) {

}
System.out.println(thName + "执行完毕");
}
};

Thread th2 = new Thread("th2") {
@Override
public void run() {
String thName = Thread.currentThread().getName();
System.out.println(thName + "开始执行");
d7.status = false;
}
};

th1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
th2.start();
System.out.println(d7.status);
}
}
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
/**
* volatile 不能替代synchronized锁机制,它还是会引发线程安全问题
*
*/
public class Demo08 {
public static volatile int num = 0;

public static void main(String[] args) {
// 1、测试volatile会引发安全问题
// Thread th1 = new Thread() {
// @Override
// public void run() {
// for (int i = 0; i < 10000; i++) {
// numadd();
// }
// }
// };
// Thread th2 = new Thread() {
// @Override
// public void run() {
// for (int i = 0; i < 10000; i++) {
// numadd();
// }
// }
// };
// th1.start();
// th2.start();
//
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(num);


//2、测试 synchronized不会引发安全问题
Thread th1 = new Thread("th1") {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
numadd();
}
}
};
Thread th2 = new Thread("th2") {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
numadd();
}
}
};
th1.start();
th2.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(num);
}


// public static void numadd() { num++; }

public static synchronized void numadd() {
num++;
}
}

7.8、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
/**
* synchronized锁的重入(重新进入)机制:
* 当一个线程获得一个对象的锁以后,在该锁中执行代码的时候,可以再次获得该对象的锁。
* 例子:比如下面的method1()方法被线程访问了以后,对应的对象被锁定了,其他的带synchronized的方法也不可以被访问,
* 但是在method1()里面又调用了method2()方法。按理说此时程序是不可以继续向下执行的,变成死锁
* 但是实际上它是可以继续执行的,因为锁的重入机制,虽然调用method1()的时候,获取了对应的对象的锁,此时在这个锁的内部执行代码
* 可以再次获取同一个对象的锁,即继续执行method2()方法
*/
public class Demo01 {
public synchronized void method1() {
method2();
System.out.println("method1()....");
}

public synchronized void method2() {
System.out.println("method2()...");
}

public static void main(String[] args) {
Demo01 demo01 = new Demo01();
Thread th = new Thread("th1") {
@Override
public void run() {
demo01.method1();
}
};
th.start();
}
}

7.9、死锁

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
/**
* 死锁: 多个线程在竞争资源的时候,过多的同步方法导致线程推进不当, 两个或者以上线程持有对方所需要的锁,但是又不可以及时释放,
* 导致线程之间都在等待对方释放锁,最终引发死锁问题。
*
* 例子: 有线程th1和th2,以及有两个被锁对象obj1和obj2 线程1执行的时候,需要先锁obj1在去锁obj2
* 与此同时,线程th2也在并发执行,需要先锁obj2,在去锁obj1
*
* 此时,可能就会出现线程th1拿到obj1锁的时候,发现obj2锁已经被th2拿了
* 那么此时th1就只能等待th2释放锁,但是th2释放obj2的锁,需要先拿到obj1
*/
public class Demo02 {
Object obj1 = new Object();
Object obj2 = new Object();

public void method1() {
synchronized (obj1) {
System.out.println("锁完boj1以后,去锁obj2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 想要2
synchronized (obj2) {
System.out.println("锁定obj2");
}
}
}

public void method2() {
synchronized (obj2) {
System.out.println("锁完boj2以后,去锁obj1");

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

// 想要1
synchronized (obj1) {
System.out.println("锁定obj1");
}
}
}
public static void main(String[] args) {
Demo02 demo02 = new Demo02();
Thread th1 = new Thread("th1") {
@Override
public void run() {
demo02.method2();
}
};
Thread th2 = new Thread("th2") {
@Override
public void run() {
demo02.method1();
}
};
th1.start();
th2.start();
}
}

7.10、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
/**
* Lock锁: Lock实现了比synchronized锁更广泛的锁操作
* 接口中提供了两个方法
* 1、Lock:获取锁
* 2、unlock:释放锁
* 他能够知道什么时候获取锁,什么时候释放锁
*
* java.util.conurrent.locks.ReentrantLock implement Lock
* 使用步骤(既然是接口,就要实现它的实现类)
*
* 1、在成员位置上创建一个ReentrantLock对象
* 2、可能会出现安全问题的代码前,调用Lock方法获取锁
* 3、可能会出现安全问题的代码后,调用unLock释放锁
*/
public class Demo10 {
public static void main(String[] args) {

Rannable10 run10 = new Rannable10();

Thread th1 = new Thread(run10);
Thread th2 = new Thread(run10);
Thread th3 = new Thread(run10);

th1.start();
th2.start();
th3.start();
}
}

class Rannable10 implements Runnable{
private static int ticket = 100;
// 1、在成员变量前创建Lock锁
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
// 2、可能会出现安全问题的代码前,调用Lock方法获取锁
lock.lock();
payticket();
}
}
public void payticket() {
if(ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket +"张票");
ticket--;
}
// 3、可能会出现安全问题的代码后,调用unLock释放锁
lock.unlock();
}
}

其实它还有一种更好的写法,那我们来一起看一下怎个要怎么写吧

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
/**
* Lock锁: Lock实现了比synchronized锁更广泛的锁操作
* 接口中提供了两个方法
* 1、Lock:获取锁
* 2、unlock:释放锁
* 他能够知道什么时候获取锁,什么时候释放锁
*
* java.util.conurrent.locks.ReentrantLock implement Lock 使用步骤(既然是接口,就要实现它的实现类)
*
* 1、在成员位置上创建一个ReentrantLock对象
* 2、可能会出现安全问题的代码前,调用Lock方法获取锁
* 3、可能会出现安全问题的代码后,调用unLock释放锁
*/
/**
* Lock(java提供的lock对象来实现synchronized相同的效果);
* Lock是一个接口类型,常见的子实现类为Reentrantlock
* 使用的时候先创建lock对象,通过lock方法加锁,unlock()方法手动释放锁
* 在他们之间的代码就都被上锁了,变成同步执行的代码
*/
public class Demo10 {
public static void main(String[] args) {

Rannable10 run10 = new Rannable10();

Thread th1 = new Thread(run10);
Thread th2 = new Thread(run10);
Thread th3 = new Thread(run10);

th1.start();
th2.start();
th3.start();
}
}

class Rannable10 implements Runnable {
private static int ticket = 100;
// 1、在成员变量前创建Lock锁
Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
// 2、可能会出现安全问题的代码前,调用Lock方法获取锁
lock.lock();
payticket();
}
}

public void payticket() {
if (ticket > 0) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
// 加了finally
} finally {
// 3、可能会出现安全问题的代码后,调用unLock释放锁
lock.unlock();
}
}
}
}

第八章、线程状态

8.1、线程状态概述

当线程被创建并启动以后,它既不是已启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,有几种状态呢?

线程状态 导致状态发生条件
new(新建) 线程刚被创建,但是未启动,还没调用start方法
runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,这取决于操作系统的处理器
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他线程持有,则该线程进入Blocked状态,当该线程持有锁,改线程编程为Runnable状态
waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入waiting状态,进入这个状态后时不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒
Timed Waiting(计时等待、休眠) 同waiting状态,有几个方法会有超时参数,调用他们进入Timed Waiting状态,这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep、Object wait
Teminated(死亡状态) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

注意:他们各个状态之间可以相互转换

8.2 Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。单独的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?

在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在run方法中添加了sleep语句,这样就强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。

其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等

待),那么我们通过一个案例加深对该状态的一个理解。

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔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
30
31
32
/**
* 实现一个计数器,计数到100,在每个数字之间暂停1秒,
* 每隔10个数字输出一个字符串
*/
/*
* 在这我们新建一个线程,它是处于可以运行状态
* 如果我们调用了sleep方法,它就会进入到计时等待状态
* 等待结束以后,它又会回到可运行的状态
* 这个就被叫做计时等待
*/
public class Demo01 {
public static void main(String[] args) {
Thread th1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 10 == 0) {
System.out.println("每间隔10个数字输出一个字符串" + i);
System.out.println("------------------------");
}
System.out.println("i = " + i);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
th1.start();
}
}

通过案例可以发现,sleep方法的使用还是很简单的。我们需要记住下面几点:

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。

  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠

  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

小提示:sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就

开始立刻执行。

8.3 BLOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态。

1
2
3
4
5
6
7
/**
* 锁阻塞:
* 首先它也是先创建一个线程,它处于可运行的状态
* 什么被称之为可运行的状态呢,就是它可以运行,但是它需要和别的线程去争抢cpu执行权
* 没有争取到锁的对象进入到锁阻塞的状态
* 如果锁阻塞状态的线程,获取到锁对象,它就会进入到可运行的状态
*/

8.4 Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

那么我们之前遇到过这种状态吗?答案是并没有,但并不妨碍我们进行一个简单深入的了解。我们通过一段代码来

学习一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 无限等待:
* 等待唤醒案例(线程之间的通信)
* 有两个商铺,一个卖包子的商铺,一个买包子的商铺
* 顾客要买包子,和老板说明买包子的数量和种类,
* 顾客就等着老板做包子(调用wait方法)
* waiting状态,无限等待
*
*
* 老板开始做包子,根据你的需要来做包子,做好包子告诉顾客(调用notify)
* 告诉顾客包子做好了,就可以开始吃了
* 所以顾客和老板之间就形成了一个通信
*
* 顾客来买包子,要等待老板做包子,调用wait方法
* 老板做好包子,告诉顾客包子做好了,调用notify方法
* 这个就形成线程之间的通信
*
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
/**
* 等待唤醒案例:线程之间的通信
* 创建一个顾客线程(消费者)告知老板要的包子的种类和数量
* 调用wait方法,放弃cpu的执行,进入到WAITING无限等待状态
*
* 创建一个老板线程(生产者)花了5秒做包子,做好包子之后,调用
* notify方法唤醒顾客,吃包子
*
* 注意:顾客和老板线程必须使用同步代码块包裹起来,包子等待和唤醒只能有
* 一个在执行
* 同步使用的锁对象必须保证唯一
*
* 只有锁对象才能调用wait和notify方法,非锁对象是不能调用的
*
* object类中的方法
* void wait()
* 在其他线程调用此对象的notify()方法或者notifyAll()方法前,导致当前线程等待
*
* void notify()
* 唤醒在此对象监视器上等待的单个线程
* 会继续执行wait方法之后的代码
*/

public class Demo05 {
public static void main(String[] args) {
// 创建锁对象,保证唯一
Object obj = new Object();

// 创建一个顾客线程(消费者)
Thread th1 = new Thread() {
@Override
public void run() {
while (true) {
// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj) {
System.out.println("顾客线程(消费者)告知老板要的包子的种类和数量");
// 调用wait方法,让它进入到无限等待状态
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒之后执行的代码
System.out.println("包子已经做好了,可以开始吃了");
System.out.println("------------------------");
}
}
}
};
th1.start();

// 创建一个老板线程(生产者)
Thread th2 = new Thread() {
@Override
public void run() {
while (true) {
// 老板线程(生产者)花了5秒做包子,做好包子之后,
try {
Thread.sleep(5000);// 花5秒做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj) {
System.out.println("做好包子,告知顾客,可以吃包子了");
// 调用notify方法唤醒顾客,吃包子
obj.notify();
}
}
}
};
th2.start();
}
}

通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的

Object.notify()方法 或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,

多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法,那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了

notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入

Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

8.5、wait和notify

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
/**
* Object类中wait带参方法和notifyAll()方法
*
* 进入到TimeWaiting(即使等待)有两种方式
* 1、使用sleep(long m) 方法,在毫秒结束以后,线程睡醒
* 进入到Runnable/Blocked状态
*
* 2、使用wait(long m)方法,wait方法如果在毫秒结束之后
* 还没被notify唤醒,就会自动醒来
*
* 唤醒的方法:
* void notify()
* 唤醒在此对象监视器上等待的单个线程,
* void notifyAll()
* 唤醒在此对象监视器上等待的所有线程,
*
*/
public class Demo06 {
public static void main(String[] args) {
// 创建锁对象,保证唯一
Object obj = new Object();
Thread th1 = new Thread("th1") {
@Override
public void run() {
while (true) {
synchronized (obj) {
System.out.println("顾客1告诉老板要的包子种类和数");
try {
obj.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒之后执行的代码
System.out.println("顾客1包子已经做好了,开吃");
System.out.println("-----------------");
}
}
}
};
th1.start();

Thread th2 = new Thread() {
@Override
public void run() {
while (true) {
synchronized (obj) {
System.out.println("顾客2告诉老板要的包子种类和数");
try {
obj.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒之后执行的代码
System.out.println("顾客2包子已经做好了,开吃");
System.out.println("-----------------");
}
}
}
};
th2.start();

// 老板线程
Thread th3 = new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println("老板5秒钟后做好包子"
+"告知顾客,包子可以吃了");
obj.notify();
}
}
}
};
th3.start();
}
}

第九章、等待与唤醒机制

9.1、线程之间的通信

概念:多线程在处理同一个资源,但是处理的动作(线程任务)却不同。

比如:A线程是用来生产包子的,B线程是用来吃包子的。包子可以理解为同一资源,线程A和线程B处理的动作一个是生产,一个是消费。那么线程A和线程B之间就存在线程通信问题,大家也可以把他们理解成以后工作以后的合作,一个人做登陆,一个人做注册,一个人做购买,等等。所以线程之间也可以存在一个通信,称为合作。

9.2、为什么要处理线程通信

多线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程共同完成一个任务,并且我们希望他们有规律的去执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

9.3、如何保证线程间通信有效利用资源

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作,就说多个线程在操作同一个数据时,避免对同一变量的争夺,也就是我们需要通过一定的手段,使各个线程能有效的利用资源,而这种手段被称之为 – 等待唤醒机制

9.4、等待唤醒机制

什么是等待唤醒机制

这个是多个线程之间的一种协作机制,讲到线程,我们经常讲到的是线程之间的竞争(race),比如去争夺锁,但是这并不是故事的全部,线程之间也会有协作机制,就好比在公司里面,你和你的同事,你们之间就存在着晋升时的竞争,但是更多的时候,你们可能存在的时候,你们需要一起合作完成某些事情的一个合作关系。

就是在线程进行了规定的操作以后,就进入到一个等待状态(wait() ) ,等待其他线程完成执行完他们指定的代码过后,在将其唤醒(notify() );在有多个线程进入等待以后,如果需要,可以使用 notifyAll() 来唤醒所有线程,

wait/notify就是一种线程协作机制

例子:

我们用包子举例,等待与唤醒机制也被称作线程之间的通信重点:有效的利用资源(生产包子,吃一个包子,在生产包子,吃一个包子。。。)这里的资源呢,指的就是包子(生产一个吃一个),

重点解释:

通信:对包子的状态进行判断(判断是否有包子,如果没有,吃包子线程则唤醒生产包子线程,吃包子线程等待,生产包子线程制作包子,做好包子之后修改包子状态,改为有包子;

有存在包子,生产包子线程则唤醒吃包子线程,生产包子线程等待,吃包子线程吃包子,吃完包子修改包子状态为没有)

他们可以反复执行,这也就叫做线程之间的通信,也被称之为等待和唤醒。

重点的是我们合理运用这个包子来判断到底是哪条线程在执行。

那等待与唤醒就要用到我们着几个方法

等待唤醒中的方法

等待唤醒机制就是用来解决线程之间的通信问题,使用到的3个方法的含义如下:

1、wait:线程不在活动,不在参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这个时候的线程即是WAITING状态,它还要等待别的线程执行一个特别的动作,也即是”通知“( notify() )在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列中(ready quene)中。

2、notify:则选取所通知的对象的wait set中的一个线程释放,例如:餐厅有位置以后,等待最久的顾客优先入座。

3、notifyAll:则释放所通知对象的wait set上的全部线程

注意:哪怕只通知了一个等待的线程,被通知的线程也不能立即恢复执行,因为它当初中断的地方是在同步块中,而此刻它已经不在持有锁,所以这个时候它需要再次尝试去获取锁,(很可能面临其他线程的竞争)成功以后才能调用wait方法之后的地方恢复执行。

调用wait和notify所需要注意的细节

1、wait和notify方法必须要由同一个锁对象调用,因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程

2、wait方法和notify方法都是属于Object类的方法,因为锁对象可以是任意对象,因为任意对象的所属类都是继承了Object类

3、wait方法和notify方法必须使用在同步代码块或者是同步函数中使用,因为必须要通过锁对象才能调用者两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 分析:需要哪些类
* 1、资源类:包子
* 设置包子的属性 :皮 馅料 状态:1、有包子true 2、没包子false
* 2、生产者(包子铺): 线程类可以继承Thread
* 设置线程任务(run),重写run方法 -- 生产包子(对包子状态进行判断true或者false)
* true:有包子,包子铺调用wait方法进入等待状态
* false:没有包子,包子铺生产包子(可以增加趣味性,生产两种包子)
* 即有两种状态,(i%2==0)这样是不是就有两个状态了
* 包子铺生产好了包子修改状态为true
* 唤醒吃货线程,让吃货线程吃包子
* 消费者(吃货)类:是一个线程类,可以继承Thread
* 设置线程任务(run),重写run方法 -- 吃包子(对包子的状态进行判断true或者false)
* false:没有包子,吃货线程调用wait方法,进入等待状态
* true:有包子,吃货吃包子,吃货吃完包子,修改包子的状态为false
* 吃货唤醒包子线程,生产包子
* 3、测试类:
* 包含main方法的类,是程序执行的入口,启动程序
* 创建包子对象,创建包子铺线程,开启
* 创建吃货线程,开启
*/
public class Demo01 {
public static void main(String[] args) {

}
}

9.5、生产者与消费者

等待与唤醒机制,其实就是经典的生产者与消费者的问题

就拿生产包子和消费包子来说等待唤醒机制如何有效的利用资源

1
2
3
4
5
6
包子铺生产包子,吃货线程消费包子当。
包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),
并通知吃货线程(解除吃货线程的等待状态),因为此时已经有包子了,那么包子铺线程进入等待状态。
接下来,吃货线程能否进一步执行,取决于锁的获取情况,如果吃货获取到锁,那么就执行吃包子动作,
包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待,
包子铺线程是否能够进一步执行,则取决于锁的获取情况

代码演示:

1
2
3
4
5
6
7
8
9
//包子类
public class Demo01 {
//皮
String pi;(wrapper)
//馅
String xian;(stuffing)
//包子的状态为true或者false。设置初始值为false
boolean flag = 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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
生产者(包子铺): 线程类可以继承Thread
设置线程任务(run),重写run方法 -- 生产包子(对包子状态进行判断true或者false)
true:有包子,包子铺调用wait方法进入等待状态
false:没有包子,包子铺生产包子(可以增加趣味性,生产两种包子)
即有两种状态,(i%2==0)这样是不是就有两个状态了
包子铺生产好了包子修改状态为true
唤醒吃货线程,让吃货线程吃包子
注意:
包子铺线程和包子线程关系 - 通信关系(互斥关系)
必须同时同步技术保证两个线程只能有一个在执行
锁对象必须保证唯一,可以使用包子对象作为锁对象。
包子铺类和吃货类需要吧包子对象作为参数传递进来
1、需要在成员位子创建一个包子变量
2、使用带参数构造方法,为这个包子变量赋值

*/
//包子铺类
public class Demo02 extends Thread {
// 1、需要定义在成员位置创建一个变量
private Demo01 bz;

// 2、使用带参数构造方法,为这个包子变量赋值
public Demo02(Demo01 bz) {
this.bz = bz;
}

// 设置线程任务(run) 生产包子
@Override
public void run() {
// 定义一个变量
int count = 0;
while (true) {

synchronized (bz) {
// 对包子状态进行判断
if (bz.flag == true) {
// 包子铺调用wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被唤醒之后执行,包子铺生产包子
// 增加一些趣味性,交替产生两种包子
if (count % 2 == 0) {
// 生产薄片包子
bz.pi = "薄片";
bz.xian = "三鲜";
} else {
// 生产厚皮,牛肉馅
bz.pi = "厚皮";
bz.xian = "牛肉";
}
count++;
System.out.println("包子铺正在生产" + bz.pi + bz.xian + "包子");
// 生产包子需要3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此时包子铺生产好了包子
// 修改包子的状态为true
bz.flag = true;
// 唤醒吃货线程,让吃货线程吃包子
bz.notify();
System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "包子,吃货们可以开始吃了");
}
}
}
}
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
/**
消费者(吃货)类:是一个线程类,可以继承Thread
设置线程任务(run),重写run方法 -- 吃包子(对包子的状态进行判断true或者false)
false:没有包子,吃货线程调用wait方法,进入等待状态
true:有包子,吃货吃包子,吃货吃完包子,修改包子的状态为false
吃货唤醒包子线程,生产包子
*/
public class Demo03 extends Thread{
//1、需要在成员位置创建一个包子变量
private Demo01 bz;
//2、使用带参数的构造方法,为这个包子变量赋值
public Demo03(Demo01 bz) {
this.bz = bz;
}

//设置线程任务(run) 吃包子
@Override
public void run() {
//使用死循环,让吃货一直吃包子
while(true) {
//必须同时同步技术保证两个线程只能有一个在执行
synchronized (bz) {
//对包子状态进行判断
if(bz.flag == false){
//调用吃货的wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行的代码,吃包子
System.out.println("吃货正在吃:" + bz.pi
+bz.xian +"的包子");
//修改包子的状态为false
bz.flag = false;
//唤醒包子铺线程
bz.notify();
System.out.println("吃货已经把" +bz.pi + bz.xian
+"的包子吃完了,包子铺开始生产包子");
System.out.println("-----------------------");
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**		
* 3、测试类:
* 包含main方法的类,是程序执行的入口,启动程序
* 创建包子对象,创建包子铺线程,开启
* 创建吃货线程,开启
*/
public class Demo04 {
public static void main(String[] args) {
//创建包子对象:
Demo01 bz = new Demo01();

//创建包子铺线程,开启,生产包子
Demo02 baozipu = new Demo02(bz);
baozipu.start();

//创建吃货线程,开启,吃包子
Demo03 chibaozi = new Demo03(bz);
chibaozi.start();
}
}

第十章、线程池

10.1、线程池思想概念

我们使用线程的时候就去创建一个线程,这样实现起来非常简单,但是就会有这样的一个问题:

如果线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁的创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法可以使线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果,今天我们就来详细讲解一下Java线程池

10.2、线程池概念

  • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多叙述。

线程池可以理解成一个容器,容器我们可以理解成什么?是不是可以理解成一个集合(ArrayList,HashSet,LinkedList,HashMap)这里我们可以用什么呢?是不是可以用一个LinkedList,也可以用ArrayList集合,这些集合的泛型放什么呢,就放我们的线程。所以这个线程池实际上就是我们的集合,就是我们的容器。

使用add方法添加线程

  1. 当程序第一次启动的时候,我们可以创建多个线程,保存到一个集合中

  2. 当我们想要使用线程的时候,就可以从集合中取出线程来使用

  3. Thread th1 = list.remove(0) :返还的是被移除的元素(线程只能被 一个任务使用)

  4. 如果使用Thread th2 = LinkedList.removeFirest()

  5. 当使用线程需要把线程归还给线程池

  6. list.add(th1);

  7. Linked.addLast(th2);

  8. 所以线程里面有一个叫做队列的东西。依次拿出线程使用

在JDK1.5以后,JDK内置了线程池,我们可以直接使用。

这就是线程池的原理

合理利用线程池能够带来三个好处:

  1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务

  2. 提高相应速度,当任务到达时,任务可以不需要的等到线程创建就可以立即执行

  3. 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB,线程开的越多,消耗的内存也就越大,最后死机)

10.3、线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上来说Executor并不是一个线程池,而是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理并不是很清楚的情况下,很有可能配置的线程池不是比较优的。因此在java.util.concurrent.Executors线程工程类里面提供了一些镜头工厂,生成一些常用的线程池,官方建议使用Executors工程类来创建线程池对象

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象(创建的是有界线的线程池,也就是线程池中的线程个数可以指定最大数量)

获取到一个线程池ExecutorService,那怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池的某一个对象并执行
  • Future接口:用来记录线程任务执行完毕后产生的结果,线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象

  2. 创建Runnable接口子类对象(task)

  3. 提交Runnable接口子类对象。(task task)

  4. 关闭线程池(一般不做)

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
/*
线程池:JDK1.5以后提供的
在java.util.concurrent.Executors
线程池工厂类,用来生产线程池
Executors有个静态方法 -- 生产线程池的方法
static ExecutorService newFicedThreadPool(int nThreads)
创建一个可重用固定线程数的线程池
参数:
int nThreads:创建线程池中包含的线程数量
返回值:
ExecutorService接口,返回的是ExecutorService
接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)

java.util.concurrent.ExectorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task) 提交一个Runnable任务用于执行

关闭/消耗线程池的方法
shutdown()

线程池的使用步骤:
1、使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool
生产一个执行线程数量的线程池
2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
3、调用ExectorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4、调用ExectorService中的方法shutdown销毁线程池(不建议使用)
注意:我们使用线程池的目的是为了反复使用,销毁了就没了对吧。
*/
public class Demo01 {
public static void main(String[] args) {
//1、使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool
// 生产一个执行线程数量的线程池,实际开发中不介意这样使用,最好是用ExecutorService的构造方法
ExecutorService es = Executors.newFixedThreadPool(2);

// 3、调用ExectorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
es.submit(new RunnableImpl());

// 线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程池可以继续使用
es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程
es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程

// 4、调用ExectorService中的方法shutdown销毁线程池(不建议使用)
es.shutdown();

//线程池被销毁了,便不能在执行了。会抛出异常
//不能在获取线程
es.submit(new RunnableImpl());
}
}
1
2
3
4
5
6
7
8
9
10
public class RunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}