JAVA多线程知识总结

目录

JAVA多线程

进程、线程、协程

线程上下文切换

Java中的线程调度算法

守护线程

线程的生命周期

5种基本状态

5种状态的转换

线程间通信

线程安全

什么是线程安全?

java中如何保证多线程的运行安全?

死锁

死锁的必要条件

防止死锁

创建线程的方式

继承Thread类

实现Runnable接口

实现Callable接口

线程池

线程池使用的时机(何时使用线程池?)

使用线程池的好处

线程池四个基本组成部分

线程池七大参数

使用线程池的流程

如何配置线程池中的线程数

四种常见线程池

悲观锁&乐观锁

公平锁&非公平锁

独占锁&共享锁

可重入锁

自旋锁


JAVA多线程

为什么要使用多线程?

1 避免阻塞

2 避免cpu空转

3 提升性能

进程、线程、协程

  • 进程是操作系统进行资源分配的最小单位。
  • 线程是操作系统进行调用的最小单元。
  • 协程和线程相似,由程序员进行调度。

在单核上,CPU决定线程a和b的执行权。在双核上,并行,一个执行线程a,另一个执行线程b。协程则是由程序员控制执行权。例如可以让一个协程a同时在两个核上运行,执行完后再使用双核同时运行协程b。

*有了进程为什么还要有线程?

  1. 从资源上,创建一个线程的开销比进程小。每创建一个新的进程必须分配给他独立的地址空间。
  2. 从上下文切换效率上,运行于一个进程上的多个线程,用的是相同的一块地址空间,他的切换时间要远小于进程切换的时间。
  3. 从通信机制上,线程的通信机制比进程要方便。对不同的进程来讲,他们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行。而在统一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他的线程所用。

线程上下文切换

cpu在执行一个已经运行的线程的时候切换到另一个等待获取CPU执行权的线程。这个切换过程就是线程上下文切换。

*如何减少线程上下文切换,提高操作系统效率?

  1. 无锁并发  多线程竞争锁时,会引起线程上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁。
  2. 使用最少线程  避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,会造成大量线程都处于等待状态。
  3. 使用协程  在单线程实现多任务的调度,并在单线程里维持多个任务间的切换。

Java中的线程调度算法

  • 分时调度模型:  让所有的线程轮流获得cpu的使用权,并且平均分配线程占用的cpu时间片。
  • 抢占式调度模型:  让优先级高的线程先运行,在运行完成之前不会切换到下一个线程。

守护线程

     也称后台线程,例如GC线程。守护线程和前台线程一起运行,当前台线程执行完,守护线程也自动结束。

 

线程的生命周期

5种基本状态

  1. NEW  初始状态
  2. RUNNABLE  可运行状态
  3. RUNNING  运行状态
  4. BLOCKED  阻塞状态
  5. DEAD 死亡状态

5种状态的转换

JAVA多线程知识总结_第1张图片

就绪状态转换为运行状态:当此线程得到处理器资源

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程执行体执行完毕或发生了异常。

*sleep()和wait()区别

  1. 释放锁:sleep方法没有释放锁;wait()方法释放了锁。
  2. 作用:wait()通常用于线程间通信;sleep()用于暂停执行。
  3. 苏醒:wait()方法执行完成后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法;sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout),超时后线程会自动苏醒。

 

线程间通信

1.同步(使用synchronized)

本质上就是共享内存上的通信。多个线程需要访问同一个共享变量,谁拿到了锁,谁就可以执行。

2.while轮询

线程a不断改变条件,b不停地通过while语句检验这个条件(list.size()==5)是否成立,从而实现了线程间通信,这种方法会浪费cpu资源。

3.wait/notify机制

条件满足时,线程b调用notify()通知线程a,也就是唤醒线程a,并且让他进入可运行状态,提高了cpu利用率。

4.管道通信

使用java.io.PipeInputStream和java.io.OutPutStream进行通信。通过管道将一个线程中的消息发送给另外一个。

 

线程安全

什么是线程安全?

如果你的代码在多线程下执行和单线程下执行永远能获得一样的结果,那么你的代码就是线程安全的。

java中如何保证多线程的运行安全?

     1.原子性:同一时间只能有一个线程对同一数据进行操作。

     2.可见性:当一个线程对数据进行操作,可以及时被其他线程看到。

     3.有序性:代码的执行和语句的顺序保持一致。

死锁

死锁指多个进程因竞争资源而进行相互等待的僵局。

死锁的必要条件

1.互斥条件

一个资源每次只能被一个进程使用。

2.请求与保持条件

进程已经获得了一个资源,但又提出了新的资源请求,而该资源已被其他进程占有;此时请求进程被阻塞,但是又对已获得的资源保持不放。

3.不可剥夺条件

进程在获得的资源使用完毕前,不能被其他进程强行夺走,只能由获得该资源的进程自己来释放。

4.循环等待条件

指若干进程间形成的首尾相接的循环等待资源的关系。

防止死锁

1.加锁顺序

确保所有线程都按相同的顺序来获得锁,那么死锁就不会发生;线程只有获取了前面的锁之后,才能获取后面的锁。

2.加锁时限

在尝试获取锁时,加一个超时时间,如果超过了这个时限,则放弃对该锁的请求;

如果没有在时限内成功获得所有需要的锁,则会回退并释放所有已经获得的锁,然后等待一段时间后重试。  

3.死锁检测

创建线程的方式

继承Thread类

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程要完成的任务,因此把run()方法称为执行方法体。
  2. 创建Thread类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动线程。

*start()和 run()的区别?

start()用于启动一个新的线程,而且start()内部调用了run()方法,所以它会开启新线程,并执行run()方法。当调用run()方法时,只会是在原来的线程中调用方法,没有新的线程启动。

实现Runnable接口

  1. 定义Runnable方法的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动该线程。

实现Callable接口

  1. 创建Callable的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

*Thread、Runnable、Callable的区别

  1. 线程类实现了Runnable和Callable的接口,还能继承其它类;而继承了Thread的线程,不能继承其它父类。
  2. Runnable、Callable如果要访问该线程,必须要调用Thread.currentThread()方法;Thread如果要访问该线程,只要使用this即可。
  3. Runnable无返回值,Callable有返回值。
  4. call()方法可以抛出异常,run()方法有异常只能在内部消化。

线程池

线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。底层由队列实现

线程池使用的时机(何时使用线程池?)

当创建任务时间+销毁线程时间远大于在线程中执行任务的时间,则可以采用线程池,用来提高服务器性能。

使用线程池的好处

  1. 提高线程的可管理性:线程池可以对线程进行统一的分配、调优和监控。
  2. 控制最大并发数:可以更好的控制线程数量,避免过多线程竞争;而不是无限的创建线程,导致应用内存溢出。
  3. 线程复用,降低资源消耗:线程池中的线程可以进行回收再利用,避免了线程频繁的创建和销毁。提高了资源的利用率。
  4. 提高响应速度:当任务到达时,任务不需要等到线程创建就可以立即执行。

线程池四个基本组成部分

1.线程池管理器

用于创建并管理线程池,包括创建线程池、销毁线程池、添加新任务。

2.工作线程

线程池中线程,在没有任务时处于等待状态,可以循环执行任务。

3.任务接口

每个任务必须实现的接口,以供工作线程调度任务的执行;它规定了任务的入口、任务执行完后的收尾工作、任务的执行状态等。

4.任务队列

用于存放没有处理的任务,提供一种缓冲机制。

线程池七大参数

  1. 核心线程数
  2. 最大线程数
  3. 多余线程存活时间
  4. 阻塞队列
  5. 线程工厂
  6. 时间单位
  7. 拒绝策略

使用线程池的流程

  1. 有了任务,开启新的线程;
  2. 当任务数大于核心线程数时,将任务放入阻塞队列;
  3. 当阻塞队列满了,将线程数调整至最大线程数;
  4. 当任务数大于最大线程数时,实行拒绝策略;
  5. 设置多余线程存活时间,超出时间时,将线程数调整至核心线程数大小。

如何配置线程池中的线程数

  1. 首先要知道自己的服务器是几核的;
  2. 如果是cpu密集型,大量运算,就是cpu数+1;
  3. 如果是IO密集型,因为cpu不是一直在执行任务,则可以尽可能多配置线程数,一般为cpu*2。

四种常见线程池

Executors为我们封装好了4种常见的线程池。

1.定长线程池(FixedThreadPool)

池内线程类型:核心线程

池内线程数量:固定

处理特点:核心线程处于空闲状态时也不会回收,除非线程被关闭;

                  当所有线程都处于活动状态时,新的任务都会处于等待状态,知道有线程空闲出来;

                  任务队列无大小限制(超出的任务会在队列中等待)。

应用场景:控制线程最大并发数

2.定时线程池(ScheduledThreadPool)

池内线程类型:核心&非核心线程

池内线程数量:核心线程数量固定;非核心线程无限制

处理特点:当非核心线程执行超时,则会被立即回收

应用场景:执行定时或周期性任务

3.可缓存线程池

池内线程类型:非核心线程

池内线程数量:不固定(可无限大)

处理特点:优先利用闲置线程处理新任务(会重用线程);

                  无线程可用时,会新建线程。任何线程任务到来时都会立即执行,不需要等待;

                  灵活回收空闲线程(具备超时机制,空闲超过60s才回收,全部回收时几乎不占系统资源)

应用场景:执行数量多、耗时少的线程任务

4.单线程化线程池(SingledThreadExecutor)

池内线程类型:核心线程

池内线程数量:1个

处理特点:保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步问题。

应用场景:单线程(不适合并发但可能引起IO阻塞以及影响UI线程响应的操作,如数据库操作等)

*四种线程池的弊端

功能线程池虽然方便,但不建议使用,建议通过ThreadPoolExecutor的方式,避免资源耗尽的风险。

定长线程池和单线程化线程池:

   堆积的请求处理队列均采用LinkedBlockedQueue,可能会耗费很大内存,甚至造成OOM(内存溢出)。

定时线程池和可缓存线程池:

   他们的现成最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至造成OOM。

锁是一种同步机制,用于在有许多执行线程的环境中强制对资源进行访问限制;锁可以强制实施排他互斥、并发控制策略。

悲观锁&乐观锁

悲观锁:认为每次拿数据时别人都会对数据进行修改;

乐观锁:认为每次拿数据时别人都不会对数据进行修改。

公平锁&非公平锁

公平锁:线程按顺序依次获取锁;

非公平锁:线程不按申请顺序获得锁,谁都有可能获得锁。

独占锁&共享锁

独占锁:同一时间只能被一个线程持有;

共享锁:同一时间可以被多个线程占有。

可重入锁

外层方法获得锁的时候,内层方法自动获得该锁。

自旋锁

是指当一个线程在获取锁的过程中,如果所已经被其他线程获取,那么该线程将循环等待,然后判断锁是否能够被成功获取,直到获取到锁才会退出循环。

待补充

你可能感兴趣的:(java基础,java,多线程)