目录
一、概念
1.什么是多线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,而多线程就是指从软件或者硬件上实现多个线程并发执行的技术,具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。多线程是在同一时间需要完成多项任务的时候实现的。
2.多线程的优缺点
优点:
-
多线程技术可以加快程序的运行速度,使程序的响应速度更快,因为用户界面可以在进行其它工作的同时一直处于活动状态
-
可以把占据长时间的程序中的任务放到后台去处理,同时执行其他操作,提高效率
-
当前没有进行处理的任务时可以将处理器时间让给其它任务
-
可以让同一个程序的不同部分并发执行,释放一些珍贵的资源如内存占用等等
-
可以随时停止任务
-
可以分别设置各个任务的优先级以优化性能
缺点:
-
因为多线程需要开辟内存,而且线程切换需要时间因此会很消耗系统内存。
-
线程的终止会对程序产生影响
-
由于多个线程之间存在共享数据,因此容易出现线程死锁的情况
-
对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。
3.一个线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
各个状态的详解:
-
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。 -
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。 -
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 -
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种: -
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
-
锁阻塞状态:
4.多线程的内存图解
5.多线程原理图解
6.线程状态图解
二、创建线程的方式
1.通过实现 Runnable 接口
/**
* 实现Runnable接口
*/
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("继承了Runnable后,重写了run方法");
}
}
public class Ch02 {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
//如果想要让线程启动,必须调用Thread类中的start方法
Thread t = new Thread(myThread2);
t.start();
}
}
用实现Runnable接口来创建多线程程序的好处
- 避免了单继承的局限性,一个类只能有一个父类,类继承了Thread就不能继承别的类了,而实现Runnable则还可以继承别的类和实现别的接口
- 增强了程序的扩展性,降低了程序的耦合性(解耦),该方式将设置线程任务和开启线程进行了分类(解耦)
2.通过继承 Thread 类本身
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写run()
方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
/**
* (1).继承Thread类,并且重写run方法
*
*/
class MyThread extends Thread{
@Override
public void run() {
System.out.println("重写的run方法。。。");
}
}
public class Ch01{
public static void main(String[] args) {
MyThread myThread = new MyThread();
//当调用start方法启动一个线程时,会执行重启的run方法
//调用的是start,执行的run
myThread.start();
}
}
3.通过 Callable 和 Future 创建线程
-
创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
-
创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
4.创建线程的三种方式的对比
-
采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
-
使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
三、线程安全
1.同步技术的原理
2.同步代码块
卖票案例出现了线程安全问题卖出了不存在的票和重复的票
- 解决线程安全问题的一种方案:使用同步代码块格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
- 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
同步代码块实现卖票案例:
/*
格式:
synchronized(锁对象){
可能出现线程安全问题的代码(访问了共享数据的代码)
}
*/
public class RunnableImpl implements Runnable {
//定义一个多线程共享的票源
private int ticket = 100;
//创建一个锁对象
Object obj = new Object();
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让买票重复操作
while (true) {
synchronized (obj) {
//判断票是否存在
if (ticket > 0) {
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "第" + ticket + "张票");
ticket--;
}
}
}
}
}
3.同步方法
同步方法实际上的锁对象是当前对象->this
静态方法的锁对象是本类的class属性
解决线程安全问题的二种方案:使用同步方法
使用步骤:
- 把访问了共享数据的代码抽取出来,放到一个方法中
- 在方法上添加synchronized修饰符
格式: 定义方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
public class RunnableImpl implements Runnable {
//定义一个多线程共享的票源
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让买票重复操作
while (true) {
sh();
//判断票是否存在
}
}
/*
创建一个同步方法:
同步方法也会把方法内部的代码锁住
只让一个线程执行
同步方法的锁对象是谁?
就是实现类对象 new RunnableImpl()
也就是this
*/
public static synchronized void sh() {
if (ticket > 0) {
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "第" + ticket + "张票");
ticket--;
}
}
}
4.Lock锁
解决线程安全问题的三种方案:使用Lock锁java.util.concurrent.Locks.Lock接口
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void Lock()获取锁。void unlock()释放锁。
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤;
- 在成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
- 在可能会出现安全问题的代码后调用Lock接口中的方法unLock释放锁
public class RunnableImpl implements Runnable {
//定义一个多线程共享的票源
private int ticket = 100;
//1.在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让买票重复操作
while (true) {
//2.在可能出现安全问题的代码钱调用lock接口中的方法Lock获取锁
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.在可能出现安全问题的代码钱调用lock接口中的方法unLock获取锁
l.unlock();//无论程序是否异常,都会将锁释放掉
}
}
}
}
}
5. LockSupport工具类
线程阻塞的工具类。,所有的方法都是静态方法,可以让线程在任意位置阻塞, 阻塞之后也有唤醒的方法。
park:停车。如果我们把Thread看成一辆车的话,park就是让车停下
unpark:就是让车启动然后跑起来
这里的park和unpark其实实现了wait和notify的功能。
区别:
- park不需要获取某个对象的锁(不释放锁)
- 因为中断park不会抛出InterruptedException异常,需要在park之后自行判断中断状态,
然后做额外的处理。
总结:
- park和unpark可以实现wait和notify的功能,但是并不和wait和notify交叉使用。
- park和unpark不会出现死锁。
- blocker的作用看到阻塞对象的信息
四、等待唤醒机制
1.线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程任务)却不同
比如:线程A用来生成包子,线程B用来吃包子,包子可以理解为同一资源,线程A和线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
多个线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
2.等待唤醒机制
等待唤醒机制是多个线程间的一种协作机制。谈到线程经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是全部,线程间也会有协作机制。
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。这种手段就叫做等待唤醒机制。
wait、notify就是线程间的一种协作机制。
- 等待唤醒中的方法:
-
wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费cpu资源,也不会竞争锁,这时的线程状态即是waiting。它还要等着别的线程执行一个特别的动作,也是“通知notify”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中
-
notify:选取所通知对象的wait set中一个线程释放;例如,餐厅有位置后,等候就餐最久的顾客最先入座。
-
notifyAll:释放所通知对象的wait set上的全部线程。
注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以需要再次常识去获取锁(很可能面临其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。
- 调用wait和notify方法需要注意的细节:
-
wait方法与notify方法必须要由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
-
wait方法与notify方法是属于Object类的方法的。因为锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
-
wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为必要通过锁对象调用这两个方法。
五、线程池
1.线程池的概念
2.线程池的好处
- 降低系统资源消耗, 通过重用已存在的线程, 降低线程创建和销毁造成的消耗;
- 提高系统响应速度, 当有任务到达时, 无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控, 线程若是无限制的创建, 不仅会额外消耗大量系统资源, 更是
占用过多资源而阻塞系统或内存不足等状况, 从而降低系统的稳定性。 线程池能有效管控线
程, 统一分配、 调优, 提供资源使用率; - 更强大的功能, 线程池提供了定时、 定期以及可控线程数等功能的线程池, 使用方便简
单
3.线程池的使用
- 使用线程池中的工厂类Executors里面提供的newFixedThreadPool生产一个指定数量的线程池
ExecutorService es = Executors.newFixedThreadPool(8);
- 实现Runnable接口
public class RunnableImpl implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
}
}
- ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
es.submit(new RunnableImpl());
自带的几种线程池:
- 1.newCachedThreadPool.创建一个可缓存的线程池,如果线程池超过处理需要,可以灵活回收空闲线程 若无可回收,创建新线程
- 2.newFixedThreadPool,创建一个定长的线程池,可以控制线程的最大并发数,超出的线程会在队列中华等待
- 3.newScheduledThreadPool:创建一个定长的线程池,支持定时及周期性任务执行
- 4.newSingleThreadExecutor:创建一个单线程化的线程池,他会用唯一的工作线程来执行任务,保证所有的任务按照顺序来执行
参数的意义(重要):
- corePoolSize:线程池的线程的数量,核心线程池的大小
- maximumPoolSize,指定了线程池最大线程数量
- keepAliveTime:当线程池线程数量大于corePoolSize,多出来的空闲线程,多长时间被销毁
- unit:时间单位
- workQueue:任务队列,用于存放提交但尚未被执行的任务
- threadFactory:线程工厂,用来创建线程,线程工厂就是我们new线程的
- handler:拒绝策略,是将任务添加到线程池中时,线程池拒绝该任务多采取的相应的措施
常见的工作队列:
- ArrayBlockingQueue:基于数组的有界阻塞队列:FIFO
- LinkedBlockingQueue:基于链表的有界阻塞队列。FIFO
jdk默认提供了四种拒绝策略:
- CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
- AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
- DiscardPolicy - 直接丢弃,其他啥都没有
- DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入