线程
什么是线程
-
线程是一个程序内部的一条执行路径
-
我们之前启动程序执行后,main方法的执行其实就是一条单独的实行路径
-
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
多线程是什么
-
多线程是指从软硬件上实现多条执行流程的技术
-
例如:消息通信 淘宝 京东系统都离不开多线程技术
多线程的创建
方式一: 继承Thread类
多线程的实现方案一:继承Thread类
-
定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
-
创建MyThread类的对象
-
调用线程对象的start()方法启动线程(启动后还是执行run方法的)
优缺点:
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展
1、为什么不直接调用了run方法,而是调用start启动线程。
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。
2、把主线程任务放在子线程之前了。
这样主线程一直是先跑完的,相当于是一个单线程的效果了。
方式二:实现Runnable接口
-
定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
-
创建MyRunnable任务对象
-
把MyRunnable任务对象交给Thread处理。
-
调用线程对象的start()方法启动线程
Thread的构造器
优缺点:
优点 : 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点 : 编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
方式三:JDK5.0新增:实现Callable接口
-
前2种线程创建方式都存在一个问题:
-
他们重写的run方法均不能直接返回结果。
-
不适合需要返回线程执行结果的业务场景。
-
-
怎么解决这个问题?
JDK 5.0提供了Callable和FutureTask来实现。
多线程的实现方案三:利用Callable、FutureTask接口实现。
-
得到任务对象
-
定义类实现Callable接口,重写call方法,封装要做的事情。
-
用FutureTask把callable对象封装成线程任务对象。
-
-
把线程任务对象交给Thread处理。
-
调用Thread的start方法启动线程,执行任务
-
线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /* 目标:学会线程的创建方式三 */ public class ThreadDemo03 { public static void main(String[] args) { //3.创建Callable任务对象 Callable<String> call = new MyCallable(100); //4.把Callable任务对象交给 FutureTask 对象 // FutureTask对象的作用1:是Runnable的对象,可以交给Thread //FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完的结果 FutureTask<String> f1 = new FutureTask<>(call); //5.线程处理 Thread t1 = new Thread(f1); t1.start(); try { String rs1 = f1.get();//调用get执行call方法返回的结果 System.out.println("结果" + rs1); } catch (Exception e) { e.printStackTrace(); } } } /** * 1.定义一个任务类,实现Callable接口 */ class MyCallable implements Callable<String> { private int a; MyCallable(int a) { this.a = a; } //重写call方法 任务方法 @Override public String call() throws Exception { int sum = 0; for (int i = 0; i < a; i++) { sum += i; } return "子线程执行的结果是" + sum; } }
优缺点:
优点:
-
线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
-
可以在线程执行完毕后去获取线程执行的结果。
缺点:
-
编程复杂
三种方式对比
Thread的常用方法
Thread常用方法:
-
获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
-
获取当前线程Thread.currentThread();
-
至于Thread类提供的诸如: yield、join、interrupt、不推荐的方法 stop、守护线程、线程优先级等线程的控制方法,在开发中很少使用。
Thread的构造器
可以设置有参构造器直接传name
Thread类的线程休眠方法
public static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒
线程安全
线程安全问题
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
取钱模型演示
-
需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
-
如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?
线程安全问题出现的原因
-
存在多线程并发
-
同时访问共享资源
-
存在修改共享资源
线程安全问题案例模拟
取钱业务
需求:
-
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
-
需要提供一个账户类,创建一个账户对象代表2个人的共享账户。
-
需要定义一个线程类,线程类可以处理账户对象。
-
创建2个线程对象,传入同一个账户对象。
-
启动2个线程,去同一个账户对象中取钱10万。
代码:
/** * 需求:模拟取钱案例 */ public class ThreadDemo { public static void main(String[] args) { //1.定义线程类,创建一个共享的账户对象 Account acc = new Account(100000); //2.创建两个线程对象,代表小明和小红同时进行 new DrawThread(acc,"小明").start(); new DrawThread(acc,"小红").start(); } } public class DrawThread extends Thread{ //接收处理的账户对象 private Account acc; public DrawThread(Account acc,String name){ super(name); this.acc = acc; } @Override public void run() { //取钱的方式 acc.drawMoney(100000); } } public class Account { private String cardID; private double money; public Account() { } public Account(double money) { this.money = money; } public String getCardID() { return cardID; } public void setCardID(String cardID) { this.cardID = cardID; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } public void drawMoney(double money) { //1.获取谁来取钱 String name = Thread.currentThread().getName(); //2.判断账户余额是否达标 if (this.money >= money){ //3.取钱 System.out.println(name + "来取钱,取钱成功,吐出" + money); //4更新余额 this.money -= money; System.out.println("剩余" + this.money); }else{ System.out.println("余额不足"); } } }
线程同步
同步思想概述
-
为了解决线程安全问题
1.取钱案例出现问题的原因?
-
多个线程同时执行,发现账户都是钱够的
2.如何才能保证线程的安全呢?
-
让多个线程实现先后一次访问共享资源,解决问题
线程同步的核心思想
-
加锁,把共享资源进行上锁,每次只能一个线程进入,访问完毕以后解锁,然后其他线程才能进来
方式一:同步代码块
作用:把出现线程安全问题的核心代码给上锁
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
锁对象要求
-
理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可
锁对象的规范要求
-
规范上:建议使用共享资源作为锁对象
-
对于实例方法建议使用this作为锁对象
-
对于静态方法建议使用字节码(类名.class)对象作为锁对象
方式二: 同步方法
-
作用:把出现线程安全问题的核心方法给上锁。
-
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
-
格式
同步方法的底层原理
-
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
-
如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
-
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
是同步代码块好还是同步方法好一点?
-
同步代码块锁的范围更小,同步方法锁的范围更大。
-
不过大多情况下都使用同步方法 更简便 便于阅读
方式三:Lock锁
-
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
-
Lock实现提供比使用synchronized方法和语句司以获得更广泛的锁定操作。
-
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
线程通信(了解即可)
什么是线程通信、如何实现?
-
所谓线程通信就是线程间相互发送数据,线程通信通常通过共享一个数据的方式实现。
-
线程间会根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
线程通信常见模型
-
生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费数据。
-
要求:生产者线程生产完数据后,唤醒消费者,然后等待自己;消费者消费完该数据后,唤醒生产者,然后等待自己。
线程通信案例模拟
-
假如有这样一个场景,小明和小红有三个爸爸,爸爸们负责存钱,小明和小红负责取钱,必须一存、一取。
线程通信的前提:
线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。
线程池[重点]
线程池概述
什么是线程池?
-
线程池就是一个可以复用线程的技术。
不使用线程池的问题
-
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
线程池的工作原理
线程池实现的API、参数说明
谁代表线程池?
-
JDK 5.0起提供了代表线程池的接口:ExecutorService
如何得到线程池对象
-
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
-
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
ThreadPoolExecutor构造器的参数说明
参数一:指定线程池的线程数量(核心线程) : corePoolsize --------------不能小于0 参数二:指定线程池可支持的最大线程数:maximumPoolsize--------------最大数量>=核心线程数量 参数三:指定临时线程的最大存活时间: keepAliveTime --------------不能小于0 参数四:指定存活时间的单位(秒、分、时、天): unit --------------时间单位 参数五:指定任务队列: workQueue --------------不能为null 参数六:指定用哪个线程工厂创建线程: threadFactory --------------不能为null 参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler--------------不能为null
线程池常见面试题
临时线程什么时候创建?
-
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
-
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
线程池处理Runnable任务
public class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld" + i); } try { System.out.println(Thread.currentThread().getName() + "本任务与线程绑定了,线程进入休眠了"); Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 目标:自定义一个线程池对象,并测试其特性 */ public class ThreadPoolDemo01 { public static void main(String[] args) { /** * 1.创建线程池对象 * public ThreadPoolExecutor(int corePoolSize, * int maximumPoolSize, * long keepAliveTime, * TimeUnit unit, * BlockingQueue<Runnable> workQueue) { * this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, * Executors.defaultThreadFactory(), defaultHandler); * } */ ExecutorService pool = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); //2.给任务线程池处理 Runnable target = new MyRunnable(); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); //关闭线程池(一般不会使用) pool.shutdownNow();//立即关闭,即使任务没有完成,会丢失任务 pool.shutdown();//会等待全部任务执行完毕之后再关闭 } }
新任务拒绝策略
线程池处理Callable任务
Executors得到线程池
线程池创建有七种方式,最核心的是最后一种:
-
newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
-
newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
-
newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
-
newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
-
newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
-
newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
-
ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
-
开发中最好不使用Executors有可能线程过多或者任务过多直接崩掉。
定时器
-
定时器是一种控制任务延时调用,或者周期调用的技术。
-
作用 : 闹钟、定时邮件发送
实现方式
-
方式一:Timer
-
方式二:ScheduledExecutorService
Timer定时器
import java.util.Timer; import java.util.TimerTask; /** * 目标:Timer定时器的使用和了解 */ public class TimerDemo01 { public static void main(String[] args) { //1.创建Timer定时器 Timer timer = new Timer(); //2.调用方法,处理定时任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行一次~~~"); } },3000,2000);//开始启动等待时间和后续等待时间 } }
Timer定时器的特点和存在问题
-
Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
-
可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。
ScheduledExecutorService定时器
import java.util.Date; import java.util.TimerTask; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class TimerDemo02 { public static void main(String[] args) { //1.创建ScheduledExecutorService线程池,做定时器 ScheduledExecutorService pool = Executors.newScheduledThreadPool(3); //2.开启定时任务 pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出AAA ===>" + new Date()); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } } },0,2, TimeUnit.SECONDS); pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出BBB ===>" + new Date()); } },0,2, TimeUnit.SECONDS); } }
scheduledExecutorService的优点
-
基于线程池,某个任务的执行情况不会影响其他定时任务的执行。
并发、并行
-
正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的。
并发的理解:
-
CPU同时处理线程的数量有限。
-
CPu会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行的理解:
-
在同一时刻上,同时有多个线程在被CPU处理并执行。
线程的生命周期
线程的状态
-
线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
-
理解线程的状态有利于提升并发编程的理解能力。
Java线程的状态
-
Java总共定义了6种状态
-
6种状态都定义在Thread类的内部枚举类中。
-
NEW 尚未启动
-
RUNNABLE 正在执行中
-
BLOCKED 阻塞的(被同步锁或者IO锁阻塞)
-
WAITING 永久等待状态
-
TIMED_WAITING 等待指定的时间重新被唤醒的状态
-
TERMINATED 执行完成