多线程
一、多线程
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
并发和并行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
进程和线程
- 进程:是正在运行的程序
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
二、实现多线程的方式
继承Thread类
方法介绍
方法名 | 说明 |
---|---|
void run() | 在线程开启后,此方法将被调用执行 |
void start() | 使此线程开始执行,Java虚拟机会调用run方法() |
实现步骤
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
代码演示:
public class Mythread extends Thread {
//封装被线程执行的代码
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程执行"+i);
}
}
}
public class text {
public static void main(String[] args) {
//开启一个线程
Mythread mythread = new Mythread();
//开始第二个线程
Mythread mythread1 = new Mythread();
//run():封装线程执行的代码,直接调用,相当于普通方法的调用
//start():启动线程;然后由JVM调用此线程的run()方法
mythread.start();
mythread1.start();
}
}
每次的结果不同,代表具有随机性,线程是由CPU执行的,是由CPU说了算
两个小问题
-
为什么要重写run()方法?
因为run()是用来封装被线程执行的代码
-
run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run()方法
实现Runnable接口
Thread的构造方法
方法名 | 说明 |
---|---|
Thread(Runnable target) | 分配一个新的Thread对象 |
Thread(Runnable target, String name) | 分配一个新的Thread对象 |
实现步骤
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
代码演示:
//实现了Runnable接口的类是没有start方法,该类对象只是作为Thread的参数
public class Mythread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"线程执行"+i);
}
}
}
public static void main(String[] args) {
Mythread mythread = new Mythread();
//每new一次Thread对象就是开启一个线程
Thread t=new Thread(mythread,"A");
Thread t1=new Thread(mythread,"B");
t.start();
t1.start();
}
}
实现Callable接口
方法介绍
方法名 | 说明 |
---|---|
V call() | 计算结果,如果无法计算结果,则抛出一个异常 |
FutureTask(Callable callable) | 创建一个 FutureTask,一旦运行就执行给定的 Callable |
V get() | 如有必要,等待计算完成,然后获取其结果 |
实现步骤 |
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
代码演示:
如果get方法放在线程执行之前,get方法就会死等
public class Mythread implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("线程执行"+i);
}
return "执行完成";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread mythread=new Mythread();
//执行完毕之后可以获取执行后的结果
FutureTask<String> stringFuture = new FutureTask<>(mythread);
//创建线程对象,开启线程
Thread t=new Thread(stringFuture);
t.start();
String s = stringFuture.get();// 如果线程还没有开启,stringFuture.get()放在线程开启之前,get方法就会死等
System.out.println(s);
}
}
三种方式的比较
三种实现方式的对比
- 实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
三种方式的选择
- 不需要扩展性,直接继承Thread类
- 如果需要线程执行之后的返回值使用Callable接口
- 需要扩展,但不需要返回值使用Runnable接口
线程类(Thread)的常见方法
设置和获取线程名称
方法名 | 说明 |
---|---|
void setName(String name) | 将此线程的名称更改为等于参数name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
- getName():
获取线程的名字,线程是有默认的名称的 Thread-编号 - setName()或者通过Thread的构造方法
t.setName(“名称”)
要在类中重写Thread的空参和传递name的构造方法 - currentThread()
返回当前正在执行的线程对象
线程休眠
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
当继承或实现的父类没有抛出异常的时候,子类只能自己处理异常 | |
一个类如果实现了抽象类或者接口中的抽象方法,那么实现该方法能否抛出异常由抽象方法决定。 | |
因为在父类中的该方法没有抛出异常,所以这里也不可以抛出异常 |
- 方法重写的时候,如果父类没有抛出任何异常,那么子类只可以抛出运行时异常,不可以抛出编译时异常。
- 如果父类的方法抛出了一个异常,那么子类在方法重写的时候不能抛出比被重写方法申明更加宽泛的编译时异常。
- 子类重写方法的时候可以随时抛出运行时异常,包括空指针异常,数组越界异常等。
线程优先级
线程调度
-
两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
-
Java使用的是抢占式调度模型
-
随机性
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
优先级相关的方法
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10 |
守护线程/后台线程
当普通线程执行完之后,守护线程就不在执行,但不会立马停止
相关方法
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
thread.setDaemon(true);
线程的安全问题(同步代码块、同步方法、lock锁)
1、有多线程环境
2、有共享数据
3、有多线程共享数据
案例需求
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
实现步骤
-
定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
-
在SellTicket类中重写run()方法实现卖票,代码步骤如下
-
判断票数大于0,就卖票,并告知是哪个窗口卖的
-
卖了票之后,总票数要减1
-
票卖没了,线程停止
-
定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
-
创建SellTicket类的对象
-
创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
-
启动线程
代码实现:
public class Mythread implements Runnable {
private int num=100;
@Override
public void run() {
while(true){
if(num<=0)
break;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"在卖票,还剩"+num+"张");
num--;
}
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread m=new Mythread();
Thread thread=new Thread(m);
Thread thread1=new Thread(m);
thread1.setName("窗口1");
thread.setName("窗口2");
thread.start();
thread1.start();
}
}
结果发现:
-
相同的票出现了多次
-
出现了负数的票
问题产生原因
- 线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题。因为在卖票的过程中,多个线程操作了共享数据
同步代码块解决数据安全问题
-
安全问题出现的条件
-
是多线程环境
-
有共享数据
-
有多条语句操作共享数据
-
-
如何解决多线程安全问题呢?
- 基本思想:让程序没有安全问题的环境
-
怎么实现呢?
-
把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
-
Java提供了同步代码块的方式来解决
-
同步代码块格式:
-
synchronized(任意对象) {
多条语句操作共享数据的代码
}
任意对象:多线程调用对象必须唯一
代码演示:
public class SellTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
//t1进来后,就会把这段代码给锁起来
if (tickets > 0) {
try {
Thread.sleep(100);
//t1休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//窗口1正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--; //tickets = 99;
}
}
//t1出来了,这段代码的锁就被释放了
}
}
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁。只有当进去的线程执行完出来之后,才会解锁
同步的好处和弊端
-
好处:解决了多线程的数据安全问题
-
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步方法解决数据安全问题
同步方法的格式
同步方法:就是把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步方法的锁对象是什么呢?
- this
静态同步方法
同步静态方法:
- 就是把synchronized关键字加到静态方法上
同步静态方法的锁对象是什么呢?
- 类名.class(为什么不是this,是因为静态方法中没有this,静态方法是在类加载的时候加载到方法区中的静态区中(jdk8之前),那时还没有对象生成)
使用同步静态方法代码演示:
public class Mythread implements Runnable {
private static int num=100;
@Override
public void run() {
while (true){
boolean b=false;
if ("窗口1".equals(Thread.currentThread().getName())) {
b = TicketSa();
}
if ("窗口2".equals(Thread.currentThread().getName())) {
b = TicketSa();
}
if(b)
break;
}
}
private synchronized static boolean TicketSa() {
if (num == 0)
return true;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
System.out.println(Thread.currentThread().getName() + "卖出第" + num + "张票");
return false;
}
}
lock锁(jdk5之后)
手动加、释放锁
- lock:获得锁
- unlock:释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
- 注意
- 当使用lock锁时,在处理sleep的异常时,要把释放锁放在finally里面,让释放锁的动作一定执行
代码演示:
public class testThread implements Runnable{
private int num=100;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
try {
if(num==0)
break;
Thread.sleep(100);
num--;
System.out.println(Thread.currentThread().getName()+"再买票"+num);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
死锁
-
概述
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
-
什么情况下会产生死锁
- 资源有限
- 同步嵌套
等待和唤醒方法
为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中
Object类的等待和唤醒方法:
注意
锁对象是谁,就用谁调用等待和唤醒方法
多线程调用共享元素
套路:
- while(true)死循环
- synchronized 锁对象一定要唯一
- 判断共享数据是否结束
线程的状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程
状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
Java中的线程存在6种状态
线程状态 | 具体含义 |
---|---|
NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。 |
RUNNABLE | 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。 |
TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
线程池
- 系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就
会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。
使用线程池的好处:
- 降低资源的消耗:通过重复使用现有的线程,避免多次的创建和销毁线程
- 提高响应速度:省略了创建线程的过程,拿到任务可以立即执行
- 提供一些附加功能:新功能:定时、延时执行等
线程池的代码实现(Executors,使用JVM创建的线程池)
概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。
我们可以使用Executors中所提供的静态方法来创建线程池
static ExecutorService newCachedThreadPool() 创建一个默认的线程池
static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池
//创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
ExecutorService executorService = Executors.newCachedThreadPool();
//Executors --- 可以帮助我们创建线程池对象
//ExecutorService --- 可以帮助我们控制线程池(线程池控制者对象)
//提交任务
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
//这个时候用的是 同一个线程
Thread.sleep(2000);
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
//停止线程池
executorService.shutdown();
- static ExecutorService newCachedThreadPool() 创建一个默认的线程池
- static ExecutorService newFixedThreadPool(int nThreads) : 创建一个指定最多线程数量的线程池
nThreads能被拥有的线程最大数是 nThreads
线程池-ThreadPoolExecutor(自己创建)
创建线程池对象 :
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
// 参数一:核心线程数量(不能被销毁)
// 参数二:最大线程数
// 参数三:空闲线程最大存活时间
// 参数四:时间单位(TimeUnit(Time工具类).SECONDS)
// 参数五:任务队列
// 参数六:创建线程工厂
// 参数七:任务的拒绝策略
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//创建一个类,继承Thread,重写run
pool.submit(()->sout(Thread.currentThread().getName()+"执行"));
pool.submit(()->sout(Thread.currentThread().getName()+"执行"));
//关闭线程池
pool.shutdown();
}
}
- corePoolSize: 核心线程的最大值,不能小于0
- maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
- keepAliveTime: 空闲线程最大存活时间,不能小于0
- unit: 时间单位
- workQueue: 任务队列,不能为null
- threadFactory: 创建线程工厂,不能为null
- handler: 任务的拒绝策略,不能为null (当任务数大于线程池最大的容量+等待队列数量)
任务拒绝策略
- ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出- - – RejectedExecutionException异常。是默认的策略。
- ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
- ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
- ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。