十、 多线程
10.1 基本概念:程序、进程、线程
10.1.1 三者的概念
- 程序(program):一段静态的文本代码。
- 进程(process):当程序加载进内存运行起来后,就成了进程。是一个动态的过程:有其自身的生命周期。
- 线程(thread):一个进程里面的一条或者几条执行路径。
- 若一个进程同一时间并行执行多个代码,就是支持多线程的。
- 但是由于线程共享JVM中的方法区和堆,多个线程操作共享的系统资源可能会有冲突,带来安全隐患。
10.1.2 线程在虚拟机中的分配
每个线程独享一份 | 每个线程共享的 |
---|---|
虚拟机栈 (VM Stack) | 堆 (Heap) |
程序计数器 (Program Counter Register) | 方法区 (Method Area) |
10.1.3 并行与并发的概念
并行 | 并发 |
---|---|
多个CPU同时执行多个任务。比如:多个人同时做不同的事。 | 一个CPU( 采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。 |
10.1.4 何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要 一些后台运行的程序时。
10.2 线程的创建和使用
10.2.1 创建方式一:继承Thread父类
( 1 1 1) 创建一个继承于Thread类的子类;
( 2 2 2) 重写Thread类中的run()方法;
( 3 3 3) 创建Thread类的子类的对象;
( 4 4 4) 通过对象调用start()方法开始执行此多线程任务。
- 其中,start() 方法的作用有:启动当前线程和调用当前线程的run() 方法。
例:
public class CreateThreadTest extends Thread{
@Override
public void run() {
for (int i=0;i<20;i++){
if (i%2==0){
System.out.println(i);
}
}
}
public static void main(String[] args) {
CreateThreadTest thread1 = new CreateThreadTest();
thread1.start();
//以下是主线程输出奇数
for (int i=0;i<20;i++){
if (i%2!=0){
System.out.println(i+"*******main********");
}
}
}
}
输出:
1*******main********
3*******main********
5*******main********
7*******main********
0
2
4
6
8
10
12
14
16
18
9*******main********
11*******main********
13*******main********
15*******main********
17*******main********
19*******main********
从结果可以看到:
- thread1 线程中的输出偶数的线程和main() 函数主线程中的输出奇数的线程,是同时进行的。
- 两个线程的输出由于是同时进行的,因此输出结果会互相穿插。
10.2.2 创建线程的注意点
-
不能通过调用
thread1.run();
的方式来启动该线程。否则仍然是在主线程 (main) 中按顺序执行罢了。并没有开辟新的线程去执行。 -
想要再启动一个线程,不能再写一次
thread1.start();
。否则在main 线程中出现 2 2 2 次thread1.start();
会报illegalThreadStateException 异常。正确的方法是:再new一个线程对象。CreateThreadTest thread1 = new CreateThreadTest(); thread1.start(); //正确的再次启动线程方法 CreateThreadTest thread2 = new CreateThreadTest(); thread2.start();
10.2.3 创建线程的练习
题目:
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。
我的首次答案:
public class ThreadExer1 {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
t1.start();
Thread2 t2 = new Thread2();
t2.start();
}
}
/**
* @Author: Sihang Xie
* @Description: 线程1-遍历100以内的偶数
* @Date: 2022/3/14 10:16
*/
class Thread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
/**
* @Author: Sihang Xie
* @Description: 线程2-遍历100以内的奇数
* @Date: 2022/3/14 10:20
*/
class Thread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出结果:
Thread-0:0
Thread-0:2
Thread-0:4
Thread-0:6
Thread-0:8
Thread-0:10
Thread-0:12
Thread-0:14
Thread-0:16
Thread-0:18
Thread-0:20
Thread-0:22
Thread-0:24
Thread-0:26
Thread-0:28
Thread-0:30
Thread-0:32
Thread-0:34
Thread-0:36
Thread-0:38
Thread-0:40
Thread-0:42
Thread-0:44
Thread-0:46
Thread-0:48
Thread-0:50
Thread-0:52
Thread-0:54
Thread-0:56
Thread-0:58
Thread-0:60
Thread-0:62
Thread-0:64
Thread-0:66
Thread-0:68
Thread-0:70
Thread-0:72
Thread-0:74
Thread-1:1
Thread-1:3
Thread-1:5
Thread-1:7
Thread-1:9
Thread-1:11
Thread-1:13
Thread-1:15
Thread-1:17
Thread-1:19
Thread-0:76
Thread-0:78
Thread-0:80
Thread-0:82
Thread-0:84
Thread-0:86
Thread-0:88
Thread-0:90
Thread-0:92
Thread-0:94
Thread-0:96
Thread-0:98
Thread-1:21
Thread-1:23
Thread-1:25
Thread-1:27
Thread-1:29
Thread-1:31
Thread-1:33
Thread-1:35
Thread-1:37
Thread-1:39
Thread-1:41
Thread-1:43
Thread-1:45
Thread-1:47
Thread-1:49
Thread-1:51
Thread-1:53
Thread-1:55
Thread-1:57
Thread-1:59
Thread-1:61
Thread-1:63
Thread-1:65
Thread-1:67
Thread-1:69
Thread-1:71
Thread-1:73
Thread-1:75
Thread-1:77
Thread-1:79
Thread-1:81
Thread-1:83
Thread-1:85
Thread-1:87
Thread-1:89
Thread-1:91
Thread-1:93
Thread-1:95
Thread-1:97
Thread-1:99
练习的体会:
-
输出的线程会互相交错出现,说明2条线程确实是并行运行的。
-
可以用
System.out.println(Thread.currentThread().getName());
来查看当前执行的线程是哪一个。
-
当需要执行多个不同功能的线程时,需要创建不同的子类来实现。
10.2.3.1 练习更优的写法
- 上述练习创建的线程对象,都是只用一次。因此可以创建匿名子类的匿名对象来启动线程。
优化后的代码:
//线程1-遍历100以内的偶数
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
//线程2-遍历100以内的奇数
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
10.2.4 线程的常用方法
线程方法 | 作用 |
---|---|
void start() | 启动线程,并执行对象的 run()方法 |
run() | 线程在被调度时执行的操作 |
String getName() | 返回线程的名称 |
void setName() | 设置该线程名称 |
static Thread currentThread() | 返回当前线程。在 Thread 子类中就是this ,通常用于主线程和 Runnable实现类 |
static void yield() | 线程让步:暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;若队列中没有同优先级的线程,忽略此方法 |
join() | 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。低优先级的线程也可以获得执行。 |
static void sleep(long millis):(指定时间:毫秒) | ① 令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重新排队。② 抛出 InterruptedException 异常。 |
stop() | 强制线程生命期结束,不推荐使用! |
boolean isAlive() | 返回 boolean ,判断线程是否还活着 |
使用例子:
( 1 1 1) 设置线程名称
- 次线程可通过
setName()
设置名字。 - 主线程可通过
currentThread().setName()
设置名字。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
主线程:1
主线程:3
主线程:5
求偶数的线程:0
求偶数的线程:2
求偶数的线程:4
求偶数的线程:6
求偶数的线程:8
主线程:7
主线程:9
主线程:11
主线程:13
主线程:15
主线程:17
主线程:19
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18
( 2 2 2) yield线程让步
-
放弃当前的线程。然后有两种可能:
① 被放弃的线程被别的线程抢到执行;
② 又被自己抢到执行机会。这样就会看起来没什么区别。
-
例子:当 i i i 能被4整除时,调用
yield
。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i % 4 == 0) {
Thread.yield();
}
}
}
}
输出:
主线程:1
主线程:3
主线程:5
主线程:7
主线程:9
求偶数的线程:0
主线程:11
求偶数的线程:2
求偶数的线程:4
主线程:13
主线程:15
主线程:17
主线程:19
求偶数的线程:6
求偶数的线程:8
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18
- 可以看到,当
求偶数的线程:4
时,线程放弃执行机会,后面就被主线程抢到。直到主线程输出完所有20以内的奇数为止。
( 3 3 3) join操作:打断当前线程的执行,插入其他线程
-
join() 操作就是一个插队操作。在当前线程中插入别的线程的 join() 方法后,当前线程就中断,去执行另一个线程直到执行完毕再恢复原来的线程。
-
join() 方法要处理异常。
-
下面的例子中,当主线程的 i i i 执行到 5 5 5 时,主线程被中断,插入了另一个线程的
threadName.join()
方法。只有当treadName
线程执行完毕,才能恢复主线程的执行。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//join()方法的测试
if (i == 5) {
try {
threadName.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
主线程:1
主线程:3
主线程:5
求偶数的线程:0
求偶数的线程:2
求偶数的线程:4
求偶数的线程:6
求偶数的线程:8
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18
主线程:7
主线程:9
主线程:11
主线程:13
主线程:15
主线程:17
主线程:19
( 4 4 4) sleep(long millis):让线程阻塞 millis 的时间
- sleep(long millis) 需要异常处理。
- 下面的例子是
threadName
线程每隔 1000 1000 1000 毫秒 ( 1 1 1 秒) 才输出一个偶数。可以看到有倒计时的效果。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//join()方法的测试
if (i == 5) {
try {
threadName.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
//sleep(long millis)的测试
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
( 5 5 5) isAlive():让线程阻塞 millis 的时间
- 线程在
run()
方法执行完之后就死亡了。可以使用isAlive()
方法判断该线程是否还存活。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//join()方法的测试
if (i == 5) {
try {
threadName.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//isAlive()方法测试
System.out.println("线程threadName是否存活:" + threadName.isAlive());
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
//sleep(long millis)的测试
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
主线程:1
主线程:3
主线程:5
求偶数的线程:0
求偶数的线程:2
求偶数的线程:4
求偶数的线程:6
求偶数的线程:8
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18
主线程:7
主线程:9
主线程:11
主线程:13
主线程:15
主线程:17
主线程:19
线程threadName是否存活:false
10.2.5 线程的优先级
-
线程优先级分为 1 1 1 ~ 10 10 10 级。其中 10 10 10 级为最高优先级。
-
Thread 类中默认设置了三档优先级属性:
优先级 属性 最大优先级 MAX_PRIORITY:10 默认优先级 NORM_PRIORITY:5 最小优先级 MIN _PRIORITY:1 - 如果没有明确声明,一般线程优先级默认设置为 5 5 5 。
-
关于优先级的方法:
方法名 作用 getPriority() 获取当前线程的优先级 setPriority(Thread.MAX_PRIORITY) 设置当前线程的优先级
说明:
- 至少要在
start()
前,设置优先级。 - 线程被设置为最大优先级时,并不意味着该线程一定能执行完,才去执行低优先级的线程。只是有很大概率能被 CPU 优先执行。
例子:
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
//设置线程优先级为最大
threadName.setPriority(Thread.MAX_PRIORITY);
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
//设置主线程优先级为最小
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println("优先级:" + Thread.currentThread().getPriority() +
"|" + Thread.currentThread().getName() + ":" + i);
}
}
//isAlive()方法测试
System.out.println("线程threadName是否存活:" + threadName.isAlive());
}
}
//线程ThreadName
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println("优先级:" + getPriority() +
"|" + Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
优先级:1|主线程:1
优先级:10|求偶数的线程:0
优先级:10|求偶数的线程:2
优先级:10|求偶数的线程:4
优先级:10|求偶数的线程:6
优先级:10|求偶数的线程:8
优先级:10|求偶数的线程:10
优先级:1|主线程:3
优先级:10|求偶数的线程:12
优先级:10|求偶数的线程:14
优先级:10|求偶数的线程:16
优先级:10|求偶数的线程:18
优先级:1|主线程:5
优先级:1|主线程:7
优先级:1|主线程:9
优先级:1|主线程:11
优先级:1|主线程:13
优先级:1|主线程:15
优先级:1|主线程:17
优先级:1|主线程:19
线程threadName是否存活:false
- 可以看到优先级为 1 的主线程,居然在第一行被优先执行了,说明优先级高的并不一定先被执行。但从整体来说,
求偶数的线程
被优先执行的概率确实是比较大的。
10.2.6 例子:卖票-线程安全问题
题目
创建三个窗口卖票,总票数为100张。
首先创建一个卖票的窗口类Window:
- 其中,
ticket
变量必须设置为static
的,所有的线程实例都共享这个静态变量。否则会出现每个线程各自卖 100 100 100 张票。
class Window extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(getName() + ":卖票,票号为:" +
ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
然后,在主线程中创建3个窗口实例。开始卖票:
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
w1.setName("窗口1");
Window w2 = new Window();
w2.setName("窗口2");
Window w3 = new Window();
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
输出:
- 可以看到,3个窗口实例线程都卖出了票号为 100 100 100 的票。这显然是不符合现实逻辑的。这就引出了线程安全问题,引出了下面第二种创建线程的方式。
10.2.7 创建方式二:实现Runnable接口
( 1 1 1) 创建一个实现了Runnable接口的类;
( 2 2 2) 实现类去实现Runnable中的抽象方法:run();
( 3 3 3) 创建实现类的对象;
( 4 4 4) 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
( 5 5 5) 通过Thread类的对象调用start()。
例:
//1.创建一个实现了Runnable接口的类
class CreateTread implements Runnable {
private int ticket = 10;//注意这里没加static
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
public class ThreadCreate2 {
public static void main(String[] args) {
//3.创建实现类的对象
CreateTread c = new CreateTread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(c);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(c);
t2.setName("窗口2");
t2.start();
}
}
输出:
窗口1:卖票,票号为:10
窗口1:卖票,票号为:9
窗口1:卖票,票号为:8
窗口1:卖票,票号为:7
窗口1:卖票,票号为:6
窗口1:卖票,票号为:5
窗口1:卖票,票号为:4
窗口1:卖票,票号为:3
窗口1:卖票,票号为:2
窗口1:卖票,票号为:1
票已售罄
窗口2:卖票,票号为:10
票已售罄
- 可以看到,虽然
ticket
没有加static
修饰,但2个线程是共用这10张票的。因为线程 t1 和 t2 都共用c
这一个对象创建线程。 - 可以看到还是存在 10 10 10 号票被2个窗口都卖出去的情况。并没有解决实际问题。
10.2.8 两种线程创建方式的比较
-
开发中,优先选择:实现Runnable接口的方式。
- 原因1:实现的方式没有类的单继承性的局限性。而继承的方式就不能继承其他父类了。
- 原因2:实现的方式更适合来处理多个线程有共享数据的情况。而继承的方式处理多个线程共享数据时需要把此数据定义为
static
的。
-
联系:
Thread
类实际上也实现了Runnable
接口。public class Thread implements Runnable { …… }
-
相同点:两种方式都需要重写
run()
,将线程要执行的逻辑声明在run()
中。
10.3 线程的生命周期
10.3.1 线程的 5 5 5 种生命周期
线程状态 | 描述 |
---|---|
新建 | 当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 |
就绪 | 处于 新建 状态的线程被 start() 后,将进入线程队列等待 CPU 时间片。此时它已具备了运行的条件 ,只是没分配到 CPU 资源。 |
执行 | 当就绪的线程被调度并获得 CPU 资源时,便进入运行状态, run() 方法定义了线程的操作和功能 |
阻塞 | 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态 |
死亡 | 线程完成了它的全部工作、或线程被提前强制性地中止、或出现异常导致结束 |
10.3.2 线程生命周期
死亡
是线程的最终状态。任何线程必然走向死亡。 阻塞不是线程的最终状态,而是中间的、暂时的一种状态。
10.4 线程的同步
10.4.1 线程安全问题
-
以上方卖票窗口的代码为例,会出现重票、甚至错票的问题。
- 重票:
-
错票:
问题出现的原因
- 当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
如何解决?
- 当一个线程a在操作
ticket
的时候,其他线程不能参与进来。直到线程a操作完ticket
时,其他线程才可以开始操作ticket
。这种情况即使线程a出现了阻塞,也不能被改变。 - 相当于上厕所时,给厕所门锁上。只有当自己方便完之后,门外的人才能进来。
10.4.2 同步机制一:同步代码块
在Java中,通过 线程同步机制
来解决线程安全问题。
格式:
- 在需要对共享数据进行操作的部分,用下面的语句包起来:
- 被
synchronized () {}
包裹的代码块,只允许一个线程进来。该线程执行过程中,其他所有线程都必须在代码块外等待。直到该线程执行完毕,其他线程再抢夺进入该代码块的执行权。
synchronized (同步监视器) {
需要被同步的代码;
}
说明:
-
操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
-
共享数据:多个线程共同操作的变量。比如:
ticket
就是共享数据。 -
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
-
要求:多个线程必须要共用同一把锁。
-
补充:在实现
Runnable
接口创建多线程的方式中,我们可以考虑使用this
充当同步监视器。注意继承Thread
方式创建的线程则不能用this
。因为继承方式每一个线程都是不同的对象。
同步代码块在继承法 Thread
创建的线程中的使用
-
在继承
Thread
方式创建的线程时,第一种方法是同步监视器必须要创建新的static
对象。这样才能保证每个线程实例对象都共用同一个同步监视器。static Object obj = new Object();
-
在继承
Thread
方式创建的线程的第二种同步监视器方式是线程类名.class
。synchronized (线程类名.class) { }
例子:
- 在 实现
Runnable
接口创建多线程的方式中采用synchronized () {}
同步代码块:
class CreateTread implements Runnable {
private int ticket = 100;
Object obj = new Object();//创建了一个对象用作同步监视器
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
//线程同步方式一:同步代码块synchronized () {}
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
}
public class ThreadCreate2 {
public static void main(String[] args) {
//3.创建实现类的对象
CreateTread c = new CreateTread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(c);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(c);
t2.setName("窗口2");
t2.start();
Thread t3 = new Thread(c);
t3.setName("窗口3");
t3.start();
}
}
输出:
窗口1:卖票,票号为:50
窗口1:卖票,票号为:49
窗口1:卖票,票号为:48
窗口1:卖票,票号为:47
窗口1:卖票,票号为:46
窗口1:卖票,票号为:45
窗口1:卖票,票号为:44
窗口1:卖票,票号为:43
窗口1:卖票,票号为:42
窗口1:卖票,票号为:41
窗口1:卖票,票号为:40
窗口1:卖票,票号为:39
窗口1:卖票,票号为:38
窗口1:卖票,票号为:37
窗口1:卖票,票号为:36
窗口1:卖票,票号为:35
窗口1:卖票,票号为:34
窗口1:卖票,票号为:33
窗口1:卖票,票号为:32
窗口1:卖票,票号为:31
窗口1:卖票,票号为:30
窗口3:卖票,票号为:29
窗口3:卖票,票号为:28
窗口3:卖票,票号为:27
窗口3:卖票,票号为:26
窗口3:卖票,票号为:25
窗口3:卖票,票号为:24
窗口3:卖票,票号为:23
窗口3:卖票,票号为:22
窗口3:卖票,票号为:21
窗口3:卖票,票号为:20
窗口3:卖票,票号为:19
窗口3:卖票,票号为:18
窗口3:卖票,票号为:17
窗口3:卖票,票号为:16
窗口3:卖票,票号为:15
窗口3:卖票,票号为:14
窗口3:卖票,票号为:13
窗口3:卖票,票号为:12
窗口3:卖票,票号为:11
窗口3:卖票,票号为:10
窗口3:卖票,票号为:9
窗口3:卖票,票号为:8
窗口3:卖票,票号为:7
窗口3:卖票,票号为:6
窗口3:卖票,票号为:5
窗口3:卖票,票号为:4
窗口3:卖票,票号为:3
窗口3:卖票,票号为:2
窗口2:卖票,票号为:1
票已售罄
票已售罄
票已售罄
-
可以看到,重票和错票就已经没有再发生了。
-
好处:同步的方式,解决了线程的安全问题。
-
坏处:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
10.4.3 同步机制二:同步方法
1. 同步方法处理 实现 Runnable
接口线程
-
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
-
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
-
Runnable
接口线程此时的同步方法是非静态的,同步监视器默认为:this
。 -
继承
Thread
线程的同步方法必须是静态的,同步监视器默认为:线程类名.class
。 -
格式:
private synchronized void show() {//同步监视器默认设置为:this 需要被同步的代码; }
例子:
//1.创建一个实现了Runnable接口的类
class Window2 implements Runnable {
private int ticket = 100;//注意这里没加static
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
show();
if (ticket <= 0) {
System.out.println("票已售罄");
break;
}
}
}
//同步方法
private synchronized void show() {//同步监视器默认设置为:this
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
//3.创建实现类的对象
Window2 w = new Window2();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(w);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(w);
t2.setName("窗口2");
t2.start();
Thread t3 = new Thread(w);
t3.setName("窗口3");
t3.start();
}
}
2. 同步方法处理 继承 Thread
创建的线程
-
根据同步监视器必须只有一个,因此继承
Thread
线程的同步方法必须是静态的,此时默认的同步监视器是线程类名.class
。才能保证下面3个线程实例对象共用同一个静态同步方法。 -
例子:
class Window3 extends Thread { private static int ticket = 100; @Override public void run() { while (true) { show(); if (ticket <= 0) { System.out.println("票已售罄"); break; } } } //静态同步方法 private static synchronized void show() {//同步监视器默认设置为:Window3.class if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } } //主线程 public class WindowTest3 { public static void main(String[] args) { Window3 w1 = new Window3(); w1.setName("窗口1"); Window3 w2 = new Window3(); w2.setName("窗口2"); Window3 w3 = new Window3(); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } }
10.4.4 单例模式中的线程安全
-
在单例模式中,分为饿汉式和懒汉式。懒汉式是到了调用的时候才新建对象,这样会存在线程安全问题。
-
要求:使用同步机制将单例模式中的懒汉式改写为线程安全的。
用同步代码块:
//懒汉的单例模式
class Bank {
private Bank() {
}
private static Bank bank = null;
public static Bank getInstance() {
synchronized (Bank.class) {
if (bank == null) {
bank = new Bank();
}
return bank;
}
}
}
用同步方法:
//懒汉的单例模式
class Bank {
private Bank() {
}
private static Bank bank = null;
public static synchronized Bank getInstance() {//此时默认同步监视器是Bank.class
if (bank == null) {
bank = new Bank();
}
return bank;
}
}
注意:
以上两种效率都不高。并不是说线程同步导致的效率低,而是:假如很多个线程同时进来 getInstance()
方法。第一个抢到执行权的线程 new
了一个 Bank
的实例对象,并返回了 bank
实例。后面的所有线程进来不用再 new
一个 Bank
的实例对象了。因此已经不存在线程安全问题,但是却还都要一个一个地排队 return bank
。这样效率显然很低,后面进来的线程不需要进入同步代码块了。
优化代码:
//懒汉的单例模式
class Bank {
private Bank() {
}
private static Bank bank = null;
public static Bank getInstance() {
//方式一:效率稍差
// synchronized (Bank.class) {
// if (bank == null) {
// bank = new Bank();
// }
// return bank;
// }
//方式二:效率更高
if (bank==null){
synchronized (Bank.class){
if (bank==null){
bank=new Bank();
}
}
}
return bank;
}
}
10.4.5 死锁的问题
10.4.6 Lock锁解决线程安全问题
- Lock锁是 JDK 5.0 之后新增的特性。在实际开发中,优先使用 Lock锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多子类) 。
1. Lock锁的使用步骤
-
Lock锁是一个接口,需要通过
ReentrantLock
实现类去实例化对象使用。 -
实现Lock接口的类:
ReentrantLock
,要声明成private
和final
的:private final ReentrantLock lock = new ReentrantLock();
-
通过
lock
对象调用lock()
方法。此后代码只允许单线程进入,要用try-finally包起来,确保在finally
中使用unlock()
方法解锁://上锁,此后代码只允许单线程进入 lock.lock(); try { 需要同步的代码; } finally { //解锁,此后代码可以恢复为多线程操作 lock.unlock(); }
2. 卖票窗口例子
class Window4 implements Runnable {
private int ticket = 100;//注意这里没加static
//实现Lock接口的类:ReentrantLock,要声明成private和final的
private final ReentrantLock lock = new ReentrantLock();
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
//上锁,此后代码只允许单线程进入,要用try-finally包起来确保解锁
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
} finally {
//解锁,此后代码可以恢复为多线程操作
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
//3.创建实现类的对象
Window4 w = new Window4();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(w);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(w);
t2.setName("窗口2");
t2.start();
Thread t3 = new Thread(w);
t3.setName("窗口3");
t3.start();
}
}
3. synchronized 与 Lock锁的对比
-
相同:二者都可以解决线程安全问题。
-
不同:
-
synchronized
机制在执行完相应的同步代码以后,自动地释放同步监视器。 -
Lock
需要手动的启动同步lock()
,同时结束同步也需要手动地实现unlock()
。 -
Lock
只有代码块锁,synchronized
有代码块锁和方法锁。 -
使用
Lock
锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多子类) 。
-
4. 优先使用顺序
Lock
--> synchronized
同步代码块(已经进入了方法体,分配了相应资源) --> synchronized
同步方法(在方法体之外)
10.4.7 线程同步练习
题目:
银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有线程安全问题?如果有,如何解决?
存在线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
i--;
}
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户2存钱,余额为:1000.0
储户1存钱,余额为:1000.0
储户1存钱,余额为:3000.0
储户2存钱,余额为:3000.0
储户2存钱,余额为:5000.0
储户1存钱,余额为:4000.0
- 可见,余额应该是 6000.0 6000.0 6000.0 元,但因为线程不同步导致余额为 5000.0 5000.0 5000.0 元。
1.用Lock锁解决线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
//Lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//上锁
lock.lock();
try {
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
} finally {
//解锁
lock.unlock();
}
i--;
}
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户2存钱,余额为:1000.0
储户1存钱,余额为:2000.0
储户2存钱,余额为:3000.0
储户1存钱,余额为:4000.0
储户2存钱,余额为:5000.0
储户1存钱,余额为:6000.0
2.用 synchronized
同步方法解决线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用同步方法
deposit();
i--;
}
}
//同步方法
private synchronized void deposit() {
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户1存钱,余额为:1000.0
储户2存钱,余额为:2000.0
储户2存钱,余额为:3000.0
储户1存钱,余额为:4000.0
储户1存钱,余额为:5000.0
储户2存钱,余额为:6000.0
3.用 synchronized
同步代码块解决线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步代码块
synchronized (this) {
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
}
i--;
}
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户1存钱,余额为:1000.0
储户2存钱,余额为:2000.0
储户2存钱,余额为:3000.0
储户1存钱,余额为:4000.0
储户2存钱,余额为:5000.0
储户1存钱,余额为:6000.0
10.5 线程的通信
10.5.1 例题
使用两个线程打印1~20。线程1、线程2交替打印。
解:
class Number implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
//唤醒被wait()的线程
notify();
if (num <= 20) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"打印:" + num);
num++;
//使得调用如下wait()方法的线程进入阻塞状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class ThreadCommunication {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
t1.setName("线程1");
Thread t2 = new Thread(n);
t2.setName("线程2");
t1.start();
t2.start();
}
}
-
上述代码的执行经过:线程1抢到同步锁进入
synchronized()
代码块中。–> -
没有线程被
wait()
,因此notify()
无效。–> -
线程1输出
number
1 1 1,执行wait()
被阻塞,同时释放同步锁。–> -
线程2得到同步锁进入
synchronized()
代码块中。–> -
线程2执行
notify()
,激活线程1。但此时同步锁在线程2手上,因此线程1只能在synchronized()
代码块外等待。–> -
线程2继续输出
number
2 2 2,执行wait()
被阻塞,同时释放同步锁。–> -
线程1得到同步锁进入
synchronized()
代码块中。–> -
线程1执行
notify()
,激活线程2。但此时同步锁在线程1手上,因此线程2只能在synchronized()
代码块外等待。–> -
线程1继续输出
number
3 3 3,执行wait()
被阻塞,同时释放同步锁。–>……
如此循环交替输出。
输出:
线程1打印:1
线程2打印:2
线程1打印:3
线程2打印:4
线程1打印:5
线程2打印:6
线程1打印:7
线程2打印:8
线程1打印:9
线程2打印:10
线程1打印:11
线程2打印:12
线程1打印:13
线程2打印:14
线程1打印:15
线程2打印:16
线程1打印:17
线程2打印:18
线程1打印:19
线程2打印:20
10.5.2 通信涉及的三个方法
通信方法 | 作用 |
---|---|
wait() | 一旦执行此方法,该线程就进入阻塞状态。并释放同步监视器的锁。 |
notify() | 一旦执行此方法,就会唤醒被 wait() 的一个线程。如果有多个线程被 wait() ,那么唤醒优先级高的。如果都为默认优先级,则随机唤醒一个。 |
notifyAll() | 一旦执行此方法,就会唤醒所有被 wait() 的线程。 |
10.5.3 使用说明
wait()
,notify()
,notifyAll()
三个方法必须使用在同步代码块或同步方法中。wait()
,notify()
,notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException
异常。wait()
,notify()
,notifyAll()
三个方法是定义在java.lang.Object类中。而不是 Thread 类中。
10.5.4 sleep()
和 wait()
的异同?
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
1)两个方法声明的位置不同:Thread
类中声明 sleep()
, Object
类中声明 wait()
。
2)调用的要求不同:sleep()
可以在任何需要的场景下调用。 wait()
必须使用在同步代码块或同步方法中。
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()
不会释放锁,wait()
会释放锁。
10.5.5 经典例题:生产者/消费者问题
题目:
难点
-
生产者端和消费者端的线程之间如何通信?
答:让生产者端和消费者端的构造器都与店员 (Clerk) 关联起来。
class Producer implements Runnable { private Clerk clerk; //把生产者端和中间端店员关联起来 public Producer(Clerk clerk) { this.clerk = clerk; } ...... }
-
消费者线程一开始就因为产品为0而被
wait()
,notify()
应该加在哪儿呢?答:只要生产了1件商品,就可以通知消费者线程开始消费了。相应的,如果店员满了20件商品被
wait()
,只要消费了1件商品,就可以通知生产线程开始生产了。products++; notify(); System.out.println(Thread.currentThread().getName() + "生产,产品数:" + products);
整体代码:
//共享对象:店员
class Clerk {
//固定的产品数量-20个
protected int products;
//生产
public synchronized void produce() {//此时同步监视器为Clerk的对象
//超过20个等一下
if (products >= 20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
products++;
notify();
System.out.println(Thread.currentThread().getName() +
"生产,产品数:" + products);
}
}
//消费
public synchronized void consume() {//此时同步监视器为Clerk的对象,与生产端一样
//没有商品了等一下
if (products <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
products--;
notify();
System.out.println(Thread.currentThread().getName() +
"消费,产品数:" + products);
}
}
}
//生产者
class Producer implements Runnable {
private Clerk clerk;
//把生产者端和中间端店员关联起来
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
//不断增加产品
while (true) {
//设置一下生产速度,暂定1秒生产1个
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produce();
}
}
}
//消费者
class Customer implements Runnable {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
//不断消耗产品
while (true) {
//设置一下消费速度,暂定1秒消耗1个
try {
Thread.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consume();
}
}
}
//主线程
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p = new Producer(clerk);
Customer c = new Customer(clerk);
//生产者端
Thread p1 = new Thread(p);
p1.setName("生产者1");
Thread p2 = new Thread(p);
p2.setName("生产者2");
Thread p3 = new Thread(p);
p3.setName("生产者3");
//消费者端
Thread c1 = new Thread(c);
c1.setName("消费者1");
//多线程启动
p1.start();
p2.start();
p3.start();
c1.start();
}
}
练习的体会:
Clerk
类,既没有继承Thread
类,也没有实现Runnable
接口。因此没有线程的run()
方法,也创建不了多线程。但是,其被实现了Runnable
接口的生产者Producer
类和消费者Customer
类各自关联到自己的构造器当中了,且调用了Clerk
类中的同步方法 生产produce()
和 消费consume()
。因此,Clerk
类中可以定义synchronized
同步方法生产produce()
和 消费consume()
。并在其中调用了线程通信的wait()
和notify()
方法。- 两个不同的类:实现了
Runnable
接口的生产者Producer
类和消费者Customer
类,可以互相通知唤醒notify()
对方的线程。因为生产者Producer
类和消费者Customer
类都调用了Clerk
类中的同步方法 生产produce()
和 消费consume()
,且这两个同步方法拥有相同的同步监视器:Clerk
类的对象this
。
10.6 JDK5.0 新增线程创建方式
10.6.1 创建方式三:实现Callable接口
1. Callable接口创建线程步骤
- 创建一个
Callable
的实现类; - 实现
call()
方法,将此线程需要执行的操作声明在call()中。 - 创建一个
Callable
的实现类的对象; - 将此
Callable
接口实现类的对象作为传递到FutureTask
构造器中,创建FutureTask
的对象; - 将
FutureTask
的对象作为参数传递到Thread
类的构造器中,创建Thread
对象,并调用start()
; - (可选) 获取
Callable
中call
方法的返回值。
2. Callable比Runnable接口强大
- 相比
run()
方法,call()
方法可以有返回值。 call()
方法可以抛出异常。- 支持泛型的返回值 (后面介绍什么是泛型)。
- 需要借助
FutureTask
类,比如获取返回结果。 Future
接口:- 可以对具体
Runnable
、Callable
任务的执行结果进行取消、查询是否完成、获取结果等。 FutureTask
类是Future
接口唯一的实现类。FutureTask
类同时实现了Runnable
和Future
接口。它既可以作为Runnable
被线程执行,又可以作为Future
得到Callable
的返回值。
- 可以对具体
3. 例子
- 输出20内的偶数,并返回这些偶数的和:
//1.创建一个Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;//包装类Integer自动装箱
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建callable实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread t1 = new Thread(futureTask);
t1.start();
try {
//6.获取Callable中call方法的返回值
Object sum = futureTask.get();
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
10.6.2 创建方式四:线程池
- 开发中真正用的都是线程池。
- 背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度 (减少创建新线程的时间);
- 降低资源消耗 (重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理:
- corePoolSize:核心池大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- ……
1. 线程池相关API
-
JDK5.0起提供了线程池相关API:
ExecutorService
和Executors
。 -
ExecutorService
:真正的线程池接口。常见实现类ThreadPoolExecutor
-
void executor(Runnable command)
执行任务/命令,没有返回值,一般用来执行
Runnable
。 -
<T>Future<T>submit(Callable<T>task)
执行任务,有返回值,一般用来执行
Callable
。 -
void shutdown()
关闭线程池。
-
-
Executors
:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。-
Executors.newCachedThreadPool()
创建一个可根据需要,创建新线程的线程池。
-
Executors.newFixedThreadPool(n)
创建一个可重用固定线程数的线程池。
-
Executors.newSingleThreadExecutor()
创建一个只有一个线程的线程池。
-
Exrcutors.newScheduledThreadPool(n)
创建一个线程池,它可以安排在给定延迟后运行命令或者定期地执行。
-
2. 线程池创建线程步骤
- 提供指定线程数量的线程池:
ExecutorService service = Executors.newFixedThreadPool(10);
- 执行指定的线程操作,需要提供实现Runnable或者Callable接口实现类的对象:
service.execute(new NumThread1());
Future future = service.submit(new NumThread2());
- 关闭线程池
service.shutdown();
3. 线程池使用说明
- 下面代码中,
ExecutorService
是接口,因此service
必定是ExecutorService
是接口的某个实现类。
ExecutorService service = Executors.newFixedThreadPool(10);
- 通过以下代码获取
service
的类:
System.out.println(service.getClass());
输出:
class java.util.concurrent.ThreadPoolExecutor
可以看到,ThreadPoolExecutor
是 ExecutorService
是接口的实现类:
-
因此,就可以通过强转成
ThreadPoolExecutor
类的对象,来//设置线程池的属性://强转 ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
4. 例子
class NumThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() +
":" + i);
}
}
}
}
//创建一个Callable的实现类
class NumThread2 implements Callable {
//实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() +
":" + i);
sum += i;
}
}
return sum;//包装类Integer自动装箱
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2.执行指定的线程操作,需要提供实现Runnable或者Callable接口实现类的对象
service.execute(new NumThread1());//执行Runnable接口实现类对象的线程
Future future = service.submit(new NumThread2());//执行Callable接口实现类对象的线程
try {
//获取Callable中call方法的返回值
Object sum = future.get();
System.out.println("sum = " + sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//3.关闭线程池
service.shutdown();
}
}
输出:
pool-1-thread-1:0
pool-1-thread-1:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-1:8
pool-1-thread-1:10
pool-1-thread-1:12
pool-1-thread-1:14
pool-1-thread-1:16
pool-1-thread-1:18
pool-1-thread-2:1
pool-1-thread-2:3
pool-1-thread-2:5
pool-1-thread-2:7
pool-1-thread-2:9
pool-1-thread-2:11
pool-1-thread-2:13
pool-1-thread-2:15
pool-1-thread-2:17
pool-1-thread-2:19
sum = 100