关于Java锁机制的理解
首先Java中为什么会使用锁:是为了解决Java共享内存模型带来的线程安全问题。
思考:两个线程都有初始值为0的静态变量做自增,一个做自减,各做5000次,counter的值会为0嘛?
public class SyncDemo { private static int counter = 0; public static void increment() { counter++; } public static void decrement() { counter--; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { increment(); } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { decrement(); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); //思考: counter=? log.info("{}", counter); }
问题分析:以上代码执行结果值可能是正数,负数,零,因为Java中对静态变量的自增,自增并不是原子操作。
如果单线程情况下该程序结果肯定为0,但多线程情况下这几行代码可能交错运行,就产生了问题,产生了临界区。
一个程序运行多个线程本身是没有问题的,问题就出在多个线程访问共享变量,多个线程读共享变量其实也没有问题,但是多线程情况下读写操作时发生指令交错,就会出现问题。
//临界资源 private static int counter = 0; public static void increment() { //临界区 counter++; } public static void decrement() {//临界区 counter--;
竞态条件:多个线程在临界区内执行,由于代码的执行系列不同而导致结果无法预测,称之为竞态条件。
为了避免临界区的竞态条件,有多种方式可以达到:
jvm内置锁synchronized,juc的lock锁,原子变量。
其中常用锁有哪些:jvm内置锁synchronized,juc的lock锁
注意:Java中的互斥和同步都可以采用synchronized关键字来实现,但是他们是有区别的:互斥是保证临界区的竞态条件,同一时刻只能有一个线程执行临界区代码,同步是由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到某个点。
synchronized的使用:
public static synchronized void increment() { counter++; } public static synchronized void decrement() { counter--;
方法二:
private static Object object = new Object(); public static void increment() { synchronized (object) { counter++; System.out.println("1::" + object.hashCode()); System.out.println(ClassLayout.parseInstance(object).toPrintable()); } } public static void decrement() { synchronized (object) { counter--; System.out.println("2::" + object.hashCode()); } }
其实synchronized实际上是用对象锁保证了临界区内代码的原子性。
synchronized的底层原理:是jvm的内置锁,基于Monitor机制实现的,依赖底层操作系统的互斥原语互斥量Mutex,它是一个重量级锁,性能较低,

while(条件不满足) { wait();
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行相同的操作;
- 只需要唤醒一个线程。

ObjectMonitor() { _header = NULL; //对象头 markOop _count = 0; _waiters = 0, _recursions = 0; // 锁的重入次数 _object = NULL; //存储锁对象 _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程) _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构) FreeNext = NULL ; _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程) _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0;
1:对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。
2:实例数据:存放类的属性数据信息,包括父类的属性信息;
3:对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
Mark Word :
数组长度(只有数组对象有):
<!-- 查看Java 对象布局、大小工具 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
public static void main(String[] args) throws InterruptedException { Object obj = new Object(); //查看对象内部信息 System.out.println(ClassLayout.parseInstance(obj).toPrintable());

对象的几个部分的作用:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)。
// 32 bits: // -------- // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) // // 64 bits: // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block) 。。。。。。 // [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread // [0 | epoch | age | 1 | 01] lock is anonymously biased // // - the two lock bits are used to describe three states: locked/unlocked and monitor. // // [ptr | 00] locked ptr points to real header on stack // [header | 0 | 01] unlocked regular object header // [ptr | 10] monitor inflated lock (header is wapped out) // [ptr | 11] marked used by markSweep to mark an object
hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。


enum
{
locked_value = 0, //00 轻量级锁 unlocked_value = 1, //001 无锁 monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁 marked_value = 3, //11 GC标记 biased_lock_pattern = 5 //101 偏向锁
}
//关闭延迟开启偏向锁 -XX:BiasedLockingStartupDelay=0 //禁止偏向锁 -XX:-UseBiasedLocking
轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode
当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
轻量级锁:
@Slf4j public class LockEscalationDemo { public static void main(String[] args) throws InterruptedException { log.debug(ClassLayout.parseInstance(new Object()).toPrintable()); //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式 Thread.sleep(4000); Object obj = new Object(); // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗 //obj.hashCode(); //log.debug(ClassLayout.parseInstance(obj).toPrintable()); Thread thread1 = new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName() + "开始执行。。。\n" + ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj) { // 思考:偏向锁执行过程中,调用hashcode会发生什么? //obj.hashCode(); log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n" + ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName() + "释放锁。。。\n" + ClassLayout.parseInstance(obj).toPrintable()); } }, "thread1"); thread1.start(); //控制线程竞争时机 Thread.sleep(1); Thread thread2 = new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"开始执行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName()+"释放锁。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread2"); thread2.start(); Thread.sleep(5000); log.debug(ClassLayout.parseInstance(obj).toPrintable()); }
public class LockEscalationDemo { public static void main(String[] args) throws InterruptedException { log.debug(ClassLayout.parseInstance(new Object()).toPrintable()); //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式 Thread.sleep(4000); Object obj = new Object(); new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"开始执行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName()+"释放锁。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread1").start(); new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"开始执行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName()+"释放锁。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread2").start(); Thread.sleep(5000); log.debug(ClassLayout.parseInstance(obj).toPrintable()); } }
intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
- Java 7 之后不能控制是否开启自旋功能
StringBuffer buffer = new StringBuffer(); /** * 锁粗化 */ public void append(){ buffer.append("aaa").append(" bbb").append(" ccc");
public class LockEliminationTest { /** * 锁消除 * -XX:+EliminateLocks 开启锁消除(jdk8默认开启) * -XX:-EliminateLocks 关闭锁消除 * @param str1 * @param str2 */ public void append(String str1, String str2) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(str1).append(str2); } public static void main(String[] args) throws InterruptedException { LockEliminationTest demo = new LockEliminationTest(); long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { demo.append("aaa", "bbb"); } long end = System.currentTimeMillis(); System.out.println("执行时间:" + (end - start) + " ms"); }
-XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启) -XX:-DoEscapeAnalysis //表示关闭逃逸分析。 -XX:+EliminateAllocations //开启标量替换(默认打开)
demo测试:
/** * * 进行两种测试 * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来 * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError * * 开启逃逸分析 jdk8默认开启 * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError * * 执行main方法后 * jps 查看进程 * jmap -histo 进程ID * */ @Slf4j public class EscapeTest { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 500000; i++) { alloc(); } long end = System.currentTimeMillis(); log.info("执行时间:" + (end - start) + " ms"); try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e1) { e1.printStackTrace(); } } /** * JIT编译时会对代码进行逃逸分析 * 并不是所有对象存放在堆区,有的一部分存在线程栈空间 * Ponit没有逃逸 */ private static String alloc() { Point point = new Point(); return point.toString(); } /** *同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除 */ public void append(String str1, String str2) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(str1).append(str2); } /** * 标量替换 * */ private static void test2() { Point point = new Point(1,2); System.out.println("point.x="+point.getX()+"; point.y="+point.getY()); // int x=1; // int y=2; // System.out.println("point.x="+x+"; point.y="+y); } } @Data @AllArgsConstructor @NoArgsConstructor class Point{ private int x; private int y; }