0%

多线程

概述

并行与并发

  • 并行:指 两个 或 多个 事件在 同一时刻 发生(同时发生)
  • 并发:指 两个 或 多个 事件在 同一个时间段 内发生(交替执行)

线程与进程

# 进程

是指一个 内存中运行的应用程序,每个进程 都有一个独立的内存空间,一个应用程序可以同时运行多 个进程,进程也是 程序的一次执行过程,是系统 运行程序的基本单位
系统运行一个程序即是 一个进程从创建、运行 到 消亡的过程

# 线程

进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,一个进程中是可 以有多个线程的,这个应用程序也可以称之为 多线程程序

# 进程与线程的区别

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多

线程

线程类

Java 使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例
每个线程的作用是 完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码
Java 使用 线程执行体 来代表这段程序流

创建并启动多线程

  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
/**
* @Author jonath_yh
* @Date 2020-06-30 11:24
* @Version 1.0
**/
public class MyThread extends Thread {

/**
* 定义指定线程名称的构造方法
*
* @param name
*/
public MyThread(String name) {
super(name);
}

/**
* 覆盖 run 方法
*/
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + "正在执行" + i);
}
}
}

执行线程

1
2
3
4
5
6
7
public static void main(String[] args) {
MyThread myThread = new MyThread("MyThread");
myThread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);
}
}

运行时序图

执行过程

程序启动运行 main 时候,Java虚拟机启动一个进程,主线程 main 在 main() 调用时候被创建
随着调用 mt的对象的 start 方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行

内存结构

Thread类

构造方法

方法名 描述
public Thread() 分配一个新的线程对象
public Thread(String name) 分配一个指定名字的新的线程对象
public Thread(Runnable target) 分配一个带有指定目标新的线程对象
public Thread(Runnable target,String name) 分配一个带有指定目标新的线程对象并指定名字

常用方法

方法名 描述
public String getName() 获取当前线程名称
public void start() 导致此线程开始执行,Java虚拟机调用此线程的 run 方法
public void run() 此线程要执行的任务在此处定义代码
public static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
public static Thread currentThread() 返回对当前正在执行的线程对象的引用

获取线程名称

  1. 可以使用 Thread类 中的方法 getName,String getName() 返回该线程的名称
  2. 可以先获取当前正在执行的线程,在通过 getName方法 获取线程名称,static Thread currentThread() 返回对当前正在执行的线程对象的引用
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
/**
* @Author jonath_yh
* @Date 2020-06-30 11:24
* @Version 1.0
**/
public class MyThread extends Thread {

/**
* 定义指定线程名称的构造方法
*
* @param name
*/
public MyThread(String name) {
super(name);
}

/**
* 覆盖 run 方法
*/
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + "正在执行" + i);

// 1. 可以使用Thread类中的方法getName
String name = getName();
System.out.println(name);// 创建时, 指定了名称,获取的就是指定的名称
// 如果没有指定名称,获取的就是Thread-0

// 2. 可以先获取当前正在执行的线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread);// Thread[Thread-0,5,main]

String name2 = currentThread.getName();
System.out.println(name2);// Thread-0
}
}

public static void main(String[] args) {
MyThread myThread = new MyThread("MyThread");
myThread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);
}
}
}

设置线程名称

  1. 可以使用 Thread类 中的方法 setName,void setName(String name) 改变线程名称,使之与参数 name 相同
1
2
3
4
5
6
7
8
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("myThreadName");
myThread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);
}
}
  1. 添加一个带参构造方法,参数传递线程的名称,调用父类的带参构造方法,把名字 传递给父类,让父亲给儿子起名字,Thread(String name) 分配新的 Thread 对象
1
2
3
4
5
6
7
8
9
10
11
/**
* 定义指定线程名称的构造方法
*
* @param name
*/
public MyThread(String name) {
super(name);
}

// 在实例化的时候指定线程名称
MyThread myThread = new MyThread("MyThread");

# public static void sleep(long millis)

使当前 正在执行的线程 以指定的 毫秒数 暂停(暂时停止执行)睡醒了,继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 覆盖 run 方法
*/
@Override
public void run() {
/* 程序在执行第二秒时, 会暂停2秒,2秒后,继续执行后面程序 */
for (int i = 1; i <= 60; i++) {
System.out.println(i);
/* 让程序睡眠1秒钟 1秒=1000毫秒 */
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Runnable接口

作用

多线程 程序的第二种实现方式

构造方法

方法名 描述
Thread(Runnable target) 分配新的 Thread 对象
Thread(Runnable target, String name) 分配新的 Thread 对象

实现步骤

  1. 创建一个类实现 Runnable 接口
  2. 重写 Runnable接口中的 run方法,设置线程任务
  3. 创建 Runnable接口的实现类对象
  4. 创建 Thread类对象,构造方法中传递 Runnable接口的实现类对象
  5. 调用 Thread类中的 start方法,开启新的线程,执行run方法
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
/**
* @Author jonath_yh
* @Date 2020-06-30 11:24
* @Version 1.0
**/
// 1. 创建一个类实现 Runnable 接口
public class RunnableImpl implements Runnable {
// 2. 重写Runnable接口中的run方法,设置线程任务
@Override
public void run() {
// 新线程执行的代码
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
}
}

public static void main(String[] args) {
// 3.创建Runnable接口的实现类对象
RunnableImpl r = new RunnableImpl();
// 4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t = new Thread(r);// 打印20次i
// 5.调用Thread类中的start方法,开启新的线程,执行run方法
t.start();

// 主线程开启新线程之后继续执行的代码
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
}
}
}

使用Runnable接口优势

  1. 避免 单继承的局限性,一个类继承了 Thread类就不能继承其他的类,一个类实现了 Runnable接口,还可以继续继承别的类,实现其他的接口
  2. 增强了程序的扩展性,降低程序的耦合度,使用 Runnable接口把设置线程任务和开启线程相分离,实现类当中,重写 run方法,设置线程任务,创建 Thread类对象,调用 start方法,开启新线程

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

匿名内部类实现多线程

匿名内部类

# 作用:把子类继承父类,重写父类的方法,创建子类对象,合成一步完成,把实现类实现接口,重写接口库的方法,创建实现类对象,合成一步完成,最终得要子类对象或实现类对象

# 格式

new 父类 / 接口 () {
  重写父类/接口中的方法
};

Thread

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
new Thread() { // new 没有名称的类 继承Thread
// 重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}.start();
}

Runnable

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
new Thread(new Runnable() { // new没有名称的类实现了Runnable接口
// 重写run方法,设置线程任务
@Override
public void run() { // 实现接口当中run方法
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}).start();
}

线程安全

什么是线程安全

多线程访问了共享的数据,就会产生线程的安全

举例:多个窗口,同时卖一种票,如果不进行控制,可能会出现卖重复的现象

代码实现

卖票线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TicketRunnableImpl implements Runnable {
// 定义共享的票源
private int ticket = 100;

// 线程任务: 卖票
@Override
public void run() {
while (ticket > 0) {
/* 为了提高线程安全问题出现的几率
让线程睡眠10毫秒,放弃cpu的执行权 */
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

开启多线程同时执行

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TicketRunnableImpl r = new TicketRunnableImpl();

// 创建3个线程
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
// 开启新的线程
t0.start();
t1.start();
t2.start();
}

同步代码块synchronized解决线程安全

格式

synchronized (锁对象) {
  出现安全问题的代码 (访问了共享数据的代码)
}

注意

  1. 锁对象可以是任意对象 new Person new Student …
  2. 必须保证多个线程使用的是同一个锁对象
  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
public class TicketRunnableImpl implements Runnable {
// 定义共享的票源
private int ticket = 100;
private Object obj = new Object(); // 锁对象

// 线程任务: 卖票
@Override
public void run() {
synchronized (obj) {
while (ticket > 0) {
/* 为了提高线程安全问题出现的几率
让线程睡眠10毫秒,放弃cpu的执行权 */
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}
}

再次抢票就不会再有重复票的情况了

总结

同步中的线程,没有执行完毕,不会释放锁对象,同步外的线程没有锁对象进不去同步代码块当中,当没有锁对象时,进入阻塞状态,一直等待,出了同步后,会把锁对象归还,同步保证了只能有一个线程在同步 中执行共享数据,保存了安全,但是程序频繁的判断锁,释放锁,程序的效率会降低

同步方法解决线程安全

格式

修饰符 synchronized 返回值类型 方法名 (参数列表) {
  出现安全问题的代码 (访问了共享数据的代码)
}

使用步骤

  1. 创建一个方法,方法的修饰符添加上 synchronized
  2. 把访问了 共享数据 的代码放入到方法中
  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
public class TicketRunnableImpl implements Runnable {
// 定义共享的票源
private int ticket = 100;
private Object obj = new Object(); // 锁对象

// 线程任务: 卖票
@Override
public void run() {
ticketMethods();
}

public synchronized void ticketMethods() {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

锁对象是谁:锁对象为 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void ticketMethods() {
synchronized (this) {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

静态同步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TicketRunnableImpl implements Runnable {
// 定义共享的票源
private static int ticket = 100;
private Object obj = new Object(); // 锁对象

// 线程任务: 卖票
@Override
public void run() {
ticketMethods();
}

public static synchronized void ticketMethods() {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void ticketMethods() {
synchronized (TicketRunnableImpl.class) {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

使用Lock锁解决线程安全

概述

java.util.concurrent.locks.Lock 接口,是 JDK1.5 之后的新特性,Lock 实现提供了 比使用 synchronized 方法和语句可获得的更广泛的锁定操作

Lock接口中的方法

方法名 描述
void lock() 获取锁
void unlock() 释放锁

使用步骤

  1. 在成员位置创建一个 Lock接口的实现类对象 ReentrantLock
  2. 在可能会出现安全问题的代码前,调用 lock方法获取锁对象
  3. 在可能会出现安全问题的代码后,调用 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
public class TicketRunnableImpl implements Runnable {
// 定义共享的票源
private int ticket = 100;
// 1. 在成员位置创建一个Lock接口的实现类对象ReentrantLock
Lock l = new ReentrantLock();

// 线程任务: 卖票
@Override
public void run() {
while (true) {
l.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 在可能会出现安全问题的代码后,调用unlock方法释放锁对象
l.unlock(); // 无论程序是否异常,都会把锁对象释放,节约内存提高程序的效率
}
}
}
}
}

线程状态

六种线程状态

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

流程图

等待与唤醒

  • public void wait(): 让当前线程进入到等待状态,此方法必须 锁对象 调用
  • public void notify(): 唤醒 当前锁 对象上等待状态的线程 此方法必须锁对象调用,会继续执行wait()方法之后的代码

需求

顾客与老板线程

  • 创建一个顾客线程 (消息者) 告诉老板要吃什么,调用 wait方法,放弃cpu的执行,进入 wating状态 (无限等待)
  • 创建一个老板线程 (生产者) 花 5秒做好,做好后,调用 notify方法,唤醒顾客,开吃

注意

顾客与老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行,同步使用的锁必须要保证唯一,只有锁对象才能调用 wait 和 notify 方法

代码实现

顾客线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object obj = new Object();
new Thread() {
@Override
public void run() {
synchronized (obj) {
System.out.println("告诉老板要吃饺子");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("做好===开始吃饺子");
}
}
}.start();

老板线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Thread() {
@Override
public void run() {
synchronized (obj) {
try {
Thread.sleep(3000);
System.out.println("老板饺子已经做好");
obj.notify();// 唤醒当前锁对象上的等待线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();

进入计时等待状态的两种方式

  1. 使用 sleep(long m)方法,在毫秒值结束后,线程睡醒,进入 Runnable/Blocked状态
  2. 使用 wait(long m)方法 wait方法如果在毫秒值结束之后,还没有被唤醒,就会自动醒来,进入 Runnable/Blocked状态

两种唤醒的方法

方法名 描述
public void notify() 随机唤醒1个
public void notifyall() 唤醒锁对象上所有等待的线程

线程池

存在问题

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

线程池

有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务,在Java中可以通过 线程池 来达到这样的效果
线程池其实就是一个 容纳多个线程 的容器,其中的线程可以反复使用,省去了频繁 创建线程对象 的操作,无需反复创建线程而消耗过多资源

线程池的简要工作模型

解释

线程池的工作模型主要两部分组成,一部分是运行 Runnable的Thread对象,另一部分就是阻塞队列,由线程池
创建的 Thread对象其内部的 run方法 会通过阻塞队列的 take方法 获取一个 Runnable对象,然后执行这个
Runnable对象的 run方法,在 Thread的 run方法中调用 Runnable对象的 run方法,当Runnable对象的run方法
执行完毕以后,Thread中的run方法又循环的从阻塞队列中获取下一个 Runnable对象继续执行,这样就实现了
Thread对象的重复利用,也就减少了创建线程和销毁线程所消耗的资源

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

  1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线 线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

Executors类

作用

在 JDK1.5 的时候 Java提供了线程池
java.util.concurrent.Executors类:线程池的工厂类,用来生产线程池

方法

方法名 描述
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程
int nThreads:创建线程池中线程的个数
submit(Runnable task) 提交一个 Runnable 任务用于执行
oid shutdown() 用于销毁线程池,一般不建议使用
注意:线程池销毁之后,就在内存中消失了,就不能在执行线程任务了

使用步骤

  1. 使用线程池工厂类 Executors提供的静态方法 newFixedThreadPool生产一个指定线程数量的线程池
  2. 调用线程池 ExecutorService中的方法 submit,传递线程任务,执行线程任务
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
public static void main(String[] args) {
// 1.使用线程池工厂类Executors提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
ExecutorService ex = Executors.newFixedThreadPool(2);
// 2.调用线程池ExecutorService中的方法submit,传递线程任务,执行线程任务
// 相当于new Thread(new Runnable(){}).start();
ex.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程任务1执行了!");
}
});
ex.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程任务2执行了!");
}
});
ex.shutdown();// 销毁线程比
ex.submit(new Runnable() { // 会报错
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程任务3执行了!");
}
});
}
↓赏一个鸡腿... 要不,半个也行↓