Java多线程

本文详细介绍了Java多线程的相关概念,包括并行与并发的区别、Java线程的7种状态及其转换,以及线程创建的4种方式。讨论了start()和run()的区别,实现Runnable接口的优势,线程池的使用原因和execute()与submit()的区别。此外,还深入探讨了Atomic类、线程同步机制如synchronized和volatile,以及锁的类型和优化技术,如公平锁、非公平锁、自旋锁、读写锁等。最后,文章提到了ThreadLocal的原理和应用场景,以及ArrayBlockingQueue阻塞队列的实现原理。
摘要由CSDN通过智能技术生成

目录

*并行与并发

  1. 并行:在同一时刻,有多个线程在多个处理器上同时执行;
  2. 并发:在同一时刻,只能有一个线程执行,但多个线程被快速轮转,使得在宏观上具有多线程同时执行的效果;

*Java线程的7种状态

  1. 新建状态(New):通过实现Runnable接口或继承Thread类,new一个线程实例后,线程就进入了新建状态;
  2. 就绪状态(Ready):线程对象创建成功后,调用该线程的start()函数,线程进入就绪状态,该状态的线程等待获取CPU时间片;运行状态下时间片用完或调用了Thread.yield()函数,线程回到就绪状态;
  3. 运行状态(Running):线程获取到了CPU时间片,正在运行自己的代码;
  4. 等待状态(Waiting):1 运行状态的线程执行object.wait()、t.join()、LockSupport.park()等,将进入等待状态,其中object.wait()和t.join()会令JVM把该线程放入锁同步队列,LockSupport.park()不会释放当前线程占有的锁资源;2 等待状态的线程不会被分配CPU时间片,等待被主动唤醒,否则一直处于等待状态;2 唤醒:通过notify()、notifyAll()、调用join的线程t执行完毕,会唤醒锁等待队列中的线程,出队的线程回到就绪状态;另一个线程调用执行LockSupport.unpark(t)唤醒指定线程,该线程回到就绪状态;
  5. 超时等待状态(Timed Waiting):1 与等待状态的区别是:超时等待状态的线程到达指定时间后会自动唤醒;2 以下函数会进入超时等待状态:object.wait(long)、t.join(long)、sleep(long)、LockSupport.parkUtil(long),其中object.wait(long)、t.join(long)会令JVM把线程放入锁等待队列;3 唤醒:超时时间到了,或通过notify()、notifyAll()、调用join的线程t执行完毕,会唤醒锁等待队列中的线程,出队的线程回到就绪状态;非锁等待队列中的线程等超时了就回到就绪状态;
  6. 阻塞状态(Blocked):运行状态的线程获取同步锁失败或发出IO请求,进入阻塞状态,如果是获取同步锁失败则JVM将该线程放入锁同步队列;
  7. 终止状态(Terminated):线程执行结束或执行过程中因异常意外终止就进入终止状态,线程一旦终止就不能复生,这是不可逆的过程;

(yield让步、join加入、park停)

一张图总结Java线程状态

在这里插入图片描述

*Java线程创建的4种方式(优化Callable)

  1. 继承Thread类:1.1 定义一个Thread类的子类,并重写run()方法,该run()方法的方法体即线程执行体;1.2 创建Thread子类的实例,即创建线程对象;1.3 调用Thread对象的start()方法来启动该线程;
  2. 实现Runnable接口: 2.1 定义一个Runnable接口的实现类,重写接口的run()方法,该run()方法的方法体即线程执行体;2.2 创建Runnable实现类的对象;2.3 使用Runnable实现类的对象作为Thread对象的target,该Thread对象即线程对象;2.4 调用Thread对象的start()方法来启动线程;
  3. 通过Callable和FutureTask创建线程:3.1创建Callable接口的实现类,并重写call()方法,该call()方法即线程执行体,并且有返回值;3.2创建Callable实现类的实例,使用FutureTask对象来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;3.3使用FutureTask对象作为Thread对象的target创建并启动新线程;3.4调用FutureTask对象的get()方法来获得线程执行结束后的返回值;
  4. 基于线程池:4.1 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建和销毁线程会大大降低系统效率;4.2 我们可以通过线程池来实现线程的复用,线程池:一个容纳多个线程的容器,其中的线程可以反复使用,无需反复创建销毁线程而消耗过多资源;4.4 ExecutorService pool = Executors.newFixedThreadPool(10);pool.execute(new Runnable()…);

*start()和run()的区别

  1. 调用Thread类的start()方法来启动线程,真正实现了多线程,这时此线程处于可运行状态,并没有真正运行,一旦得到cpu时间片,就开始运行并执行run()方法,而此时无需等待run()方法执行完毕,即可继续执行主线程下面的代码;
  2. run()方法只是Thread类的一个普通方法,如果直接调用run()方法,程序中依然只有主线程这一个线程,程序执行路径还是只有一条,要等待run()方法执行完毕后才可继续执行下面的代码;

*实现Runnable接口比继承Thread类的优势(可改进)

  1. 可以避免Java单继承的局限性;
  2. 代码可被多个线程共享,代码和线程独立;
  3. 线程池只能放入实现Runnable或Callable接口的对象,不能放入继承Thread类的对象;

*为什么使用线程池

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
  2. 提高响应速度。当任务到达时,不需要等到线程创建就能立即执行;
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统⼀的分配、调优和监控;

*执行execute()方法和submit()方法的区别?(可改进)

  1. execute() 用于提交没有返回值的任务;
  2. submit() 用于提交有返回值的任务。线程池会返回⼀个 Future 类型的对象,可以通过 Future.get() 来获取返回值;

*Runnable和Callable的区别(可改进)

  1. Callable 的实现方法是 call(),Runnable 的实现方法是 run();
  2. Callable 任务执行后有返回值。Callable 与 Future 配合使用,运行 Callable 任务和获取一个 Future 对象表示异步计算后的结果;Runnable 任务执行后没有返回值;

如何创建线程池

*方法1:通过Executor框架的工具类Executors来实现

  1. newFixedThreadPool(固定大小的线程池):如果任务数量大于等于线程池中线程的数量,则新提交的任务将在阻塞队列中排队,直到有可用的线程资源;
  2. newSingleThreadExecutor(单个线程的线程池):1 确保池中永远有且只有一个可用的线程;2 在该线程停止或发生异常时,会创建一个新线程代替该线程继续执行任务;
  3. newCachedThreadPool(可缓存的线程池):1 提交新任务时如果有可重用的线程,则重用它们,否则创建一个新线程并将其添加到线程池中;2 线程池的keepAliveTime默认60秒,超过60秒未被利用线程会被终止并从缓存中移除,因此在没有线程任务运行时,newCachedThreadPool不会占用系统资源;3 在有执行时间很短的大量任务需要执行的情况下,newCachedThreadPool能很好地复用线程资源来提高运行效率;
  4. newScheduledThreadPool(可做任务调度的线程池):可定时调度,可设置在给定延迟时间后执行或定期执行某个线程任务;
  5. newWorkStealingPool(足够大小的线程池):1 用于创建持有足够数量线程的线程池来达到快速运算的目的;2 JDK根据当前的运行需求向操作系统申请足够多的线程;

例如

ExecutorService executor = Executors.newSingleThreadExecutor();

*方式2:通过ThreadPoolExecutor构造方法创建(饱和策略优化)

《阿里巴巴Java开发手册》中建议线程池不使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式一是规避资源耗尽的风险,二是可以更加明确线程池的运行规则。

Executors 返回线程池对象的弊端如下:1. newFixedThreadPool 和 newSingleThreadExecutor:允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM(OutOfMemoryError);2. newCachedThreadPool 和 newScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM;在这里插入图片描述
以上是ThreadPoolExecutor类提供的4个构造方法,下面分析最长的那个,其他三个采用默认参数:

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.keepAliveTime = unit.toNanos(keepAliveTime);
	this.workQueue = workQueue;
	this.threadFactory = threadFactory;
	this.handler = handler;
}

ThreadPoolExecutor 构造函数参数分析:

  1. corePoolSize:核心线程数;
  2. maximumPoolSize : 线程池中能拥有最大线程数;
  3. workQueue : 用于缓存任务的阻塞队列;
  4. keepAliveTime :当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime 才会被回收销毁;
  5. unit : keepAliveTime 参数的时间单位;
  6. threadFactory :executor创建新线程时会用到;
  7. handler :饱和策略;

corePoolSize、maximumPoolSize、workQueue 三者关系

  1. 如果没有空闲线程执行该任务且当前运行的线程数少于 corePoolSize ,则添加新线程执行该任务;
  2. 如果没有空闲线程执行该任务且当前的线程数等于 corePoolSize ,同时阻塞队列未满,则将任务入队列,而不添加新线程;
  3. 如果没有空闲线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize ,则创建新线程执行任务;
  4. 如果没有空闲线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize ,则根据构造函数中的 handler 指定的饱和策略来拒绝新的任务。

ThreadPoolExecutor 饱和策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时, ThreadPoolTaskExecutor 定义了⼀些饱和策略:

  1. ThreadPoolExecutor.AbortPolicy :抛出RejectedExecutionException,意味着需要对这种情况进行适当的错误处理,默认的处理策略;
  2. ThreadPoolExecutor.CallerRunsPolicy:用调用者所在的线程来执行任务。即被拒绝的任务在主线程中运行,别的任务只能在被拒绝的任务执行完之后才会继续被提交到线程池执行。适用于对于那些不允许丢失任务的业务场景,例如在金融交易系统中,每一笔交易都不能被丢弃
  3. DiscardPolicy:直接丢弃任务,不给调用者任何通知,适用于不重要或可重试的任务;
  4. DiscardOldestPolicy:添加新任务前丢弃阻塞队列中最老的任务,可以确保重要的新任务有机会被执行;

在这里插入图片描述

案例

/**
* 定义⼀个简单的Runnable类,需要⼤约5秒钟来执⾏其任务。
*/
public class MyRunnable implements Runnable {
   
    private String command;
    public MyRunnable(String s) {
   
        this.command = s;
    }
    @Override
    public void run() {
   
        System.out.println(Thread.currentThread().getName() + " Start.Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End.Time = " + new Date());
    }
    private void processCommand() {
   
        try {
   
            Thread.sleep(5000);
            System.out.println("执行command" + command);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值