进程:进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构如果想让-一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一.条线程指的是进程中-一个单一顺序的控制流, 一个进程中可以并发多个线程,每条线程并行执行不同的任务。
并发:多个事情在在同一个时间段内同时进行。
并行:同一时间多个不同事件一起进行。
死锁:由于一些情况比如多个线程争夺同一个资源导致多个线程一直处于阻塞状态。有可能一直无法还原到运行状态。这种现象叫做死锁。导致程序无法正常终止。
wait()会立即释放对象的锁notify() 不会立即释放锁 当执行完同步代码块就会释放对象的锁
显示锁和内部锁的比较:
- 内部锁简单,但是不灵活
- 显示锁支持在一个方法内申请锁,却在另一个方法里释放锁
- 显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁
wait和notify必须在sychronized的锁块内部来进行使用。并且notify必须在wait之后进行执行
- 死锁产生的四个条件
- 互斥条件:进程要求对分配的资源进行排他性控制。属于在一段时间内仅为一个进程占用
- 请求和保持条件:进程获的请求资源而阻塞时,对以获得的资源不放
- 不剥夺条件:进程获得资源在未使用之前,不能剥夺。只能在使用完成时自己释放
- 环路等待条件:必然存在进程资源的环行链
可重入锁:指当前线程获取到锁对象,进入内部方法如果锁对象是同一个那么就不需要释放锁,加锁这个操作,不会因为之前获取到锁而阻塞。
显示锁:需要手动加锁和解锁
隐士锁:不需要手动加锁和释放锁
公平锁:多个线程按照申请锁的顺序依次获得锁。所有线程进入等待队列中。队列的第一个线程先获取锁。
优点
- 等待的线程不会出现锁饥饿问题
- 吞吐量效率较低
缺点:
- 切换线程需要CPU进行唤醒线程和阻塞线程效率较低
非公平锁:多个线程去抢占锁,如果当前锁没有被使用,那么可以直接占有锁。如果没有抢到锁在排到队列的队尾进行排队,如果此时锁是可用的那么不需要阻塞。非公平锁会出现后申请锁的线程先进入锁。
优点:
- 减少CPU唤醒线程的开销,较少了CPU的空闲时间
- 吞吐效率高
- 有可能减少线程唤起线程的开销
缺点:
- 容易出现锁饥饿
- 在等待队列中的线程有可能一直获取不到锁。或者很长时间才能或获取到锁
使用场景:需要吞吐量的时候
乐观锁:每次获取数据都会认为是最新的数据不会有人修改。所以不会上锁,但是最后进行更新操作会判断期间有没有人进行更改,通过版本号或CAS算法来实现。
- 提高吞吐量
- 适用于读多于写的场景
悲观锁:每次获取数据都会认为当前数据被修改,每次获取数据都会进行加锁,阻塞其他线程。只有当前线程执行完毕后才会释放锁。
读锁【共享锁】:针对lock下的类来说的 读读共享
写锁【独占锁】:针对lock下的类来说的 读写互斥
用户线程:自定义线程
守护线程: 如垃圾回收线程【GC】
线程状态:
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runn s'table):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权, 即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
- 阻塞的情况分三种:
- ①.等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的, 必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒。
- ②.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
- ③.其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时, 或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
volatile两大特性:[原子性:完成一些列操作中间不间断,必须是当前所在线程改变后的最新值]
1:可以保证内存可见性,可以实现快速刷新到主存中的值,前面的修改对后面所有线程都是可见的。实际上在执行字节码指令时生成了一个lock addl指令 这个指令作用是将工作内存中的修改的值,立马写回主内存。同时令其他工作内存缓存的数据无效化。
2: 保证有序性,通过内存屏障禁止指令重排。
3:无法保证原子性 在java线程中的原子性是指某个变量值被其他线程使用时。
volatile适用场景:
1:当读远多于写,结合使用内部锁和volatile 变量来减少同步的开销理由:利用volatile 保证获取操作的可见性;利用synchronized保证复合操作的原子性。
2:多线程下的单例模式 给当前引入属性对象加volatile关键字 防止指令重排。
- 如何保证可见性:
(1)当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
(2)当读一个volatile变量时,JMM 会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
- 如何保证顺序性和可见性 :通过内存屏障:内存屏障(也称内存栅栏,是一个CPU指令,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的-一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现 了Java内存模型中的可见性和有序性(禁重排).
写屏障 (Store Memory Barrier) :告诉 处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。
读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
JMM:【与操作系统无关】三大特性:JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见
线程堆变量的读取和修改都是在自己的工作内存中完成的。不能直接读写主内存的数据,不同的线程之间也无法访问自己的工作内存中的数据。
指令重排:对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。前提是不能存在数据依赖,如果存在数据依赖禁止重排序。JVM规范中试图定义一 种Java内存模型(java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异以实现让:Java程序在各种平台下都能达到一致的内存访问效果。
JMM实现java程序在各个平台上访问内存一致的效果。,主要目的是定义各种平台程序变量的访问规则。
cas【compare and swap】:比较比较并交换,cas过程是个原子性操作不会被其他线程中断。包含三个参数 内存位置 、预期值、更新值。执行cas操作过程中。如果预期值与更新值一致 ,那么处理器会将内存位置更新为新的值。如果不匹配那么不做任何操作。多个线程同时执行cas只有一个会成功。因为cas是一个原子性操作。是不会被打断的。是JDK提供的一个非阻塞型的原子操作。底层通过CPU的cmpxchg,不会出现数据不一致的问题。是通过操作unsafe类实现的。
CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身具有原子性,说明这玩意更可靠。CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一-致问题,Unsafe提 供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在 多线程情况下性能会比较好。
CAS:产生两个问题:
- ABA问题
- 循环操作,开销比较大
synchronized:隐士锁、独占锁、可重入锁【由JVM进行上锁和释放锁】:
synchronized每个对象都可以作为锁的原因:
-
- 每个对象都有一个指向monitor对象的指针,这个monitor对象在java中是由ObjectMonitor实现的,当某一个monitor被某个线程持有后就会进行锁定
synchronized锁升级原因:
-
- java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒--个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,
- 因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一-些寄存器值、
变量等,以便内核态调用结束后切换回用户态继续工作。在Java早期版本中,synchronized属 于重量级锁,效率低下。
-
- 因为监视器锁(monitor) 是依赖于底层的操作系统的Mutex Lock(系统互斥量)来实现的,它保证了同一时间只能有一个线程访问临界资源挂起线程和恢复线程都需要转入内核态去完成。阻塞或唤醒--个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
锁升级过程:
偏向锁:
优点:
- 加锁和解锁不需要额外的消耗
缺点:
- 可能存在锁消耗,会带来额外的撤销锁消耗
使用场景:
- 适用只有一个线程访问同步代码块的的场景
会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。即偏向锁在资源没有竞争情况下消除了同步语句,不需要进行cas操作,直接提高程序性能那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。如果不是,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,竞争成功,表示之前的线程不存在了,MarkWord 里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
偏向锁的撤销:
偏向锁使用--种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
①第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
②第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
轻量级锁:有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自旋锁CAS
优点:
- 竞争的线程不会阻塞
缺点:
- 始终得不到锁竞争的线程,会消耗掉CPU
使用场景:
- 追求响应时间
- 通知执行速度快
目的:为了在线程近乎交替执行同步块时提高性能。在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。
升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。此时线程B操作中有两种情况:如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;如果锁获取失败, 则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00), 此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻级锁。轻量级锁的加锁JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方成为DisplacedMarkWord。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。自旋CAS:不断尝试去获取锁,能不升级就不往上捅,尽 量不要阻塞轻量级锁的释放在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
java6之前:自旋多少次升级为重量级锁:默认启用,默认情况下自旋的次数是10次或者自旋线程数超过cpu核数一半.java6之后:线程如果自旋成功了,那下次自旋的最大次数会增加因为JVM认为既然上次成功了,那么这一-次也很大概率会成功。反之如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。偏向锁和轻量级锁的区别:1.争夺轻量级锁失败时,自旋尝试抢占锁2.轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁.
重量级锁:
优点:
- 吞吐效率高
缺点:
- 导致线程间阻塞
锁升级后Hashcode的值去哪了:
在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次 被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
偏向锁:,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果- - 个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那Mark Word中的identity hash code必然会被偏向线程ld给覆盖,这就会造成同-一个对象前后两次调用hashCode()方法得到的结果不一-致。
轻量级锁:JVM会在当前线程的栈帧中创建--个锁记录(LockRecord)空间,用于存储锁对象的MarkWord拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
重量级锁:升级为重量级锁后,MarkWord保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的MarkWord,锁释放后也会将信息写回到对象头。
线程中断协商机制:
1:早期线程需要通过stop sleep来实现线程的停止,但是正常下一个线程不应该被强制中断或者停止。而是由线程自己自行停止。
2:java种没有办法立即停止线程,通过中断协商机制。让线程自己去停止线程。调用interrupt,给线程添加一个中断标识位置。然后自己去检测线程是否被停止,通过isinterrupted
3:interrupted:返回当前线程的中断状态,将当前线程的中断状态设置为0并重新中断标识设为false,清除线程的中断状态。
locksupport:
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程LockSupport和每个使用它的线程都有一个许可(permit)关联。每个线程都有一个相关的permit, permit最多只有-一个,重复调用unpark也不会积累凭证。
形象的理解线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park,方法时如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加-一个凭证,但凭证最多只能有1个,累加无效。
ThreadLocal:提供给每个线程一个变量副本,而且不实现多线程共享。
实现原理:底层通过ThreadLocalMap,Thread的静态内部类。而在Thread的内部维护了一个Treadlocal.ThreadLocalMap
1、shutDown()
当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
2、shutdownNow()
执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
线程池:一种线程的使用模式,如果每次都以new的方式会造成堆内存对象堆积。占用较多不必要的内存。影响性能,线程池避免了短时间创建线程。防止过分调度。
线程池工作原理:
-
-
- 当需要用到线程池时线程池在没有超过核心线程数时,每次都会创建一个线程
- 当核心线程数达到最大,会将任务放置到队列中去
- 如果队列满了。核心线程数满了。此时就需要创建新的线程执行任务
- 当队列任务满了,超过了最大线程数,那么就会执行对应的拒绝策略。对任务进行拒绝
-
常用的线程池:
1.newCachedThreadPool 创建一个按需的线程池
2.newFixedThreadPool 创建一个定长的线程池
3.newSingleThreadExecutor 创一个只有一个线程的线程池 但是无界队列
注意::
1)FixedThreadool 和singletreadPool:允许的请求队列长度为Integer.Max. _VALUE,可能会堆积大量的请求,从而导致OOM,【outofmemoryerror】。
2) CachedThreadPool 和ScheduledThreadPool:允许的创建线程数量为Integer.Max, _VALUE,可能会创建大量的线程,从而导致OOM。
七个参数:
-
-
-
- corePoolSize - 即使空闲时仍保留在池中的线程数,除非设置
- allowCoreThreadTimeOutmaximumPoolSize - 池中允许的最大线程数
- keepAliveTime - 当线程数大于内核时,这是多余的空闲线程在终止前等待新任务的最大时间。
- unit - keepAliveTime参数的时间单位
- workQueue - 用于3在执行任务之前使用的队列。 这个队列将仅保存execute方法提交的Runnable任务。
- threadFactory - 执行程序创建新线程时使用的工厂
- handler - 执行被阻止时使用的处理程序,因为达到线程限制和队列容量
-
-
4种拒绝策略:
-
- AbortPolicy(默认:直按抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunspolicy:调用者运行“一种调节机制.该策略既不会抛弃任务.也不会抛出异常.而是将某些任务回退到使用者.从而降低新任务的流量。
- DiscardOldestPolicy :抛弃队列中等待最久的任务+然后把当前任务加人队列中尝试再次提交当前任务。
- DiscardPolicy :该策略默默地丢弃无法处理的任务.不做任何处理也不抛出异常。如果允许任务丢失, 这是最好的种策略。
队列:【阻塞队列】
- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作将会被阻塞.
- 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素。
- 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
AQS实现原理:队列同步器,是一个FIFO 的双端队列,通过自旋等待。通过stae变量判断是否阻塞 从尾部入队 从头部出队
AQS内部类Node
同时有个重要的变量state=0 空闲 大于0被占用