目录
synchronized关键字演变过程
JDK1.5之前
在JDK1.5之前,我们若想实现线程同步,只能通过synchronized关键字这一方式来达成;底层Java也是通过synchronized关键字来做到数据的 原子性维护的:synchronized关键字是JVM实现的一种内置锁,从底层角度来说,这种锁的获取与释放都是由JVM帮助我们隐式实现的。
JDK1.5开始
从JDK1.5开始,并发包引入了Lock锁,Lock同步锁是基于Java来实现,因此锁的获取与释放都是通过Java代码来实现与控制的;然而,synchronized 是基于底层操作系统的Mutex_Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换会极大地增加系统的负担;在并发量较高时,也就是说锁的竞争比较激烈时,synchronized锁在性能上的表现就非常差。
JDK1.6之后
从JDK1.6开始,synchronized锁的实现发生了很大的变化;JVM引入了相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁,轻量级锁及重量级锁等,从而减少锁的竞争所带来的的用户态与内核态之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来实现的;对应锁的访问与改变,实际上都与Java对象头息息相关。
对象组成
从JDK 1.6开始,对象实例在堆当中会被划分为三个组成部分:对象头,实例数据与对齐填充.
对象头主要也是由三块内容来构成:
- 1.Mark Word
- 2.指向类的指针
- 3.数组长度
Mark Word
其中Mark Word(它记录了对象,锁及垃圾回收相关的信息,在64位的JVM中,其长度也是64bit)的位置包括了如下组成部分:
- 1.无锁标记
- 2.偏向锁标记
- 3.轻量级锁标记 (自旋锁属于轻量级锁的一种实现方式)
- 4.重量级锁标记
- 5.GC标记
锁演变
对于synchronized锁来说,锁的升级主要都是通过Mark Word中的锁标志位与是否偏向锁标志位来达成的;synchronized关键字所对应的锁都是先从偏向锁开始的, 随着锁的竞争不断升级,逐步演化至轻量级锁,最后则变成了重量级锁。
对于锁的演化来说,它会经历如下阶段:
无锁 -> 偏向锁 ->轻量级锁 ->重量级锁
- 无锁:当前对象没有线程访问
偏向锁
只针对单个线程同步: 针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象 就会在其Mark word中的将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个对象锁的synchronized方法时,它会检查这个对象 的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就
无需再去进行管程(Monitor)了,而是直接进入到该方法体中。
如果是另一个线程访问这个synchronized方法,那么实际情况会如何了?
偏向锁会被取消掉
轻量级锁
多个线程同步,若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁, 而第二个线程在争抢时,会发现改对象的对象头中的Mark Word已经是偏向锁了,但里面存储的线程ID并不是自己(第一个线程),那么会进行CAS(Compare and Swap),从而获取到锁这里面存在两种情况:
- 获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。
- 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会进行升级,升级为轻量级锁。
自旋锁
若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor(即内核态)
自旋最大的一个特点就是避免了线程冲用户态进入带内核态。
重量级锁
线程最终从用户态进入到内核态.
编译器对于锁的优化措施
编译器对于锁的优化措施: (锁消除和锁粗化都是针对运行期的 且针对代码块)
锁消除技术
JIT编译器(Just In Time编译器)可以在动态编译同步带吗时,使用一种叫做逃逸分析的技术,来通过该项技术判别程序中所使用的锁对象是否只被一个线程所使用,而没有散步到其他线程当中;如果情况就是这样的话,那么JIT编译器在编译这个同步代码时就不会生成synchronized关键字锁标识的身躯与释放机器码,从而消除了锁的使用流程.
/**
* 锁消除技术
*/
public class MyTest4 {
// private Object object = new Object();
public void method() {
/*
属于栈被每个线程所独有object 不共享 此时synchronized 被消除是不执行的
但字节码还是会生成monitorenter 金额monitorexit
*/
Object object = new Object();
synchronized (object) {
System.out.println("hello world");
}
}
}
锁粗化
JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个对象的锁(monitor),那么它就会把这几个synchronized块给合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就无需频繁申请与释放锁了,从而达到申请与释放锁各一次,就可以执行完全部的同步代码块,从而
提升了性能。
/**
* 锁粗化
*/
public class MyTest5 {
Object o = new Object();
public void method() {
synchronized (o) {
System.out.println("hello world");
}
synchronized (o) {
System.out.println("hello ");
}
synchronized (o) {
System.out.println(" world");
}
}
}
常见的锁类别(死锁,活锁,饿死)
- 死锁: 线程1等待线程2互斥持有的资源,而线程2也在等待线程1互斥持有的资源,两个线程都无法继续执行。
- 活锁: 线程持续重试一个总是失败的操作,导致无法继续执行.
- 饿死: 线程一直被调度器延迟访问其赖以执行的资源,也许是调度器先于低优先级的线程而执行高优先级的线程,同时总是会有一个高优先级的线程可以执行,饿死也叫做无限延迟.
死锁案例
/*
* jps -l
* jstack 进程id
*/
public class MyTest6 {
//生成两个不同的对象每个对象都有一个monitor
private Object lock1 = new Object();
private Object lock2 = new Object();
public void myMethod1() {
synchronized (lock1) {
//这里锁的是成员变量对象1
synchronized (lock2) {
//这里锁的是成员变量对象2
System.out.println("myMethod1 invoked");
}
}
}
public void myMethod2() {
synchronized (lock2) {
synchronized (lock1) {
System.out.println("myMethod2 invoked");
}
}
}
public static void main(String[] args) {
MyTest6 myTest6 = new MyTest6();
Runnable run1 = () -> {
while (true) {
myTest6.myMethod1();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread myThread1 = new Thread(run1, "myThread1");
Runnable run2 = () -> {
while (true) {
myTest6.myMethod2();
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread myThread2 = new Thread(run2, "myThread2");
myThread1.start();
myThread2.start();
}
}
通过jvisualvm查看
服务端可以通过 jsp查看
打印信息
D:\workspace\jvm\jvm>jps -l
7536 sun.tools.jps.Jps
10708 org.jetbrains.jps.cmdline.Launcher
10472 org/netbeans/Main
7976
8076 org.jetbrains.jps.cmdline.Launcher
8316 com.example.demo.com.concurrecy.concurrency3.MyTest6
9468 org.jetbrains.jps.cmdline.Launcher
D:\workspace\jvm\jvm>jstack 8316
Found one Java-level deadlock:
=============================
"myThread2":
waiting to lock monitor 0x000000001c789108 (object 0x000000076ba72f40, a java.lang.Object),
which is held by "myThread1"
"myThread1":
waiting to lock monitor 0x000000001c789058 (object 0x000000076ba72f50, a java.lang.Object),
which is held by "myThread2"
Java stack information for the threads listed above:
===================================================
"myThread2":
at com.example.demo.com.concurrecy.concurrency3.MyTest6.myMethod2(MyTest6.java:29)
- waiting to lock <0x000000076ba72f40> (a java.lang.Object)
- locked <0x000000076ba72f50> (a java.lang.Object)
at com.example.demo.com.concurrecy.concurrency3.MyTest6.lambda$main$1(MyTest6.java:52)
at com.example.demo.com.concurrecy.concurrency3.MyTest6$$Lambda$2/708049632.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"myThread1":
at com.example.demo.com.concurrecy.concurrency3.MyTest6.myMethod1(MyTest6.java:21)
- waiting to lock <0x000000076ba72f50> (a java.lang.Object)
- locked <0x000000076ba72f40> (a java.lang.Object)
at com.example.demo.com.concurrecy.concurrency3.MyTest6.lambda$main$0(MyTest6.java:39)
at com.example.demo.com.concurrecy.concurrency3.MyTest6$$Lambda$1/2085857771.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.