概述
并行与并发
- 并行:指 两个 或 多个 事件在 同一时刻 发生(同时发生)
- 并发:指 两个 或 多个 事件在 同一个时间段 内发生(交替执行)
线程与进程
# 进程
是指一个 内存中运行的应用程序,每个进程 都有一个独立的内存空间,一个应用程序可以同时运行多 个进程,进程也是 程序的一次执行过程
,是系统 运行程序的基本单位
系统运行一个程序即是 一个进程从创建、运行 到 消亡的过程
# 线程
进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,一个进程中是可 以有多个线程的,这个应用程序也可以称之为 多线程程序
# 进程与线程的区别
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多
线程
线程类
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执行了!"); } }); }