文章目录
区分进程(Process)线程(Thread)
说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。
线程是CPU调度和执行的的单位。
-
进程是操作系统资源分配的最小单元,线程是任务调度和执行的基本单位。(根本区别)
-
包含关系:每个进程至少有一个线程,主线程。进程中可以创建多个线程。
**注意:**很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
线程就是独立的执行路径
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程,main()称之为主线程,为系统的入口,用于执行整个程序;
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;线程会带来额外的开销,如cpu调度时间,并发控制开销。
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程的创建
- Thread class 继承Tread类(重点)
- Runnable接口 实现Runnable接口(重点)
- Callable接口 实现Callable接口(了解)
继承Tread类
- 继承Tread父类
- 重写run方法
- 线程实例调用start方法
- 不建议使用:避免oop单继承的局限性
public class ThreadDemo01 {
public static void main(String[] args) {
ThreadTest demo = new ThreadTest();
demo.start();
}
}
class ThreadTest extends Thread{
public void run(){
System.out.println("使用继承Thread创建线程!");
}
}
//运行结果:
//使用继承Thread创建线程!
//Process finished with exit code 0
public class ThreadDemo02 extends Thread{
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(currentThread().getName()+"在运行");
}
}
public static void main(String[] args) {
ThreadDemo02 demo02 = new ThreadDemo02();
demo02.start();
for (int i = 0; i < 100; i++) {
System.out.println(currentThread().getName()+"在运行");
}
}
}
//运行结果:
// .....
//main在运行
//main在运行
//main在运行
//main在运行
//main在运行
//Thread-0在运行
//Thread-0在运行
//Thread-0在运行
//Thread-0在运行
//Thread-0在运行
//main在运行
//main在运行
//...
实现Runnable接口
- 实现Runnable接口
- 重写run方法,执行线程需要丢入runnable接口实现类
- 调用start方法
- 推荐使用:避免单继承的局限性,灵活方便,方便一个对象被多个线程使用
public class RunnableDemo01 implements Runnable{
public void run(){
System.out.println("创建"+Thread.currentThread().getName()+"线程!");
}
public static void main(String[] args) {
RunnableDemo01 demo01 = new RunnableDemo01();
Thread th1 = new Thread(demo01);
Thread th2= new Thread(demo01);
Thread th3 = new Thread(demo01);
th1.start();
th2.start();
th3.start();
}
}
//运行结果:
//创建Thread-0线程!
//创建Thread-1线程!
//创建Thread-2线程!
实现多个线程操作同一个对象
购买火车票为例
//模拟售票案例
public class RunnableDemo02 implements Runnable {
public void run(){
try{
int tickets = 50;
while(true){
if(tickets<0) break;
Thread.currentThread().sleep(100);
System.out.println(Thread.currentThread().getName()+"售出第"+(tickets--)+"张票");
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
RunnableDemo02 demo = new RunnableDemo02();
new Thread(demo,"窗口一").start();
new Thread(demo,"窗口二").start();
new Thread(demo,"窗口三").start();
}
}
//运行结果
//窗口三售出第50张票
//窗口一售出第50张票
//窗口二售出第50张票
//窗口三售出第49张票
//窗口二售出第49张票
//窗口一售出第49张票
//窗口二售出第48张票
//窗口一售出第48张票
//...
运行结果出现不同线程抢到同一数据
发现问题: 多个线程操作同一个资源的情况下,线程不安全,数据紊乱。
解决问题: 线程同步
模拟两人跑步比赛
//模拟两人跑步比赛
public class RunnableDemo03 implements Runnable{
public void run(){
int track = 1000; //定义赛道长度
int speed = 0; //定义参赛者的速度
if((Thread.currentThread().getName()).equals("运动员一号")){
speed = 10;
}else{
speed = 8;
}
while(true){
try{Thread.sleep(100);}catch(InterruptedException e){}
track -= 2*speed;
System.out.println(Thread.currentThread().getName()+"剩余"+track+"米");
if(track<=0) {
System.out.println(Thread.currentThread().getName()+"获得胜利");
return;
}
}
}
public static void main(String[] args) {
RunnableDemo03 demo03 = new RunnableDemo03();
new Thread(demo03,"运动员一号").start();
new Thread(demo03,"运动员二号").start();
}
}
实现Callable接口(了解)
优点:
- 可以定义 返回值
- 可以抛出异常
步骤:
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常 (通过Future接口获取)
- 创建目标对象
- 使用FutureTask接受目标对象,得到FutureTask对象
- Thread线程传入FutureTask对象
- start() 开启线程
- get() 获取call()返回结果,get方法一定在call之后,且一定要抛出异常
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class CallableDemo implements Callable<String> {
public String call() throws Exception {
String name = Thread.currentThread().getName();
return name;
}
public static void main(String[] args) throws Exception {
//线程开启之后需要执行call方法
CallableDemo c1 = new CallableDemo();
CallableDemo c2= new CallableDemo();
CallableDemo c3 = new CallableDemo();
//Thread t1 = new Thread(c1) 是错误的,Callable并不是Thread子类,也不是Runnable的实现类,无法传入目标对象
//因此需要中间件FutureTask,FutureTask是Runnable的实现类
FutureTask<String> r1 = new FutureTask<>(t1);
FutureTask<String> r2 = new FutureTask<>(t2);
FutureTask<String> r3 = new FutureTask<>(t3);
//创建线程并实现
new Thread(r1).start();
new Thread(r2).start();
new Thread(r3).start();
//获取返回值,如果线程还没有结束,那么get方法会死等
//
System.out.println(r1.get());
System.out.println(r2.get());
System.out.println(r3.get());
}
}
-
实现Callable接口,需要返回值类型
-
重写call方法,需要抛出异常 (通过Future接口获取)
-
创建目标对象
-
创建执行服务
- ExecutorService ser = Executors.newFixedThreadPool(3); //数值3表示开启的线程数
- 提交执行
- result1 = ser.submit(t1);
- result2 = ser.submit(t2);
- result3 = ser.submit(t3);
- 获取结果
- boolean r1 = result1.get();
- boolean r2 = result2.get();
- boolean r3 = result3.get();
- 关闭服务: ser.shutdownNow();
Lambda表达式在线程中的应用
Lambda表达式为函数式编程,在线程中也很常用,简化代码
函数式接口:任何接口如果只含有唯一一个抽象方法称为函数式接口
对于函数式接口,可以使用lambda表达式来创建该接口的对象
代码
interface Test{
void test();
}
....
Test test = (int a)=>{
System.out.print("运行第"+a+"次")
}
test.test(1);
//简化
Test test = a =>
System.out.print("运行第"+a+"次")
test.test(1);
前提条件:
- 使用lambda表达式,该接口为函数式接口
- 简化去掉花括号,该方法中只有一条语句,多条语句必须花括号使其为代码块
- 简化去掉包裹参数的括号,当参数只有一个时,当多个参数必须使用包裹参数的括号
线程的状态
5大状态:创建状态、就绪状态、阻塞状态、运行状态、死亡状态
- 创建状态: Thread t = new Thread() 线程对象一旦创建就进入到新生态
- 就绪状态 : 当调用start()方法,线程立即进入就绪状态,但是不意味着立即调度执行
- 阻塞状态: 当调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞时间解除后,重新进入就绪
- 运行状态:进入运行状态,线程才正真执行线程体的代码块
- 死亡状态:线程中断或者结束,一旦进入死亡状态,就不能再次启动
线程的常用方法
public static Thread currentThread()
返回对当前正在执行的线程对象的引用。
public static void yield()
线程礼让,对调度程序的一个暗示,即当前线程愿意让出当前使用的处理器,回到就绪状态,增大其他线程调度cpu资源的概率(注:礼让后也可能再次执行该线程)
很少使用这种方法。 它可能对调试或测试有用,可能有助于根据种族条件重现错误。 在设计并发控制结构时也可能有用。
public static void sleep(long millis) throws InterruptedException
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 线程不会丢失任何显示器的所有权。
如果 millis
值为负数 会抛出 IllegalArgumentException`
如果任何线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。 InterruptedException`
public static void sleep(long millis,int nanos) throws InterruptedException
导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。 线程不会丢失任何显示器的所有权。
-
参数
millis
- 以毫秒为单位的睡眠时间长度nanos
-0-999999
额外的纳秒睡眠 -
异常
IllegalArgumentException
-如果值millis
是否定的,或的值nanos
不在范围0-999999
InterruptedException
- 如果任何线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。
protected Object clone() throws CloneNotSupportedException
将CloneNotSupportedException作为线程抛出无法有意义地克隆。 构造一个新的线程。
-
存在异常
CloneNotSupportedException
public void start()
使线程开始执行;Java虚拟机调用此线程的run
方法。
不允许多次启动线程,只准启动一次。 特别地,一旦线程完成执行就可能不会重新启动。
-
异常
IllegalThreadStateException
- 如果线程已经启动。
public void run()
Thread的Thread
应该覆盖此方法,线程运行状态会执行run方法的代码块
public void interrupt()
中断这个线程。
-
异常
SecurityException
- 如果当前线程不能修改此线程
public static boolean interrupted()
测试当前线程是否中断。该方法可以清除线程的中断状态 。换句话说,如果这个方法被连续调用两次,那么第二个调用将返回false(除非当前线程再次中断,在第一个调用已经清除其中断状态之后,在第二个调用之前已经检查过)。
忽略线程中断,因为线程在中断时不存在将被该方法返回false所反映。
-
结果
true
如果当前线程已被中断;false
否则。
setPriority(int newPriority) / getPriority()
更改/获取线程的优先级,优先级越高,调度cpu的概率越大(注:优先级高的不一定比优先级低的先调度cpu,存在概率问题)
setName / getName
设置/获取线程的名字。用户线程默认名字为 Thread-xxx
public final void join() throws InterruptedException
等待这个线程死亡。
调用此方法的行为方式与调用完全相同 join(0)
join(参数1,参数2):参数1等待这个线程死亡的时间(毫秒),超时永久等待,参数2等待这个线程死亡的额外时间(纳秒)
-
异常
InterruptedException
- 如果任何线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。
public Thread.State getState()
返回此线程的状态。 该方法设计用于监视系统状态,不用于同步控制。
-
结果
这个线程的状态。
boolean isAlive()
测试线程是否处于活动状态
停止线程
不推荐使用JDK提供的stop().destroy()方法(已废弃)推荐线程自己停止下来
建议使用一个标志位进行终止变量当flag=false,则终止线程运行。
public class Teststop implements Runnable {
//1.线程中定义线程体使用的标识
private boolean flag = true;
@Override
public void run () {
//2.线程体使用该标识
while (flag) {
system.out.println ( "run. . . Thread");
}
}
//3.对外提供方法改变标识
public void stop ( ) {
this.flag = false;
}
}
线程休眠sleep
- sleep(时间)指当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延迟,倒计时,打印系统当前时间等
- 每一个对象都有一个锁,sleep不会释放锁
模拟倒计时
//模拟倒计时
public class ThreadSleep implements Runnable{
public void run() {
try{
for (int i = 10; i > 0; i-- ){
Thread.sleep(1000);
System.out.println(i);
}
}catch(InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadSleep s = new ThreadSleep();
new Thread(s).start();
}
}
打印系统当前时间
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadSleep2 {
public static void main(String[] args) {
//获取当前的时间
Date time = new Date(System.currentTimeMillis());
while(true){
try{
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(time));
time = new Date(System.currentTimeMillis());
}
catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
线程礼让yield
当使用yield方法时,当前线程会变为就绪状态,重新与其他线程进行抢夺cpu资源,因此礼让不一定成功,看cpu心情
线程强制执行join
当使用join时,该线程强制执行,其他线程回到就绪状态,类似于排队插队现象
观测测试线程状态
thread.getState()
-
线程状态。线程可以处于以下状态之一:
NEW
尚未启动的线程处于此状态。RUNNABLE
在Java虚拟机中执行的线程处于此状态。BLOCKED
被阻塞等待监视器锁定的线程处于此状态。WAITING
正在等待另一个线程执行特定动作的线程处于此状态。TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。TERMINATED
已退出的线程处于此状态。
一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。
线程优先级
最小优先级为1,最大为10,默认为5(代码测试)
getPriority():获取当前线程的优先级
setPriority(int newPriority):设置当前线程的优先级,小于1大于10会报错
优先级低只是意味着获得调度的概率低.并不是优先级低就不会被调用了.这都是看CPU的调度
守护线程daemon
线程分为用户线程和守护线程(默认创建的线程都是用户线程)
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
如,后台记录操作日志,监控内存,垃圾回收等待.
设置线程为守护线程
thread.setDaemon(true); //默认是false表示是用户线程,正常的线程都是用户线程…
并发
同一个对象被多个线程同时操作
并发与并行:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
它们最关键的点在于:是否是 “同时”
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步.线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
线程同步的安全性的形成条件:队列+锁(synchronized)
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可.存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下﹐加锁﹐释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.
线程存在不安全问题
一、定义:
多线程并发执行某个代码时,产生逻辑上的错误,结果和预期值不相同
二、产生原因
- 线程是抢占式执行的,即抢占cpu时间片
- 存在操作不是原子的,即当cpu执行线程时,调度器可能调走cpu,去执行另一个线程
- 多个线程修改同一个变量时。在多线程编程中,若干线程为了实现公共资源的操作,往往使复制相应变量的副本,待操作完成后再将此副本变量数据与原始数据进行同步处理(可以使用volatile关键字直接操作原始变量,但是并不能描述同步的处理)
- 内存可变性
- 指令重排序,java的编译器在编译代码时,会对指针进行优化,调度指令的先后顺序,保证原有逻辑变的情况下,来提高程序的运行效率
三、解决办法
加锁使线程同步
售票案例
//模拟售票案例
public class RunnableDemo02 implements Runnable {
public void run(){
try{
int tickets = 50;
while(true){
if(tickets<0) break;
Thread.currentThread().sleep(100);
System.out.println(Thread.currentThread().getName()+"售出第"+(tickets--)+"张票");
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
RunnableDemo02 demo = new RunnableDemo02();
new Thread(demo,"窗口一").start();
new Thread(demo,"窗口二").start();
new Thread(demo,"窗口三").start();
}
//出现重复售出同一张票的情况
线程集合问题
import java.util.Arraylist;
import java.util.List ;
//线程不安全的集合
public class Unsafelislt
public static void nain(string[] args) l
list<SLring? list = new Ar r aylist<sLring> ();
for (int i - e ; i < 1Goeo; i++)r
new Thread(()->{
list.add ( Thread.currentThread ( ) .getName ( ) ) ;}).start( );
}
system.out . println( list.size( ) ) ;
}
}
//出现线程小于1000,原因是不同的线程添加到同一个集合位置,导致覆盖
线程同步
同步块:synchronized (Obj ){ }(锁变量)
Obj称之为同步监视器
Obj可以是任何对象﹐但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器﹐因为同步方法的同步监视器就是this ,就是这个对象本身﹐或者是class[反射中讲解]
同步监视器的执行过程
1.第一个线程访问,锁定同步监视器﹐执行其中代码.
2. 第二个线程访问﹐发现同步监视器被锁定﹐无法访问.
3.第一个线程访问完毕,解锁同步监视器.
4.第二个线程访问,发现同步监视器没有锁﹐然后锁定并访问
JUC并发
import java.util.concurrent.copyonwriteArrayList;
//测试juc安全类型的集合
public class TestJUC {
public static void main( string[] args) {
ll
copyonwriteArrayList<String> list = new CopyonwriteArrayList<String>()for (int i = 0; i < 10000; i++) {
new Thread ( ()->{
list.add ( Thread.currentThread ( ).getName( ));}).start();
}
try {
Thread.sleep( millis: 3000);
} catch (InterruptedException e) {
e.printstackTrace();
system.out.println(list.size());
}
}
线程死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形.某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题.
产生死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破坏其中的任意一个或多个条件就可以避免死锁发生
Lock(锁)
synchronized 与Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
class A{
private final ReentrantLock lock = new ReenTrantLock();public void m(){
lock.lock();try{
l保证线程安全的代码;
}
finallyi
lock.unlock();
//如果同步代码有异常,要将unlock()写入finally语句块}
}
}
线程的协作通信
生产者消费者模式
应用场景:生产者和消费者问题
-
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库﹐消费者将仓库中产品取走消费.
-
如果仓库中没有产品,则生产者将产品放入仓库﹐否则停止生广开寺付,且到仓库中的产品被消费者取走为止.
-
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止.
Producer (生产者) -----> 数据缓存区 ----------> Consumer(消费者)
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件.
对于生产者,没有生产产品之前,要通知消费者等待﹒而生产了产品之后,又需要马上通知消费者消费
对于消费者﹐在消费之后﹐要通知生产者已经结束消费﹐需要生产新的产品以供消费.
在生产者消费者问题中﹐仅有synchronized是不够的
synchronized可阻止并发更新同一个共享资源,实现了同步synchronized不能用来实现不同线程之间的消息传递(通信)
解决方法
Java提供了几个方法解决线程之间的通信问题
wait()
表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout)
指定等待的毫秒数
notify()
唤醒一个处于等待状态的线程
notifyAll()
唤醒同一个对象上所有调用wait()方法的线程,优先级中较高的线程优先调度
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常lllegalMonitorStateException
1.方法一(管程法)
并发协作模型“生产者/消费者模式”—>管程法
生产者:负责生产数据的模块(可能是方法﹐对象﹐线程﹐进程);
消费者:负责处理数据的模块(可能是方法﹐对象,线程﹐进程);
缓冲区:消费者不能直接使用生产者的数据﹐
他们之间有个“缓冲区生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
2.方法二(信号灯法)
并发协作模型“生产者/消费者模式”—>信号灯法
线程池
前言
线程是调度cpu的最小单元,也叫轻量级进程LMP(Light Weight Process)
线程的创建存在的问题
-
线程频繁创建和销毁(性能开销)
-
线程创建的数量控制(上下文切换)
因此引入了线程池!!!
java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
- 背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。
- 优点
- 提高响应速度,减少了创建新线程的时间
- 降低资源消耗,重复利用线程池中线程,不需要每次都创建
- 便于线程管理
corePoolSize: 核心池的大小
maximumPoolSize: 最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止
- 线程池的创建
- 自动创建:体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool;
- 手动创建:体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:
两种线程模型
- 用户线程(ULT)模型:用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/内核态的切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它所有的线程)阻塞
- 内核线程(KLT)模型:系统内核管理线程(KLT),内核保存线程的状态和上下信息,线程阻塞不会引起进程进程阻塞,在多处理系统上,多线程在多处理器上并行运行,线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快
简而言之:某一个线程如果是由APP管理,则为用户线程,如果是由操作系统管理则是内核线程
JVM虚拟机使用的是KLT
线程池的意义
线程是稀缺资源,它的创建与销毁是一个相对偏重且耗资源的操作,而Java线程依赖于内核线程,要进行操作系统状态切换,为避免资源过度消耗需要设法重用线程执行多个任务。线程池就是一个负责对线程进行统一分配、调优与监控。
什么时候使用线程池?
- 单个任务处理时间比较短·需要处理的任务数量很大
线程池优势 - 重用存在的线程,减少线程创建,消亡的开销,提高性能
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。·提高线程的可管理性,可统一分配,调优和监控。
阻塞队列
FIFO
1、在任意时刻,不管并发有多高,永远只有一个线程能够进行队列的入队或者出队操作!线程安全的队列有界|无界
5
队列满,只能进行出队操作,所有入队的操作必须等待,也就是被阻塞
0
队列空,只能进行入队操作,所有出队的操作必须等待,也就是被阻塞
创建线程池
普通线程与线程池性能对比(初步体验线程池的优点)
普通
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class ThreadPool {
public static void main(String[] args) throws Exception{
Long start = System.currentTimeMillis();
Random random = new Random();
final List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
Thread t = new Thread(){
public void run(){
list.add(random.nextInt());
}
};
t.start();
t.join();
}
System.out.println("时间:"+(System.currentTimeMillis()-start));
System.out.println("大小:"+list.size());
}
}
//运行结果
//时间:13286
//大小:100000
线程池
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo01{
public static void main(String[] args) throws InterruptedException{
Long start = System.currentTimeMillis();
Random random = new Random();
final List<Integer> list = new ArrayList<>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
System.out.println("时间:"+(System.currentTimeMillis()-start));
System.out.println("大小:"+list.size());
}
}
//运行结果
//时间:43
//大小:100000
同样创建100000条线程,线程池只用了43毫秒!!!
线程池构造方法参数的含义
public ThreadPoolExecutor(
int corePoolSize,核心线程数
int maximumPoolSize,非核心线程数
long keepAliveTime,时间
TimeUnit unit,时间单位
BlockingQueue workQueue, 队列
!eaoaCtQry. threadFactory, 线程工厂
RejectedExecutionHandler handler 拒绝策略)
自动创建线程池常用的三种方式
- Executors.newSingleThreadExecutor(); //可缓存的线程池 最慢
- Executors.newCachedThreadPool(); //固定大小的线程池 快
- Executors.newFixedThreadPool(); //定时任务(做心跳检测:Eureak /nacos/ dubbos) 慢
这三种方式底层都是实现ThreadPoolExecutor类,但是因为其参数不同造就了不同的功能
Executors.newCachedThreadPool();
有多少任务就创建多少的线程,形成一对一,因此运行最快
核心数为0,非核形数2的31次方-1
参数:
CorePoolSize : 0 核心线程
MaximumPoolSizae :Integer.MAX_VALUE() (值为0x7fffffff即 2的31次方-1) 线程最大总数(核心加非核心)
keepAliveTime:60L 非核心线程存在的时间,类似于员工签定的工作合同的有效期
SynchronousQueue : 队列(长度为一),当任务超出线程数时,进入队列(newCachedThreadPool一般不会出现此情况)
Executors.newFixedThreadPool(int nThreads)
非核心数为0,非核心数为指定数量
存在线程复用,当任务超过线程数量进入队列等待已完成的线程重新执行,因此运行速度慢。
接受指定线程数的指定数量的任务为批次,分批次接受任务
超过线程数量的任务进入队列,该队列与newCachedThreadPool()的SynchronousQueue 队列不一样,长度为Integer.MAX_VALUE(),即2的31次方-1
参数:
CorePoolSize : int nThreads 核心线程
MaximumPoolSizae :int nThreads 线程最大总数(核心加非核心)
keepAliveTime:0 非核心线程存在的时间,类似于员工签定的工作合同的有效期
LinkedBlockingQueue : 长度Integer.MAX_VALUE(),即2的31次方-1
Executors.newSingleThreadExecutor()
只有一个线程接收任务,不断的复用这个线程,其他任务在队列中等待,因此是最慢的
参数
CorePoolSize: 1
MaximumPoolSize: 1
keepAliveTime: 0
LinkedBlockingQueue
自动三种创建线程池的弊端
在一线互联网中不允许使用自带的线程池!!!
- Executors.newCachedThreadPool()
- 当时当任务积压过多,不断的创建线程将会给cpu带来巨大负担的同时也会导致OOM
- Executors.newSingleThreadExecutor()
- 会出现内存溢出现象(oom),当任务积压过多超过LinkedBlockingQueue队列最大值
- Executors.newFixedThreadPool(int nThreads)
- 会出现内存溢出现象(oom),当任务积压过多超过LinkedBlockingQueue队列最大值
总结:在一线互联网中任务量很大,不允许使用自带的创建线程池的方式,需要手动创建
阿里手册中的要求
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors返回的线程池对象的弊端如下:
- FixedThreadPool和singleThreadPool:
允许的请求队列长度为Integer.MAX_vALUE,可能会堆积大量的请求,从而导致OOM。 - cachedThreadPool和 scheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
自定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 10,20,0L,TimeUnit.MILLISECONDS
)
可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同
参数:
CorePoolSize : 10 核心线程
MaximumPoolSizae : 20
keepAliveTime:0 非核心线程存在的时间,类似于员工签定的工作合同的有效期
ArrayBlockingQueue: 10
以CorePoolSize为10为例,当接受任务1-10由核心池接受,11-20由非核心池,21-30进入队列等待,超过30的拒绝
参数设置的预估值一般在预计任务量的两至三倍
提交优先级和执行优先级
前景:当有30个任务需要被执行,他们的执行顺序是怎么样的?顺序是1-10 21-30 11-20 为什么会有这种情况?
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo02 {
public static void main(String[] args) throws Exception{
//自定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,0L,
TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(10));
for (int i = 1; i <= 1000; i++) {
threadPoolExecutor.execute(new Test(i));
}
}
}
class Test implements Runnable{
int i ;
public Test(int i) {
this.i = i;
}
public void run(){
System.out.println(Thread.currentThread().getName()+"---"+i);
try{Thread.sleep(1000L);}catch (InterruptedException e){}
}
}
提交优先级:
- 优先提交到核心池 1-10 ,再提交到队列11-20,最后提交到非核心池21-30
执行优先级:
- 优先执行核心池,再执行非核心池,最后队列的任务被取出执行
因此出现 顺序为1-10 21-30 11-20
源码分析
提交优先级
if (workerCountOf(c) < corePoolSize) {
//判断核心线程池是否已满
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
//线程处于运行态且队列是否已满
//offer 相当于add,add的底层逻辑使用的就是offer,只不过add有两种返回值,Boolean和异常处理语句,而offer只有Boolean
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//核心线程池且队列也满
else if (!addWorker(command, false))
reject(command);
线程池的处理流程
- 用户提交任务
- 判断核心线程池是否已满 否:创建线程池 是:执行步骤3
- 判断队列是否已满 否:将任务存储在队列中 是:执行步骤4
- 线程池是否已满 否:创建线程执行任务 是:执行步骤5
- 按照策略处理无法执行的步骤
执行优先级
流程:
- 用户提交任务 ----- execute
- 创建Work对象 ------ addWorker
- 启动Work对象 ------ runWorker
- 获取任务 -------- getTask
- 启动执行任务 ---- processWorkerExit
产生差异的核心代码
while (task != null || (task = getTask()) != null)
逻辑或,当task != null满足时就不会执行(task = getTask()) != null
线程池中的线程复用原理
在执行中,procWorkerExit 调用 addWorker 实现线程循环利用
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
阻塞队列
意义:使得线程安全
BlockingQueue阻塞队列,线程安全的
BlockingQueue的操作有四种形式,这四种形式的处理方式不同
- 第一种是抛出异常
- 第二种是返回一个特殊值(null或者false,具体取决于操作)
- 第三种是在操作可以成功前,无限期地阻塞当前线程;
- 第四种是在放弃前只在给定的最大时间限制内阻塞
抛出异常 | 特殊值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移出 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
拒绝策略(RejectedExecutionHandler)
当任务数大于最大线程池数与队列容量的和,此时会采用拒绝策略
可以通过ThreadPoolExecutor重载的构造方法进行设置
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- AbortPolicy
- 抛异常 Java中默认的策略
- 直接抛出拒绝异常(继承自RUntimeException),会中断调用者的处理过程
- CallerRunsPolicy
- 在调用者线程中,运行当前被丢弃的任务
- DiscardOldestPolicy
- 丢弃队列中最老的,然后再次尝试提交新的任务
- DiscardPolicy
- 默默丢弃无法加载的任务
- 自定义
- 当以上都不合适时,可以自定义符合场景的拒绝策略,需要实现RejectedExecutionHandler接口,并将自己的逻辑写在RejectedExecutionHandler方法内
前四种策略是java中自带的四种策略
package thread.stu.com;
import java.util.concurrent.*;
public class RejecctedExecutionHandlerDemo {
public static void main(String[] args) throws Exception {
// RejectedExecutionHandler reject = null;
//线程池的四种拒绝策略
//reject = new ThreadPoolExecutor.AbortPolicy(); //默认,队列满了爹任务抛出异常
// reject = new ThreadPoolExecutor.CallerRunsPolicy(); // 如果添加到线程池失败,那么由主线程去执行该任务
// reject = new ThreadPoolExecutor.DiscardPolicy(); // 队列满了丢任务不异常
//reject = new ThreadPoolExecutor.DiscardOldestPolicy(); // 将最早进入队列的任务删除,之后再次尝试加入队列
//自定义拒绝策略
CustomRejectedExecutionHandler reject = new CustomRejectedExecutionHandler();
//自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1,1,0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(2),reject);
for (int i = 1; i <= 1000; i++) {
threadPool.execute(new Test(i));
}
}
}
/**
* 自定义拒绝策略
*/
class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// 核心改造点,由blockingqueue的offer改成put阻塞方法
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test implements Runnable{
int i ;
public Test(int i){
this.i = i;
}
public void run(){
System.out.println(Thread.currentThread().getName()+"---"+i);
try{Thread.sleep(1000L);}catch (InterruptedException e){}
}
}
线程池的五种状态
- Running
能接受新任务以及处理已添加的任务. - Shutdown
不接受新任务,可以处理已经添加的任务· - Stop
不接受新任务,不处理已经添加的任务,并且中断正在处理的任务· - Tidying
所有的任务已经终止, ctl记录的”任务数量”为0, ctl负责记录线程池的运行状态与活动线程数量 - Terminated
线程池彻底终止,则线程池转变为terminated状态
shutdown()/shutdownNow() 的区别
- shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断
- shutdownNow是将线程池的状态设置为STOP,正在执行的任务则会被停止,没有被执行的则返回
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wvS9p4b-1631154768632)(C:/Users/wp990/AppData/Roaming/Typora/typora-user-images/image-20210831003209796.png)]
l = new ThreadPoolExecutor(1,1,0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue(2),reject);
for (int i = 1; i <= 1000; i++) {
threadPool.execute(new Test(i));
}
}
}
/**
-
自定义拒绝策略
*/
class CustomRejectedExecutionHandler implements RejectedExecutionHandler {@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// 核心改造点,由blockingqueue的offer改成put阻塞方法
executor.getQueue().put®;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test implements Runnable{
int i ;
public Test(int i){
this.i = i;}
public void run(){
System.out.println(Thread.currentThread().getName()+"—"+i);
try{Thread.sleep(1000L);}catch (InterruptedException e){}
}
}
## 线程池的五种状态
- Running
能接受新任务以及处理已添加的任务.
- Shutdown
不接受新任务,可以处理已经添加的任务·
- Stop
不接受新任务,不处理已经添加的任务,并且中断正在处理的任务·
- Tidying
所有的任务已经终止, ctl记录的”任务数量”为0, ctl负责记录线程池的运行状态与活动线程数量
- Terminated
线程池彻底终止,则线程池转变为terminated状态
## shutdown()/shutdownNow() 的区别
- shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断
- shutdownNow是将线程池的状态设置为STOP,正在执行的任务则会被停止,没有被执行的则返回