文章目录
线程与进程
线程和进程的区别
- 根本区别: 进程是操作系统进行资源分配的基本单位;线程是处理器任务调度和执行的基本单位。
- 资源开销:进程拥有独立的代码和数据空间,进程间切换有较大开销;线程共享所属进程的资源,但拥有自己的程序计数器和虚拟机栈,线程间切换开销小。
- 包含关系:一个进程可以有多条线程;一条线程只能属于一个进程。
- 影响关系:一个进程崩溃不会影响到其他进程;一个线程崩溃则整个进程都会死掉,因此多进程更加健壮。
- 执行过程:每个独立的进程都有程序运行入口、顺序执行序列、程序出口;而线程不能单独执行,必须由应用程序来提供多线程的执行控制。
创建线程的三种方式
- 实现Runable或Callable接口:仅仅实现接口,还可以继承其他类。多个线程可共享一个target对象,适合多个相同线程来处理同一份资源;但是编程复杂,访问当前线程必须使用
Thread.currentThread()
。 - 继承Thread类:囿于Java的单继承而无法继承其他类;但是变成简单,且仅使用
this
就能获取当前线程。
Runable和Callable区别:
- Callable重写call(),Runable重写run()。
- Callable有返回值,Runable无。
- call()可以抛出异常,run()不行。
- 运行Callable任务可以拿到一个Future对象,能够检查计算是否完成,获取计算结果,也可以取消任务的执行。
- 工具类Executors可以实现Runable对象和Callable对象之间的相互转换。
线程状态转换
- 新建状态New:线程对象创建后就进入了新建状态,比如
new Thread()
。 - 就绪状态Runable:当调用
start()
后线程进入就绪状态。就绪状态只是说明当前线程已经随时准备好接受cpu的调度执行,而不是说会立即执行。 - 运行状态Running:cpu调度执行就绪态的线程后,线程才真正执行,进入运行状态。
- 阻塞状态Blocking:运行状态的线程由于某些原因,暂时放弃cpu使用权,进入阻塞状态。直到其重新进入就绪状态,才有机会重新被cpu调用进入运行态。阻塞状态根据阻塞原因不同可以分为三类:
- 等待阻塞:运行中线程执行
wait()
,使本线程进入等待阻塞状态。JVM将线程放入等待序列waitting queue。 - 同步阻塞:获取
synchronized
锁失败,进入同步阻塞状态。JVM将线程放入锁池lock pool中。 - 其他阻塞:调用
sleep()
、join()
,或者发出了IO请求时,线程会进入阻塞状态。sleep和join超时或终止,或IO结束,线程重新就绪。
- 等待阻塞:运行中线程执行
- 死亡状态Dead:线程执行完毕或因异常而终止,结束生命周期。
- 正常结束,run()或call()执行完毕。
- 异常结束,抛出一个未捕获的异常。
- 强制终止,调用stop()。不推荐,容易死锁。
死锁的四个条件与解决方案
死锁指两个或以上的线程因资源竞争或彼此通信造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。
- 互斥条件:该资源任意时刻只能被一个线程占用
- 破坏:临界资源需要互斥访问无法破坏
- 请求与保持:一个进程在请求资源被阻塞时不释放已有资源
- 破坏:让线程一次性申请所有资源
- 不剥夺:线程已获取的资源不能被其他线程强制剥夺,只能由其使用完毕后自主释放
- 破坏:占用部分资源的线程申请其它资源被阻塞时,主动释放自己已占用的资源
- 循环等待:若干进程间形成首尾相接的循环资源等待现象
- 破坏:按一定顺序申请资源,并以相反顺序释放资源。
此外还有锁排序法:
指定锁的获取顺序。比如,只有在获取A锁的前提下才能资格获取B锁。
或者可以使用ReentrantLock.try(long,TimeUnit)
来申请锁。
shutdown()、isShutdown()
- shutdown():关闭线程池,线程池状态变为SHUTDOWN。线程池不再接受新任务,但是老任务会执行完毕。
- shutdownNow():管理线程池,线程池状态变为STOP。终止当前任务并返回正在等待执行的任务列表List。shutdownNow原理是遍历线程池内所有的工作线程,并逐个调用线程的interrupt方法来中断线程,所以无法相应中断的任务可能永远无法终止。
- isShutdown():当调用shutdown()方法后返回true。
- isTerminal():调用shutdown()且所有提交任务完成后返回ture。
sleep()和wait()区别
两者都可以暂停线程,但是存在区别。
- sleep()是Thread类的静态方法,通过
Thread.sleep()
调用;wait()是Object类方法,必须与synchronized
关键字一起使用。 - sleep()没有释放锁;wait()释放锁。
- sleep()通常被用于暂停;wait()用户线程间的通信/交互。
- sleep()的线程在时间到了自动从阻塞态变为就绪态;wait()的线程必须由其他线程调用同一个对象的
notify()
或notifyAll()
才会唤醒。
另外Thread类中还有一个静态的yield()
方法。可以暂停当前正在执行的线程,让其他具有相同优先级的线程执行。但是它只能保证当前线程放弃cpu,而不能保证其他线程占用cpu。也有可能执行yield()
的线程进入暂停状态后马上又被执行。
volatile
两层语义:
- volatile保证了变量对于所有线程的可见性:当volatile变量被修改时,新的值会对所有线程立即更新。可以理解为在多线程环境下使用volatile修饰的变量的值一定是最新的。
- JDK1.5后,volatile完全避免了指令重排优化,实现了有序性。
volatile的原理是通过lock指令,相当于一个内存屏障,令lock指令后的指令无法重排序到内存屏障前的位置。同时lock可以使本线程的volatile变量值立即写入主内存并使其他线程的该变量无效化,这样其他线程就必须重新从主存读取变量值。
守护线程
守护线程是运行在后台的一种特殊的线程,独立于控制终端并且周期的执行某种任务或者等待处理某些发生的事件。Java中的垃圾回收线程就是特殊的守护线程。
CAS
CAS是什么
Compare and Swap,比较并交换,是一条cpu同步原语。是硬件对于并发的支持,用于管理对于共享数据的并发访问。
CAS是一种无锁的非阻塞算法的实现,包括3个操作数:需要读写的内存值V,旧的预期值A,要更新为的值B。当且仅当V的值为A时,将A更新为B。否则,不会进行任何操作。
CAS的缺陷
- ABA问题。 A变为B又重新变为A的情况,此A已非彼A,数据相同但意义可能不同,即使CAS修改成功也可能存在问题。
- 循环时间长,开销大。 自旋CAS如果一直不成功一直循环执行,开销非常大。所以通常会有自旋次数的限制。
- 只能保证一个变量的原子操作。 对多个变量进行操作时,无法直接保证原子性。可以通过互斥锁、或多个变量封装为对象通过AtomicReference来保证原子性。AtomicReference通过CAS和volatile来实现,前者保证赋值的原子性,后者保证可见性。
synchronized
底层实现原理
synchronized同步代码块是通过monitorenter和monitorexit指令,前者指向同步代码块的起始位置,后者指向同步代码块的结束位置。当执行到enter指令,线程尝试获取对象的monitor。内部包含一个计数器,为0可以成功获取,然后计数器+1,执行完exit指令后计数器置0并释放锁。如果获取锁失败,那么就阻塞等待。
monitor位于每个Java对象的对象头中,synchronized就是通过这种方式获取锁的。这也是为什么所有的Java对象都能作为锁的原因。
synchronized修饰的方法是通过一个ACC_SYNCHRONIZED标识,该标识指明方法是一个同步方法,然后JVM执行对应的同步调用。
锁升级的原理(锁膨胀)
在锁对象的对象头中有一个threadid字段,第一次访问时threadid为空,jvm让线程持偏向锁,并将threadid设置为该线程的id。后面再次进入就会判断threadid与线程id是否一致,一致则直接进入(可重入锁),不一致将偏向锁升级为轻量级锁。通过自旋获取锁,自旋一定次数仍未获取到锁,则将锁升级为重量级锁。
除了升级之外,锁也是可以降级的。在全局安全点中,执行清理任务时会尝试锁降级。锁降级会重置锁对象的对象头,以及ObjectMonitor并将其放入全局空闲列表,等待后续使用。
为什么要引入偏向锁和轻量级锁
因为重量级锁依赖于底层的同步函数,这些同步函数会涉及到核心态与用户态的切换、进程上下文的切换,开销非常大。而在很多情况下,获取锁的可能只有一个线程,用重量级锁非常不划算。因此,引入偏向锁和轻量级锁来降低没有竞争时的锁的开销。
锁消除
在编译阶段,消除不存在竞争关系的锁。
锁粗化
扩大锁的范围,避免反复加锁释放锁而引起性能的下降。
在JDK1.6后优化了synchronized的实现方式,使用了偏向锁->轻量级锁->重量级锁的方式,主要是为了降低锁带来的性能消耗。
自旋锁与自适应自旋锁
自旋锁:轻量级锁失败后,为了避免线程真正在操作系统层面挂起,会采用自旋锁。通常,共享数据的锁定时间较短,切换线程并不划算。因此,让线程循环等待锁的释放,并不让出cpu。如果顺利得到锁,就进入临界区,否则就挂起。但若一直得不到锁,也会增加开销。
自适应自旋锁:自旋次数不再固定,而是由此锁的上一次自旋时间和锁当前拥有者的状态来决定的。
和volatile的区别
- volatile只能用于变量级别;synchronized可以作用于变量、方法、类级别。
- volatile本质上是告诉JVM当前变量的值需要到主存中去读取;synchronized是锁定当前的变量值,阻塞其他想要访问此变量的线程。
- volatile仅能实现变量的修改可见性,不能保证原子性;synchronized可以保证变量的修改可见性与原子性。
- volatile不会阻塞线程;synchronized会阻塞其他线程。
- volatile修饰的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
和Lock的区别
- Lock只能用于代码块加锁;synchronized可以用于变量、方法、代码块、类级别
- Lock需要手动加锁释放锁,使用不当容易死锁;synchronized无需手动释放锁,不会死锁。
- Lock可以知道有没有成功获取锁(通过
tryLock()
);synchronized不可以。
和ReentrantLock的区别
synchronized和ReentrantLock都是可重入锁,也就意味着一个线程可以多次获取同一把锁。比如一个线程执行一个带锁的方法,该方法又调用了另外一个需要相同锁的方法,此时该线程可以直接执行调用的方法,无需重新获取锁。同一个线程每进入一次,锁计数器+1,要等到计数器下降为0才能够释放锁。
- synchronized是依赖于JVM实现的,JDK1.6中在虚拟机层面对其做了很多优化;ReentrantLock依赖于API,需要lock()、unlock()配合try/catch来完成。
- ReentrantLock相比synchronized增加了一些高级功能。
- 等待可中断:通过
lock.lockInterruptibly()
实现。在等待的线程可以放弃等待,去做其他的事情。 - 可以通过构造函数来指定公平/非公平锁。公平锁就是先等待的线程先获取锁,而synchronized只能是非公平锁。
- ReentrantLock的线程对象可以注册在指定的Condition中,两者结合可以在使用
notify()、notifyAll()
时实现选择性通知。
- 等待可中断:通过
但是请注意,如果不是需要使用ReentrantLock的高级功能,就尽量使用synchronized。因为synchronized是JVM实现的一种锁机制,被JVM原生的支持,并做了大量的优化,并且不用担心因为忘记释放锁而导致死锁问题。
ReadWriteLock
ReenTrantLock某些时候可能有局限,可能本身是为了防止A写B读造成的数据不一致,但是C读D读的情况下仍然加锁就降低了系统的性能。因此诞生了ReadWriteLock,它是一个接口,ReenTrantReadWriteLock是具体的实现,实现了读写的分离。读和读之间是不会互斥的,提升了读写性能。
ThreadLocal
是什么?
线程本地变量。如果创建了一个ThreadLocal变量,那么访问这个变量的每一个线程都会有一个这个变量的本地拷贝,多线程操作时其实是操作自己本地内存的变量,从而起到线程隔离的作用,保证了线程安全。应用场景:数据库连接池、会话管理
实现原理
Thread类中有一个类型为ThreadLocalMap的实例变量,内部维护着Entry数组,每个Entry代表一个完整的对象,Key是ThreadLocal,value是其泛型值。每个线程在给ThreadLocal值时其实都存放在自己的ThreadLocalMap中,读取也是在自己的map中找。
内存泄漏问题
ThreadLocalMap中的key使用的是ThreadLocal的弱引用,也就是说只要GC,不管JVM内存是否够用,都会被回收。而ThreadLocalMap和Thread的生命周期是一样的,当key被回收而value还在时,就出现了内存泄漏。所以我们在使用完ThreadLocal后,及时调用remove()
来释放空间
应用场景
ThreadLocal的应用场景最好满足两个条件,一是该对象不需要在线程间被共享,而是该对象需要在线程内被传递。
- 利用ThreadLocal变量存储用户信息,避免每一个需要用户信息的接口都要传入HttpServletRequest参数。后续的请求链路当中都是一个线程,随时都可以通过这个ThreadLocal来获取用户信息。
线程池
为什么要用线程池?
线程池提供了一种限制和管理资源,并且维护一些基本的统计信息。
- 降低资源消耗。通过重复利用已创建的线程来降低线程创建和销毁带来的资源消耗。
- 提高响应速度。任务到达时无需等待线程创建。
- 提高线程的可管理性。可以对线程进行统一的分配、调优、监控。
另外,线程池中一个线程就是一个worker,会通过while循环从阻塞队列获取任务,通过置换worker中的runnable对象,运行其run方法起到线程置换效果,避免了多线程的频繁线程切换,提高程序的性能。
execute() 和 submit()
- execute()用于提交不需要返回值的任务,无法判断任务是否被线程池成功执行。
- submit()用于提交需要返回值的任务。线程池会返回一个future对象,
- 通过future可以得知任务是否执行成功
- 并可以通过future的get()来获取返回值,get会阻塞当前线程直到任务完成
- get(long time, TimeUnit unit)可以在阻塞线程一段时间后立即返回,任务可能并未执行完成
线程池的核心参数
- corePoolSize:核心线程数量。
- maximumPoolSize:最大线程数量。
- keepAliveTime:非核心线程存活时间。非核心线程在keepAliveTime内没有运行任务就会消亡。
- workQueue:阻塞队列,用于存放线程任务。
- defaultHandler:饱和策略。ThreadPoolExecutor类一共有4种饱和策略,通过实现RejectedExecutionHandler。
- AbortPolicy:直接丢弃报错,默认策略。
- DiscardPolicy:直接丢弃不报错。
- DiscardOldestPolicy:丢弃workQueue的队首任务,并将最新任务加入队列。
- CallerRunsPolicy:线程池之外的线程直接调用run执行。
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
- ThreadFactory:线程工厂。
另外要注意一点,当线程池新增任务时,核心线程满了之后,任务会优先放入阻塞队列,在阻塞队列满了之后才会创建非核心线程,非核心线程也满了就会执行饱和策略。
常用线程池
- CachedThreadPool --> SynchronousQueue
核心线程数为0,最大线程数是Integer.Maximum,也就是说工作线程数量基本上没有限制,可以灵活的往线程池中添加线程。创建出来的线程会被缓存,以尝试重复利用。若工作线程空闲了指定的时长(默认1min),该工作线程将自动终止。
使用newCachedThreadPool时要注意提交的任务数量,否则大量线程同时运行很可能OOM。 - FixedThreadPool --> LinkedBlockingQueue
典型且优秀的线程池,创建一个指定工作线程数量的线程池。每提交一个任务就创建一个工作线程,如果工作线程数量达到最大数,将提交的任务存储到池队列。
它能够提高程序效率并节省线程创建销毁的性能,但是当线程池空闲,它不会释放工作线程,还会占用一定的系统资源。 - SingleThreadPool --> LinkedBlockingQueue
单线程的线程池,可以保证任务按照某种顺序执行(FIFO、LIFO)。如果一个线程异常结束,会有另一个取代它。在任意时间不会有多个线程同时活动。 - ScheduleThreadPool --> DelayedWorkQueue
定长线程池,支持定时以及周期性的任务。
线程池常用阻塞队列
- LinkedBlockingQueue:可指定长度。默认长度为Integer.maximum,可以认为是无界队列。FixedThreadPool和SingleThreadPool因为自身的线程数量固定,无法创建更多线程,因此需要使用没有限制的阻塞队列来存放任务。
- SynchronousQueue:存储长度为0,对应可以无限创建线程的CachedThreadPool。一旦有任务被提交就直接转发给线程或者新建线程来执行。所以我们自定义线程池使用SynchronousQueue队列时,如果不希望任务被拒绝,要注意将最大线程数尽量设置的大一些。
- DelayedWorkQueue:对应ScheduleThreadPool,内部元素并不是按照放入时间排序,而是按照延迟的时间长短进行排序,使用堆。
Executor 和 Executors区别
在Java中通常什么什么s这样的类都是工具类,比如Utils、Collections等等。Executors可以按照我们的需求来创建不同的线程池,来满足我们的业务需求。
Executor接口对象能够执行我们的线程任务。ExecutorService接口继承了Executor并进行了拓展,我们能够获取到任务的执行状态以及返回值。使用ThreadPoolExecutor可以创建自定义线程池,使用Future表示异步计算的结果。
线程池在业务中的应用场景
-
场景1:快速响应用户的请求
描述:用户发起实时请求,需要快速响应。比如查看某个商品的价格、库存、图片、优惠信息等等,那么我们就需要将上述信息聚合起来展示给用户。
分析:用户肯定希望这个响应越快越好,如果响应比较慢,很可能用户就放弃查看这个商品了。而面向用户的功能聚合通常比较复杂,往往伴随着调用间的级联。因此,采用线程池,将调用封装成任务并行执行来缩短响应时间,是一个不错的选择。此外,使用的线程池也是有考量的。此种场景下需要尽快的响应用户的请求,因此不应设置阻塞队列来缓冲并发任务,应该调高corePoolSize和maxPoolSize去尽可能创建多的线程来快速执行任务。
-
场景2:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如统计全国所有门店的商品属性用于后续的营销策略分析。
分析:这种场景下需要执行大量的任务,所以也应该使用多线程来进行并行计算。但是相对于实时的用户请求,此场景对于响应速度的要求并不高。因此,我们更应该关注的是如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先。所以,我们需要设计队列来缓冲并发任务,调整合适的corePoolSize。如果设置的线程数过多,可能会导致上下文频繁切换、线程创建的性能消耗问题。
AQS(AbstractQueuedSynchronizer)
什么是AQS?
- AQS,队列同步器,是一个锁框架。它定义了锁的实现机制,并开放出需要扩展的地方让子类来实现。比如开放state字段来让子类决定是否能够获取到锁,获取不到锁的线程AQS会自动进行管理,无需子类锁关心。
- AQS底层由同步队列(双向循环链表)+条件队列组成。同步队列管理着获取不到锁的线程的排队与释放,条件队列是对同步队列的补充。条件队列中的线程是在获取到锁之后,运行中发现进一步的条件还不满足导致不能往下执行,被迫无奈放弃之前获取到的共享资源的锁,阻塞并进入条件队列。当条件满足后被唤醒并重新加入同步队列的队尾,进一步等待获取共享资源锁并执行。
- AQS围绕着两个队列一共有4大场景:获取锁、释放锁、条件队列的阻塞、条件队列的唤醒。
AQS的设计模式
AQS队列同步器是基于模板方法模式来设计的,自定义同步器需要继承AbstractQueuedSynchronizer
并重写下面指定方法,并将AQS组合在自定义同步组件的实现中,在调用模板方法时会调用使用者重写的方法。
- isHeldExclusively():该线程是否正在独占资源。用到condition才需要去实现它。
- tryAcquire(int): 用独占方式尝试获取资源,true成功,false失败。
- tryRelease(int):独占方式尝试释放资源,true,false。
- tryAcquireShared(int):共享方式尝试获取资源。负数表示失败,0成功但没剩余可用资源;正数表成功且有剩余资源。
- tryReleaseShared(int):共享方式尝试释放资源。true,false。
AQS两种资源共享方式
- Exclusive:独占,只有一个线程能同时使用,比如ReentrantLock。分为公平锁和非公平锁。
- Share:共享,多个线程可同时执行,比如ReadWriteLock、Semaphore。
AQS组件
- Semaphore(信号量):允许多个线程同时访问。synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,Semaphore允许多个线程同时访问某个资源。
- CountDownLatch(倒计时器):同步工具类,用于协调多个线程间的同步。这个工具通常用来控制线程的等待,可以让某个线程等待直到倒计时结束再开始执行。
- CyclicBarrier(循环栅栏):和倒计时器类似,可以实现线程间的技术等待,可循环使用的屏障。当一组线程到达一个屏障时被阻塞,直到最后一个线程到达后,屏障才会开启,所有被拦截的线程才会继续干活。构造方法可以定义要拦截的线程数量,每个线程调用await()告诉CyclicBarrier自己已经到达屏障,然后阻塞。
Atomic原子类
什么是原子类
Atomic就是指一个操作是不可中断的。即使是在多线程的情况下,一个操作一旦开始就不会终止,不会被其他线程干扰。原子类,也就是具有原子/原子操作特征的类。java.util.concurrent的原子类都存放在java.util.concurrent.atomic下。
JUC包下原子类的四大类型
- 基本类型:AtomicInteger、AtomicLong、AtomicBoolean
- 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 引用类型:
- 对象属性修改类型:
AtomicInteger类的原理
主要利用CAS、volatile和native方法来保证操作的原子性,从而避免synchronized的高开销,执行效率大为提升。
比如Integer类型的num++在多线程下是不安全的,单单使用volatile修饰也不行,但是AtomicInteger的num++是线程安全的。