多线程

基本概念

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念

进程是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位

通常在一个进程中可以包含多个线程当然一个进程中至少有一个线程,不然没有存在的意义。

线程是CPU调度和执行的单位

注意:很多多线程是模拟出来的,真正的多线程是指由多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉

本章核心概念

  • 线程是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc(garbage collection)线程;
  • main()称之为主线程,为系统的入口,用于执行整个程序

Java程序都是以main()作为入口的。main函数是一个线程(主线程),同时还是一个进程。在现在的操作系统中,都是多线程的。但是它执行的时候,对外来说就是一个独立的进程。这个进程,可以包含多个线程,也可以只包含一个线程

主线程的重要性体现在两个方面:

  1. 是产生其他子线程的线程
  2. 通常它必须最后完成执行,比如执行各种关闭操作

线程之间的关系、执行关系如图示:

image-20210826172535380
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

继承Thread类

1
2
3
public class Thread
extends Object
implements Runnable

线程是程序中的执行线程。Java虚拟机允许应用程序同时运行多个执行线程

每个线程都有优先级,CPU按优先级调度执行线程

当Java虚拟机启动时,通常有一个非守护进程线程(通常是调用某些指定类的名为main的方法)。Java虚拟机将继续执行线程,直到发生以下任一情况

  • 已经调用了Runtime类的exit方法,并且安全管理器已经允许进行退出操作
  • 所有不是守护进程线程的线程已经死亡,无论是从调用返回到run方法还是抛出超出run方法的run。

创建一个新的执行线程有两种方法:

  • 一个是将一个类声明为Thread的子类。这个子类应该重写run类的方法Thread。然后可以分配并启动子类的实例。

  • 另一种方法来创建一个线程是声明实现类Runnable接口。那个类然后实现了run方法。然后可以分配类的实例,在创建Thread时作为参数传递,并启动。

创建线程实例

法一:继承自Thread类

步骤:

  • 自定义线程类继承Thread类
  • 重写**run()**方法
  • 创建线程对象,调用**start()**方法启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建线程方式1:继承Thread类,重写run方法,调用start开启线程
public class TestThread1 extends Thread {
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("我在看代码-----");
}
}
public static void main(String[] args) {
// main线程,主线程

// 创建一个线程对象,CPU按优先级调度
TestThread1 testThread1 = new TestThread1();
// 调用start()方法开启线程,执行线程的run函数
testThread1.start();

for (int i = 0; i < 200; i++) {
System.out.println("我在学习多线程-----");
}
}
}

执行结果:

image-20210826155918945

执行时,线程和线程同时执行,CPU按优先级调度

总结:注意,线程开启不一定立即执行,由CPU调度执行

法二:实现类Runnable接口

步骤:

  • 定义类实现Runnable接口
  • **实现run()**方法,编写线程执行体
  • 创建线程对象,调用**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
// 创建线程方式2:实现Runnable接口,重写run方法,执行线程需要丢入Runnable接口实现类,调用start方法
public class TestThread3 implements Runnable {
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("我在看代码-----" + i);
}
}

public static void main(String[] args) {
// main线程,主线程

// 创建一个Runnable接口的实现类对象
TestThread3 testThread3 = new TestThread3();

// 类似于静态代理模式
new Thread(testThread3).start();

for (int i = 0; i < 200; i++) {
System.out.println("我在学习多线程-----" + i);
}
}
}

执行结果

image-20210826181206428
  • 实现接口Runnable具有多线程能力

  • 启动线程:传入目标对象+Thread对象.start()

  • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

比较两种方法

  • 继承Thread类

    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口

    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread对象.start()
    • 推荐使用避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
    image-20210826181558650

法三:实现Callable接口(暂时了解即可)

步骤:

  • 定义类实现Callable接口
  • **实现call()**方法,编写线程执行体
  • 创建执行服务->提交执行->获取结果->关闭服务
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
import java.util.concurrent.*;

// 线程创建方式3:实现Callable接口
/*
* Callable的好处:
* 1. 可以定义返回值
* 2. 可以抛出异常
* */
public class TestCallable implements Callable<Boolean> {
private String url; // 图片的网络地址
private String name; // 图片的文件名

public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}

// 下载图片的执行体
@Override
public Boolean call() throws Exception {
WebDownLoader webDownLoader = new WebDownLoader();
webDownLoader.downLoader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fuploadfile.bizhizu" +
".cn%2Fup%2Ff1%2Fcd%2F63%2Ff1cd63164d1ff922c286ff631cb22f9b.jpg.source" +
".jpg&refer=http%3A%2F%2Fuploadfile.bizhizu.cn&app=2002&size=f9999," +
"10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1632627419&t=05c8225e2a04de881deff50946f3fa06", "1.jpg");
TestCallable t2 = new TestCallable("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fdik.img.kttpdq" +
".com%2Fpic%2F75%2F52402%2F6edaab3c79f9906c.jpg&refer=http%3A%2F%2Fdik.img.kttpdq" +
".com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1632627419&t" +
"=da0fd2d2933805dfd24439813eaced44", "2.jpg");
TestCallable t3 = new TestCallable("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fk.zol-img.com" +
".cn%2Fnbbbs%2F7336%2Fa7335241_s.jpg&refer=http%3A%2F%2Fk.zol-img.com.cn&app=2002&size=f9999," +
"10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1632627419&t=81b1925f8461c1f782fab729e144d479", "3.jpg");

// 创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(3);

// 提交执行
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);

// 获取结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();

System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);

// 关闭服务
ser.shutdown();
}
}

使用Callable好处:

  1. 可以定义返回值
  2. 可以抛出异常

线程中变量的作用域和访问空间

image-20210901101316366

线程五个状态

  • 创建:new一个线程
  • 就绪:线程start()进入就绪
  • 运行:CPU调度执行
  • 阻塞:线程执行全被抢夺
  • 死亡:线程执行完毕,销亡

注意:死亡之后的线程不可以再次start,因为一个线程不可以启动两次

线程同步机制

线程同步方法

关键字:synchronized

关键字放在方法上面,默认锁住的是this指代的对象

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
class BuyTicket implements Runnable {
//票
private int ticketNums = 10;
boolean flag = true; //外部停止方式

@Override
public void run() {
//买票
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

// synchronized 实现同步,锁的是this
public synchronized void buy() throws InterruptedException {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName() + "拿到了" + ticketNums--);
}
}

上面的java代码中,锁住的是BuyTicket类型的对象,因为后面需要修改该对象的ticketNums属性值

image-20210908111419823

线程同步块

需要同步操作的对象放在参数列表中

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

import java.util.ArrayList;
import java.util.List;

//使用到了同步块
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();


for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}

Thread.sleep(3000);

System.out.println(list.size());
}
}

上面的代码中,list为需要进行同步操作的变量,所以放到了同步块中,以进行同步控制。

image-20210908111454528

线程死锁

同步机制中可能会产生死锁,所以需要避免死锁问题

死锁产生的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

死锁解决方法

只要破坏上面四个条件中的任意一个就行

Lock接口

ReentratLock实现类(可重入锁)

image-20210908143622156

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package syn.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();

new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}

class TestLock2 implements Runnable {
int tickNums = 10;

//定义lock锁
private final ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
try {
lock.lock(); //加锁
if (tickNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tickNums--);
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}
}
}

生产者消费者问题

管程法

信号灯法:

线程池

线程池的作用

提前创建好多个线程,放入线程池中,使用时直接获取,使用完返回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

线程池API

  • JDK5提供了线程池相关的API:ExecutorServiceExecutors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
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 threadsPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//测试线程池
public class TestPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
//newFixedThreadPool 参数为:线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);

//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());

//2.关闭连接
service.shutdown();
}
}

class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

image-20210909155310355

Contents
  1. 1. 基本概念
  2. 2. 本章核心概念
  3. 3. 继承Thread类
  4. 4. 创建线程实例
    1. 4.1. 法一:继承自Thread类
    2. 4.2. 法二:实现类Runnable接口
    3. 4.3. 比较两种方法
    4. 4.4. 法三:实现Callable接口(暂时了解即可)
  5. 5. 线程中变量的作用域和访问空间
  6. 6. 线程五个状态
  7. 7. 线程同步机制
    1. 7.1. 线程同步方法
    2. 7.2. 线程同步块
    3. 7.3. 线程死锁
      1. 7.3.1. 死锁产生的四个必要条件
      2. 7.3.2. 死锁解决方法
    4. 7.4. Lock接口
      1. 7.4.1. ReentratLock实现类(可重入锁)
    5. 7.5. 生产者消费者问题
      1. 7.5.1. 管程法
      2. 7.5.2. 信号灯法:
  8. 8. 线程池
    1. 8.1. 线程池的作用
    2. 8.2. 线程池API
|