进程和线程
进程:是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,有独立的地址(内存)空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程;一个进程崩溃后,在保护模式下不会对其它进程产生影响
多进程在linux系统中效率比多线程开销小,对于落地项目需要关注多进程之间的通讯。进程通俗点就是一个软件,所以linux中多节点部署效率更好。
线程:CPU调度的最小单位,是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间。
多线程是java的特性,因为现在cpu都是多核多线程的,可以同时执行几个任务,为了提高jvm的执行效率,java提供了这种多线程的机制,以增强数据处理效率。多线程对应的是cpu,并发是多线程的运行方式,异步是多线程的目的,高并发对应的是访问请求,可以用单线程处理所有访问请求,也可以用多线程同时处理访问请求。
多线程在window下效率好,所以更多会关注抢资源和同步问题。
CPU核心数和线程数的关系:CPU的RR调度,多个线程会导致上下文切换耗时,所以线程过多效率也慢;线程是CPU分配的基本单位,核心数:线程数=1:1 ;使用了超线程技术后—> 1:2
线程不是计算机硬件的功能,而是操作系统提供的一种逻辑功能,本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。
对于操作系统来看,进程就相当于独立的应用程序,而线程是用来实现进程的调度和管理以及资源分配,多线程就是在一个应用程序中有多个执行部分可以同时执行。
程序计数器:用来记录线程当前要执行的指令地址 ,线程私有。
栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
Java程序的进程(Java的一个程序运行在系统中)里至少包含主线程和垃圾回收线程(后台线程)。
多线程是为了同步完成多项任务,不是为了提供程序运行效率,而是通过提高资源使用效率来提高系统的效率。
并行和并发
首先并行和并发都是对于多线程的运行方式
并行:在单位时间内多个任务同时在执行,cpu比如是8核16线程,对于cpu的调度就可能保证有16个线程可能是并行运行的。实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是并行编程。
并发:是指同一个时间段内多个任务同时都在执行,强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。从微观角度上也是有先后顺序的,那么哪个线程先执行完全取决于CPU调度器(JVM来调度)。
计算机只有一个CPU时,在任意时刻只能执行一条计算机指令,每一个进程只有获得CPU的使用权才能执行指令.所谓多进程并发运行,从宏观上看,其实是各个进程(应用程序)轮流获得CPU的使用权,分别执行各自的任务.
那么,在进程池中,会有多个线程处于就绪状态等到CPU,JVM就负责了线程的调度.JVM采用的是抢占式调度,没有采用分时调度,因此可以能造成多线程执行结果的的随机性。
异步概念
异步:异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回.实现异步可以采用多线程技术或则交给另外的进程来处理。
异步是目的,而多线程是实现这个目的的其中一个方法
DMA:本质上所有的程序最终都会由计算机硬件来执行,DMA就是直接内存访问的意思,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源,硬盘、光驱、网卡、声卡、显卡是有DMA功能的。只要CPU在发起数据传输时发送一个指令,硬件就开 始自己和内存交换数据,在传输完成之后硬件会触发一个中断指令来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。
因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以可以减少共享变量的数量,减少了死锁的可能。
当需要执行I/O操作时,使用异步操作比使用线程+同步 I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。
异步调用:并不是要减少线程的开销, 它的主要目的是让调用方法的主线程不需要同步等待在这个函数调用上, 从而可以让主线程继续执行它下面的代码.与此同时, 系统会通过从ThreadPool中取一个线程来执行,帮助我们将我们要写/读的数据发送到网卡.由于不需要等待,等于同时做了两件事情. 这个效果跟另外启动一个线程来执行等待方式的写操作是一样的.
异步执行也需要在线程中,只是不在当前线程执行,异步通常用线程池的线程(因为可以多次利用,申请时不需要重新申请一个线程,只需要从池里取就行了)
异步有时优先级比主线程还高了,这个特点和多线程不同
线程信息
**ThreadInfo:**获取当前系统所有线程信息
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo info : threadInfos ) {
System.out.println(info.getThreadId() + "-------"+info.getThreadName());
}
System.out.println(Thread.currentThread().getId()+":"+Thread.currentThread().getName());
}
打印信息:
6-------Monitor Ctrl-Break
5-------Attach Listener
4-------Signal Dispatcher
3-------Finalizer
2-------Reference Handler
1-------main
1:main
JVM线程
当类被调用的时候,也就是运行时启动JVM线程,然后JVM线程去启动其他线程,比如main方法的线程
守护线程
在后台运行的线程,其目的是为其他线程提供服务,也称为“守护线程"。JVM的垃圾回收线程就是典型的后台线程。
若所有的前台线程都死亡,则后台线程自动死亡;前台线程没有结束,后台线程是不会结束的。
守护线程使用try…finally时finally不一定会执行!
thread.isDaemon():线程对象是否为后台线程
setDaenon(true):前台线程创建的线程默认是前台线程,设置为后台线程;当后台线程创建的新线程时,新线程是后台线程。
Thread thread = new Thread("守护") {
@Override
public void run() {
System.out.println("守护线程");
Thread sonT = new Thread("son") {
@Override
public void run() {
System.out.println("子线程");
}
};
System.out.println(sonT.isDaemon()); //true
}
};
//设置为true之后,thread和sonT都是守护线程;注释之后则为false
thread.setDaemon(true);
System.out.println(thread.isDaemon());
thread.start();
联合线程
**join():**相当于合并为一个线程,调用者线程完成后再执行其他线程
Thread thread = new Thread("join") {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("join:###" + i);
}
}
};
for (int i = 0; i < 5000; i++) {
System.out.println("main:###" + i);
if (i==10){
//当i=10时线程进入就绪状态,但是肯定还有和mian线程进行竞争
thread.start();
}
if (i==500){
//进行到500时,如果thread没有跑完,则mian线程停下等待thread跑完再接着跑main
//如果已经跑完了就无法加入到main线程中
thread.join();
}
}
线程生命周期
java线程是协作式,而非抢占式,所以所有线程方式都是通知,而非直接干预!
1:新建状态(new):使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前.仅仅只是存在一个线程对象而已.当新建状态下的线程对象调用了start方法,此时进入可运行状态.
Thread t = new Thread();//此时t就属于新建状态
2:可运行状态(runnable):分成两种状态,ready和running,分别表示就绪状态和运行状态。
就绪状态:线程对象调用start方法之后,等待JVM的调度(此时该线程并没有运行).
运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行.
3:阻塞状态(blocked):正在运行的线程因为某些原因放弃CPU,暂时停止运行,就会进入阻塞状态.
此时JVM不会给线程分配CPU,直到线程重新进入就绪状态,才有机会转到运行状态.
阻塞状态只能先进入就绪状态,不能直接进入运行状态.
阻塞状态的两种情况:
1):当A线程处于运行过程时,试图获取同步锁时,却被B线程获取.
此时JVM把当前A线程存到对象的锁池中,A线程进入阻塞状态.
2):当线程处于运行过程时,发出了IO请求时,此时进入阻塞状态.
4:等待状态(waiting)(等待状态只能被其他线程唤醒):此时使用的无参数的wait方法,
当线程处于运行过程时,调用了wait()方法,此时JVM把当前线程存在对象等待池中.
5:计时等待状态(timed waiting)(使用了带参数的wait方法或者sleep方法)
1):当线程处于运行过程时,调用了wait(long time)方法,此时JVM把当前线程存在对象等待池中.
2):当前线程执行了sleep(long time)方法.
6:终止状态(terminated):通常称为死亡状态,表示线程终止.
1):正常执行完run方法而退出(正常死亡).
2):遇到异常而退出(出现异常之后,程序就会中断).
线程一旦终止,就不能再重启启动,否则报错(IllegalThreadStateException).
线程启动和中断
start() 方法是多线程启动的唯一方式,这个方法的作用就是通知线程规划器此线程可以运行了。要注意,调用start方法的顺序不代表线程启动的顺序,也就是cpu执行哪个线程的代码具有不确定性。
run() 这个方法是线程类调用start后执行的方法,如果在直接调用run而不是start方法,那么和普通方法一样,没有区别。
isAlive() 是判断当前线程是否处于活动状态,活动状态就是已经启动尚未终止。
**线程安全停止工作:**stop(),resume(),suspend()已不建议使用,stop()会导致线程不会正确释放资源,suspend()容易导致死锁。
线程自然终止:自然执行完或抛出未处理异常
isInterrupted() 判定当前线程是否处于中断状态
interrupt() 中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为true。
Thread.interrupted() 不是继承thread时调用,判定当前线程是否处于中断状态,同时将中断标志位设置为true,就是调用 currentThread().isInterrupted(true)
调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
Thread thread = new Thread("myThread") {
@Override
public void run() {
//isInterrupted和interrupt是thread提供的方法
//Thread.interrupted()和isInterrupted区别不大
while (!Thread.interrupted()) {
System.out.println("######"+isInterrupted()); //myThread
//false 表示此时还没有中断
System.out.println(Thread.currentThread().getName());
}
//执行了thread.interrupt();标志位改为true,所以跳出循环
//false,但是并没有真正中断线程
System.out.println("######"+Thread.interrupted());
if (!isInterrupted()){
interrupted(); //再次进行中断
System.out.println("isAlive###"+isAlive()); //true,线程还在活跃中
}
if (!isInterrupted()){
System.out.println("isAlive###");
}
}
};
thread.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
//线程还是会继续跑完
thread.interrupt();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程跑完了,所以不活跃false,但是仍然还存在
System.out.println("isAlive###"+thread.isAlive()); //false
System.out.println(thread.getName()+"######"+thread.isInterrupted()); //false
线程等待和唤醒
wait和notify由object提供
**wait():**使线程处于一种等待状态,释放所持有的对象锁。
**notify():**唤醒一个正在等待状态的线程。注意调用此方法时,并不能确切知道唤醒的是哪一个等待状态的线程,是由JVM来决定唤醒哪个线程,不是由决定的。
**notifyAll():**唤醒所有等待状态的线程,并不是给所有唤醒线程一个对象锁,而是让它们竞争。
**sleep():**使一个正在运行的线程处于睡眠状态,是一个静态方法,捕获异常,不释放对象锁。
yield(): 线程让步,一个在运行状态的线程转为就绪状态和其他线程继续同时等待JVM分配
sleep方法和yield方法的区别:
1):都能使当前处于运行状态的线程放弃CPU,把运行的机会给其他线程.
2):sleep方法会给其他线程运行机会,但是不考虑其他线程的优先级,**yield方法只会给相同优先级或者更高优先级的线程运行的机会**.
3):调用sleep方法后,线程进入计时等待状态,调用yield方法后,线程进入就绪状态.
线程顺序
线程优先级:优先级的高低只和线程获得执行机会的次数多少有关,并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度。
MAX_PRIORITY=10,最高优先级
MIN_PRIORITY=1,最低优先级
NORM_PRIORITY=5,默认优先级
int getPriority() :返回线程的优先级。
void setPriority(int newPriority) : 设值线程的优先级。
每个线程都有默认优先级,主线程默认优先级为5,如果A线程创建了B线程,那么B线程和A线程具有相同优先级.
不同的操作系统支持的线程优先级不同的,建议使用上述三个优先级,不要自定义.
什么是JMM
Java memory model 是一个规范,抽象的模型 ,把 Java 虚拟机内部划分为线程栈和堆。
**线程栈:**工作空间内存,每一个运行在 Java 虚拟机里的线程都拥有自己的线程栈,所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。存放私有信息,基本数据类型直接分配到工作内存,引用的地址存放在工作内存,引用的对象存放在堆中。
**堆:**共享内存,堆上包含在 Java 程序中创建的所有对象,包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作空间内存,线程的工作空间内存中保存了该线程中是用到的局部变量和主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。线程修改私有数据直接在工作空间去修改,而修改共享数据需要先复制到工作空间,在工作空间处理完之后刷新内存中的数据。
硬件的内存模型
计算机包含一个主存,所有的 CPU 都可以访问主存。主存通常比 CPU 中的缓存大得多,当一个 CPU 需要读取主存时,它会将主存的部分读到 CPU 缓存中。将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当 CPU 需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。
MESI协议
**MESI协议:**Modified修改,Exclusive独占,Shared共享,Invalid无效。是基于Invalidate的高速缓存一致性协议,并且是支持回写高速缓存的最常用协议之一。阻止两个以上CPU同时修改缓存了相同主存数据的缓存副本,简单理解就是:cpuA和cpuB都读取了同一个cache Line(CL),分别在各自缓存中将拷贝CL副本状态设置为独享,此时A和B同时嗅探总线发现CL不止一个副本,那么CL副本的状态都会改为共享,同时为A和B做了一个群联系。A将CL修改后状态改为M并将消息通知B,那么B就将CL副本状态改为无效。当A进行嗅探发现所有的CL副本都被置为无效时就将修改的CL刷新到主内存,并修改cpuA的CL副本状态为独享。其他cpu如果也想读取该CL,则会直接去取主内存的CL,然后以此往复。
Cache Line
Cache Line: 越靠近cpu的结构,读取速度越快,首先是寄存器,然后是高速缓存,内存等;想要提高执行效率,可尽可能把数据,留在高速缓存中,减少去内存读取数据。
Cache Line 是 CPU 和主存之间数据传输的最小单位,理解为CPU Cache中的最小缓存单位,常见大小为64字节,也就是在传输数据的时候,从内存中连续取64字节的数据
CPU缓存的一致性问题:并发处理的不同步
cpuA修改主内存的data数据时,先拷贝到缓存中,最终在寄存器中继续修改;而在刷新到主内存之前,cpuB读取了主内存中的data数据,即使cpuA将修改的数据刷新到主内存中,cpuB读取的数据仍然是脏数据。
处理思路:
1.加总线锁,也就是对data数据加锁,只允许同时只有一个cpu去操作
2.缓存上的一致性协议(MESI),当cpuA进行修改之后并将cache line置为无效,其他cpu则读取data时只会重新从主内存获取data数据,而不会使用缓存中的data数据
JMM对三个特征的保证
并发编程的三个特性:原子性,可见性,有序性
原子性
原子操作是对于多线程而言的,对于单一线程,无所谓原子性;原子操作是针对共享变量的,访问某个共享变量的操作从其执行线程之外的线程来看,该操作要么已经执行完毕,要么尚未发生,其他线程不会看到执行操作的中间结果。
站在访问变量的角度,如果要改变一个对象,而该对象包含一组需要同时改变的共享变量,那么,在一个线程开始改变一个变量之后,在其它线程看来,这个对象的所有属性要么都被修改,要么都没有被修改,不会看到部分修改的中间结果。
非原子操作变成一个原子操作:
1.加锁:Synchronized关键字,Lock类
2.concurrent包下原子类基于 CAS 实现的,比如:AtomicInteger、AtomicLong、AtomicReference等。
CAS 算法是硬件对于并发操作共享数据的支持。
CAS 包含了三个操作数:内存值 V,预估值 A,更新值 B
当且仅当 V == A 时,V 将被赋值为 B,否则循环着不断进行判断 V 与 A 是否相等
可见性
线程只能操作自己工作空间中的数据,当一个线程修改了某一个共享变量的值,其他线程是不能立即知道这个修改。对于串行来说,也就是单线程,可见性问题是不存在的。
1.volatile关键字来保证可见性,在JMM模型上实现MESI协议,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
2.通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性
程序执行的顺序不一定按照代码的先后顺序执行,因为JVM在真正执行这段代码的时候可能会发生指令重排,指的是处理器可能会对输入代码进行乱序执行(Out-of-Order Execution)优化,以让处理器内部的运算单元能尽量被充分利用。
单线程重排后不影响结果,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
1.volatile关键字保证一定的“有序性”
2.通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
3.Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
锁定规则:后一次加锁必须等前一次解锁。
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则得出操作A先行发生于操作C。
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
Synchronized 原理
Java监视控制台Jconsole
在jdk目录/bin/jconsole
线程状态:
JVM内存分析:
字节码反编译javap
反编译.class,用于JVM指令分析,反编译的是字节码文件
-c:分解方法代码,即显示每个方法具体的字节码
-v :指定显示更进一步的详细信息
-public | protected | package | private:用于指定显示哪种级别的类成员
jclasslib插件:idea插件
jstack: 用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息
Jstack pid:cmd命令 Jstack 8396 ,通过上面VM概要中-连接名称:pid: 8396
同步代码块原理
Monitorenter作为互斥的入口标识,Monitorexit作为互斥出口标识
Monitorenter作用:
1.计数器不为0,lock内存
2.可重入,同一线程对同一对象可进行多次加锁,每次加1
3.monitor一个线程占有,其他线程请求时会进入BOLCK,直到monitor为0
Monitorexit作用:每次执行完,计数器减1,为0时为解锁
不管是普通的对象还是字节码类对象都是堆内存中的一个实例对象,而每个对象都会有一个 monitor 对象,监视器。
某一线程占有这个对象的时候,先判断monitor 的计数器是不是0,如果是0表示还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1。
Synchronized是一种悲观锁,从实现方式可以看,悲观锁在操作内存时,一开始就进行判断,然后加限制;而乐观锁(CAS)会让所有的线程都能访问到内存,可以拿到当前的一个标识,在提交操作时只有标识不变的情况下可以操作,所以当并发高的时候产生冲突的情况会比较频繁,会造成一直无法更新和提交。
//obj可以是实例对象,也可以是类对象
synchronized (obj) {}
同步方法原理
对方法的加锁标识是 ACC_SYNCHRONIZED,同步方法是隐式的,一个同步方法会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符。常量池在加载时就会加载到运行时常量池中,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,则先要获得对应的monitor锁,然后执行方法。当方法执行结束(不管是正常return还是抛出异常)都会释放对应的monitor锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。当同步方法中抛出异常且方法内没有捕获,则在向外抛出时会先释放已获得的monitor锁
加锁和释放锁的原理和同步代码块方式一样
Synchronized锁的优化
JDK 1.6,JVM对synchronized同步锁做了充分的优化,在某些场景下的性能已经超越了Lock同步锁
Java对象头
在JVM中,对象在堆内存中的布局分为三块区域:对象头、实例数据和对齐填充
1.实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
2.填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
3.对象头:主要结构又由 Mark Word 和 Class Metadata Address 组成,采用2个字宽(一个字宽代表4个字节,一个字节8bit)来存储对象头(如果对象是数组则会分配3个字宽,多出来的1个字宽记录的是数组长度)
偏向锁
偏向锁作用:大多数情况下,锁不存在竞争关系,总是会被一个线程持有,为了减少互斥锁的代价
偏向锁加锁方式:
先读取对象头的Mark Word 判断是否处于可偏向的状态,即检查Mark Word中的 是偏向状态(是否是偏向锁)和锁标志位,简单来说就是验证当前线程是否已经获取了偏向锁。偏向锁状态为1,锁标志位是01,则表明为可偏向状态。判断线程ID是否指向当前线程,如果不是则通过CAS操作竞争锁。
如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,CAS获取偏向锁失败,说明其他线程在竞争,当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码(撤销偏向锁的时候会导致stop the word)。将偏向状态改为0,验证已获取锁的线程是否存活,如果死亡,将锁标志位恢复到无锁状态,重新加锁。如果存活,将锁标志位升级为轻量级锁(01)。
偏向锁的释放:
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
**无锁状态到偏向锁:**一个线程所访问,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,是否偏向锁置1,写入线程号
**偏向锁到轻量级锁:**如果失败说明发生竞争,偏向锁就会升级为轻量级锁,其他线程会通过自旋CAS的形式尝试获取锁,不会阻塞,提高性能,如果自旋成功则依然处于轻量级状态
**轻量级锁到重量级锁:**为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁,重量级锁会让他申请的线程进入阻塞。
状态 | 标志位 | 存储内容 |
---|---|---|
未锁定 | 01 | 对象哈希码、对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
膨胀(重量级锁定) | 10 | 执行重量级锁定的指针 |
GC标记 | 11 | 空(不需要记录信息) |
可偏向 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 |
**锁消除:**以上锁都是JVM自己内部实现,当执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作。而且JIT在编译的时候把不必要的锁去掉了。
Volatile原理
通过汇编指令(反编译字节码指令没有体现,只能通过hsdis查看汇编指令)可看到被volatile修饰的变量会有lock:前缀,类似于lock指令,也就是对cpu指令进行加锁,从而在有lock标志时,其他CPU的读写请求都会被阻塞,当指令完成后释放锁并且变量的缓存数据刷新到主内存,根据缓存一致性协议使其他cpu中的缓存全部置为无效。这就是常说的可见性!
当写一个volatile变量时,JMM会把该线程对应的缓存中的共享变量值刷新到主内存;那么当另一线程读一个volatile变量时,JMM会把该线程对应的缓存置为无效,线程接下来将从主内存中读取共享变量。
HAPPENS-BEFORE(先行发生)原则volatile规则就是对一个变量的写操作先行发生于后面对这个变量的读操作。根据从汇编指令中可以看出volatile类型小型的synchronized,被修饰变量的在同一时刻也只会被一个线程访问就保证其有序性!
LOCK#信号,会锁定系统总线。当这个输出信号发出的时候,来自其他处理器或总线代理的控制请求将被阻塞,不过实际后来的处理器都采用锁缓存替代锁总线,所以有堵塞!
对于i++在i被volatile修饰时,首先从汇编指令可以发现被修饰的变量,不同于sychronized是对一整个指令进行加锁,而是对修改指令加了lock,并不能保证其原子性!详细的过程:线程A读取i,对i++,此时并没有赋值,也就是没有修改,执行完++释放锁,就在准备执行赋值指令时,B执行完++并抢到锁进行了赋值;修改了i值之后其他线程拿到i的缓存都失效了,但是!A线程内存中寄存器缓存保存的是中间值,所以当B将i刷新到主内存中,A线程在进行到赋值指令的时候并没有去再去读取i值,只是将中间值赋值给缓存i!
线程0读取了status为false,并进行if条件判断指令,执行完判断指令,线程1拿到锁准备赋值操作(还没有执行所以status是自由的),此时线程0开始下一个循环,直到线程1进行值修改为true,将线程0的缓存设为无效并刷新到主内存中再释放锁,正巧线程0开始进行if判断,因为要读取缓存值,所以重新读取了status发现值为true并通过了if条件,循环往复!
static volatile boolean status=false;
public static void main(String[] args) {
//线程0
new Thread(() -> {
while (true) {
if (status) {
status = false;
System.out.println(status+Thread.currentThread().getName());
}
}
}).start();
//线程1
new Thread(() -> {
while (true) {
if (!status) {
status = true;
System.out.println(status+Thread.currentThread().getName());
}
}
}).start();
}