多线程
概述
并行与并发
- 并行:指 两个 或 多个 事件在 同一时刻 发生(同时发生)
- 并发:指 两个 或 多个 事件在 同一个时间段 内发生(交替执行)
线程与进程
# 进程
是指一个 内存中运行的应用程序,每个进程 都有一个独立的内存空间,一个应用程序可以同时运行多 个进程,进程也是 程序的一次执行过程
,是系统 运行程序的基本单位
系统运行一个程序即是 一个进程从创建、运行 到 消亡的过程
# 线程
进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,一个进程中是可 以有多个线程的,这个应用程序也可以称之为 多线程程序
# 进程与线程的区别
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多
线程
线程类
Java 使用 java.lang.Thread
类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例
每个线程的作用是 完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码
Java 使用 线程执行体
来代表这段程序流
创建并启动多线程
- 定义 Thread类 的子类,并 重写 该类的 run() 方法,该 run() 方法的方法体就代表了线程需要完成的任务,因此把 run() 方法称为线程执行体
- 创建 Thread 子类的实例,即创建了线程对象
- 调用线程对象的
start()
方法来启动该线程
示例
自定义线程类
/**
* @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);
}
}
}
执行线程
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() | 返回对当前正在执行的线程对象的引用 |
获取线程名称
- 可以使用 Thread类 中的方法
getName
,String getName() 返回该线程的名称 - 可以先获取当前正在执行的线程,在通过 getName方法 获取线程名称,static Thread currentThread() 返回对当前正在执行的线程对象的引用
/**
* @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);
}
}
}
设置线程名称
- 可以使用 Thread类 中的方法
setName
,void setName(String name) 改变线程名称,使之与参数 name 相同
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);
}
}
- 添加一个带参构造方法,参数传递线程的名称,调用父类的带参构造方法,把名字 传递给父类,让父亲给儿子起名字,Thread(String name) 分配新的 Thread 对象
/**
* 定义指定线程名称的构造方法
*
* @param name
*/
public MyThread(String name) {
super(name);
}
// 在实例化的时候指定线程名称
MyThread myThread = new MyThread("MyThread");
# public static void sleep(long millis)
使当前 正在执行的线程 以指定的 毫秒数 暂停
(暂时停止执行)睡醒了,继续执行
/**
* 覆盖 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 对象 |
实现步骤
- 创建一个类实现 Runnable 接口
- 重写 Runnable接口中的
run方法
,设置线程任务 - 创建 Runnable接口的实现类对象
- 创建 Thread类对象,构造方法中传递 Runnable接口的实现类对象
- 调用 Thread类中的 start方法,开启新的线程,执行run方法
/**
* @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接口优势
- 避免 单继承的局限性,一个类继承了 Thread类就不能继承其他的类,一个类实现了 Runnable接口,还可以继续继承别的类,实现其他的接口
- 增强了程序的扩展性,降低程序的耦合度,使用 Runnable接口把设置线程任务和开启线程相分离,实现类当中,重写 run方法,设置线程任务,创建 Thread类对象,调用 start方法,开启新线程
如果一个类继承 Thread,则不适合资源共享,但是如果实现了 Runnable接口的话,则很容易的实现资源共享
匿名内部类实现多线程
匿名内部类
# 作用:把子类继承父类,重写父类的方法,创建子类对象,合成一步完成,把实现类实现接口,重写接口库的方法,创建实现类对象,合成一步完成,最终得要子类对象或实现类对象
# 格式
new 父类 / 接口 () {
重写父类/接口中的方法
};
Thread
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
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();
}
线程安全
什么是线程安全
多线程访问了共享的数据,就会产生线程的安全
举例:多个窗口,同时卖一种票,如果不进行控制,可能会出现卖重复的现象
代码实现
卖票线程
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--;
}
}
}
开启多线程同时执行
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 (锁对象) {
出现安全问题的代码 (访问了共享数据的代码)
}
注意
- 锁对象可以是任意对象 new Person new Student ...
- 必须保证多个线程使用的是同一个锁对象
- 锁对象的作用: 把 {} 中代码锁住,只让一个线程进去执行
示例
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 返回值类型 方法名 (参数列表) {
出现安全问题的代码 (访问了共享数据的代码)
}
使用步骤
- 创建一个方法,方法的修饰符添加上
synchronized
- 把访问了 共享数据 的代码放入到方法中
- 调用同步方法
同步方法
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
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--;
}
}
}
静态同步方法
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)
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() | 释放锁 |
使用步骤
- 在成员位置创建一个 Lock接口的实现类对象 ReentrantLock
- 在可能会出现安全问题的代码前,调用 lock方法获取锁对象
- 在可能会出现安全问题的代码后,调用 unlock方法释放锁对象
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 方法
代码实现
顾客线程
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();
老板线程
new Thread() {
@Override
public void run() {
synchronized (obj) {
try {
Thread.sleep(3000);
System.out.println("老板饺子已经做好");
obj.notify();// 唤醒当前锁对象上的等待线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
进入计时等待状态的两种方式
- 使用 sleep(long m)方法,在毫秒值结束后,线程睡醒,进入 Runnable/Blocked状态
- 使用 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对象的重复利用,也就减少了创建线程和销毁线程所消耗的资源
合理利用线程池能够带来三个好处
- 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线 线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
Executors类
作用
在 JDK1.5 的时候 Java提供了线程池
java.util.concurrent.Executors类:线程池的工厂类,用来生产线程池
方法
方法名 | 描述 |
---|---|
static ExecutorService newFixedThreadPool(int nThreads) | 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程 int nThreads:创建线程池中线程的个数 |
submit(Runnable task) | 提交一个 Runnable 任务用于执行 |
oid shutdown() | 用于销毁线程池,一般不建议使用 注意:线程池销毁之后,就在内存中消失了,就不能在执行线程任务了 |
使用步骤
- 使用线程池工厂类 Executors提供的静态方法 newFixedThreadPool生产一个指定线程数量的线程池
- 调用线程池 ExecutorService中的方法 submit,传递线程任务,执行线程任务
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执行了!");
}
});
}