2022-08-02 java之多线程
一、概念
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.同步技术的原理
3.同步方法
同步方法实际上的锁对象是当前对象->this
静态方法的锁对象是本类的class属性
4.Lock锁
四、等待唤醒机制
1.线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程任务)却不同
比如:线程A用来生成包子,线程B用来吃包子,包子可以理解为同一资源,线程A和线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
多个线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
2.等待唤醒机制
等待唤醒机制是多个线程间的一种协作机制。谈到线程经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是全部,线程间也会有协作机制。
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。这种手段就叫做等待唤醒机制。
就是在一个线程进行了规定操作后,就进入等待状态(wait()),等待其他线程执行完他们的指定代码过后,再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有等待的线程。
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());