并发编程知识总结

线程的生命周期
  • NEW(新建)状态
    当我们用new关键字创建一个Thread对象的时候,此时它并不处于执行状态,因为没有调用start()方法启动该线程,那么这个线程的状态为NEW状态。
    NEW状态通过start()方法进入RUNNABLE状态。

  • RUNNABLE(可运行)状态
    此时才是真正地在JVM进程中创建了一个进程,但并不会立即得到执行,线程的时候进行都需要听令于CPU的调度,那么我们把这个中间状态称为RUNNABLE状态,也就是说它具备执行的资格,但是并没有真正的执行起来而是等待CPU的调度。

  • RUNNING(运行)状态
    CPU通过轮训或者其他方法从任务队列可执行队列中选中了线程,那么才会进入RUNNING状态,需要说明的一点是一个正在RUNNING状态的线程实际上也是RUNNABLE的,但反过来不成立。

  • BLOCKED(阻塞)状态
    处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。
    阻塞的情况分三种:

  1. 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
  2. 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
  3. 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  • TERMINATED(终止)状态
    线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
调用start()方法时会执行run()方法,如果直接调用run()方法呢?

new一个Thread,线程进入了NEW方法,调用start()方法,会启动一个线程并进入RUNNABLE状态,当分配到CPU的时候就可以工作了。start()方法会执行线程相应的准备工作,然后自动执行run()方法,这是真正的多线程工作。
而如果直接调用run()方法,仅仅是把它当成主线程下的普通方法调用,并不是多线程工作。

守护线程
什么是守护线程
public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {
            while(true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //thread.setDaemon(true);//设置守护线程
        thread.start();
        Thread.sleep(2_000L);
        System.out.println("Main thread finished lifecycle");

    }

如代码所示,当main线程结束的时候,如果没有设置守护线程,那么JVM进程永远不会退出,即使main线程结束了自己的生命周期。
打开注释之后,设置了线程为守护线程的,当main线程结束生命周期后,JVM进程会随之退出运行,当然thread线程也会结束运行。
注意
setDaemon()方法只在线程启动之前才能生效,如果一个线程已经死亡,那么再设置setDaemon则会抛出IllegalThreadStateException 异常。

守护线程的作用

当你希望关闭某些线程的时候,或者是JVM进程退出的时候,一些线程可以自动关闭,此时就可以考虑用守护线程做这些事情。

线程sleep

sleep()方法会使当前线程进入指定毫秒数的休眠,暂停执行。虽然给定了一个休眠时间,但是最终要以系统的定时器和调度器精度为准,休眠有一个重要的特性,那就是其不会放弃monitor锁的所有权。

线程yield

yield()方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的CPU资源,但是这个yield()方法知识一个提示,CPU调度器不会担保每一次都能满足yield提示。
调用yield()方法,会使当前线程从RUNNING状态切换为RUNNABLE状态。

线程sleep与线程yield的区别
  • sleep会导致当前线线程暂定指定的时间,并不会引CPU时间片的消耗。
  • yield只是对CPU调度器的一个提示,当CPU没有忽略这个提示的时候,会引起线程上下文的切换。
  • sleep会使线程短暂的block,会在给定的时间内释放CPU资源。
  • yield会使当前线程从RUNNING状态切换为RUNNABLE状态。
  • sleep会百分百的完成给定时间的休眠,而yield不一定能担保。
  • 一个线程sleep,另外一个线程调用interrupt会捕捉到中断信息,而yield不会
线程join

join某个线程A,会使当前线程B进入等待,直到线程A生命周期结束,或者达到给定的时间,那么,再次期间B线程是处于BLOCKED的,而不是A线程。

线程interrupt方法

调用interrupt打断阻塞。打断一个线程并不等于该线程的生命周期结束,仅仅时打断当前线程的阻塞状态

线程isInterrupt方法

判断当前线程是否被中断。

线程的优先级

进程有进程的优先级,线程同样有线程的优先级,理论上线程优先级高的有优先被CPU获取到的机会,但实际上往往不会如你所愿,设置线程优先级是一个hint操作。
如果CPU比较忙的时候,优先级高的线程可能会获取到更多的CPU时间片,但闲的时候优先级高低几乎不会有任何作用。
通过setPriority方法设置线程优先级。

sleep()与wait()方法的区别

相同点

  • 两者都可以使线程阻塞。
  • 两者均是中断方法,被中断后都会收到中断异常
    不同点
  • sleep是Thread类的静态方法,wait是Object类的方法
  • sleep不释放锁,wait释放锁
  • wait必须要在同步代码块中进行,而sleep不需要
  • sleep方法短暂休眠之后会主动退出阻塞,而wait方法则需要被其他线程中断后才能退出阻塞,即notify()或者notiayAll()方法。
notify()和notifyAll()方法的区别

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。

notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

notify()与wait()方法的注意事项
  • 线程执行了某个对象的wait方法之后,会加入与之对应的wait set中,每一个对象的锁都有一个与之关联的wait set。
  • 当线程进入wait set中之后,notify可以将其唤醒,使其从wait set中弹出,同时中断wait中的线程也会将其唤醒
  • 必须在同步代码块中进行wait和notify方法,因为执行wait和notify的前提是必须持有同步方法的monitor锁的所有权。
程序死锁
  1. 交叉锁可导致程序出现死锁
    线程A持有R1的锁等待获取R2的锁,线程B持有R2的锁等待获取RI的锁(典型的哲学家吃面),这种情况最容易导致程序发生死锁的问题。

  2. 内存不足
    当并发请求系统可用内存时,如果此时系统内存不足,则可能会出现死锁的情况。举个例子,两个线程T1和T2,执行某个任务,其中T1已经获取了10MB内存,T2获取了20MB内存,如果每个线程的执行单元都需要30MB的内存,但是剩余可用的内存刚好为20MB,那么两个线程有可能都在等待彼此能够释放内存资源。

  3. 一问一答式的数据交换
    服务端开启某个端口,等待客户端访问,客户端发送请求立即等待接收,由于某种原因服务端错过了客户端的请求,仍然在等待-问-答式的数据交换, 此时服务端和客户端|都在等待着双方发送数据(笔者在刚参加工作的时候就犯过这样的错误)。

  4. 数据库锁
    无论是数据库表级别的锁,还是行级别的锁,比如某个线程执行for update语句退出了事务,其他线程访问该数据库时都将陷人死锁。

  5. 文件锁
    同理,某线程获得了文件锁意外退出,其他读取该文件的线程也将会进人死锁直到系统释放文件句柄资源。

  6. 死循环引起的死锁
    程序由于代码原因或者对某些异常处理不得当,进入了死循环,虽然查看线程堆栈信息不会发现任何死锁的迹象,但是程序不工作,cCrU 占有率又居高不下,这种死锁一股称为系统假死,是一种最为致命也是最难排查的死锁现象,由于重现困难,进程对系统资源的使用量又达到了极限,想要做出dump有时候也是非常困难的。

线程的调度策略

线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

  1. 线程体中调用了 yield 方法让出了对 cpu 的占用权利

  2. 线程体中调用了 sleep 方法使线程进入睡眠状态

  3. 线程由于 IO 操作受到阻塞

  4. 另外一个更高优先级线程出现

  5. 在支持时间片的系统中,该线程的时间片用完

线程池

线程池的优点
  1. 降低资源消耗,通过重复使用已创建的线程降低线程的创建销毁造成的消耗
  2. 提高响应速度,当任务到达的时候,可以直接拿线程直接执行,跳过了创建线程的步骤
  3. 提高线程的可管理行,线程是稀缺资源,如果无限制的创建不仅会消耗资源,还会降低系统的稳定性。把线程交给线程池统一管理,统一分配调度优化和监控。
创建线程池
  • Executors.newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • Executors.newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
  • Executors.newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • Executors.newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

Execcutors风险

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
ThreadPoolExecutorThreadPoolExecutor线程池

ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
核心参数
corePoolSize :核心线程数
maximumPoolSize:最大工作线程数
workQueue:当达到核心线程数的时候,进入队列中等待

其余参数
keepAliveTime:在队列中等待的时间,超过等待时间对线程销毁回收
unit:等待时间单位
threadFactory:为线程池提供创建新线程的线程工厂
handler:超过最大线程的拒绝策略

线程的拒绝策略

AbortPolicy

丢弃任务并抛出RejectedExecutionException异常
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

DiscardPolicy

丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略

DiscardOldestPolicy

丢弃队列最前面的任务,然后重新提交被拒绝的任务。
此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。

CallerRunsPolicy

由调用线程处理该任务,如果任务被拒绝了,则由调用线程(提交任务的线程,例如main())直接执行此任务

AQS

AQS源码分析

Java并发工具类
名称描述详细
CountDownLatch同步计数器初始化时,传入需要计数的线程等待数,并用 await() 阻塞当前线程,其他线程中可以调用 countDown()方法让计数器减一,当计数器为 0 时,则放行
CyclicBarrier栅栏让一组线程达到某个屏障被阻塞,一直到组内最后一个线程达到屏障时,屏障开放,所有被阻塞的线程才会继续运行
Semaphore信号量/令牌桶类似于令牌桶算法,通过控制令牌数量,让持有令牌的线程运行,未持有的等待,实现多线程的并发控制,常用于流量控制
Exchange两个线程的数据交换器通过在两个线程中定义同步点,当两个线程都到达同步点时,交换数据结构
CountDownLatch 和 cyclicBarrier 的区别
CountDownLatchcyclicBarrier
等待线程数目一个线程等待,直到其他线程都完成且调用 countDown(),才继续执行所有线程都等待,直到所有线程都准备好进入 await()后,全部放行
使用次数只能使用一次可以调用 reset() 方法重置,能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
拓展性提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值