-
JAVA锁
2019-09-17 15:49:30一. JAVA中锁的概念 1、自旋锁: 2、乐观锁: 3、悲观锁: 4、独享锁: 5:共享锁:(限流)一. JAVA中锁的概念
1、自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其他的线程获取,那么该线程将循环等待,然后不断的判断是否能够被成功获取,知道获取到锁才会退出循环
2、乐观锁:假定没有冲突,在修改数据的时候如果发现和之前的获取的不一致,则读取最新的数据,修改后重试修改
3、悲观锁:界定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁
4、独享锁:给资源加上写锁,线程可以修改资源,其他线程不能再加锁(单写)
5:共享锁:(限流)给资源加上读锁之后只能读不能改,其他的线程也只能加读锁,不能加写锁(多读)
6、可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码二、Synchronized详解
首先说下对象在堆中存的是怎么存的?
1、对象值:最本质的就是对象在堆里面存了对象的值,类对象和一些方法的作为类的结构信息会存在方法区里面;如果类里面存在对象,那么只能存对象的引用。
2、对象头:里面有引用会指向类对象,通过这个标记,会记录对象的值是属于哪个类。
3、padding:对象存在堆中的规律是8个字节的整数倍,padding做补位,无太大的意义。重点在对象头部:
根据图片我们知道,对象头里面存了3个字段
Mark Word:锁的状态
Class Mate address:存储对象的引用,根据这个引用找到方法区的类对象。
Array length:如果当前对象是个数组对象,这个字段则标记数组的长度。如果不是数组,这个字段无意义。Synchronized的加锁过程如下:
要点在于Mark word:
1、线程会在虚拟机栈内开辟一块内存区域 叫做Lock Record
2、线程会把对象头里面的Mark Word里面的HashcodeAge0(无锁状态)写到这个Lock Record的内存区域
3、多个线程枪锁的时候就会进行CAS操作(新值:Lock Record Adress;旧值:HashcodeAge0),抢到锁的线程会把HashcodeAge0值替换成Lock Record Addess值,这个时候说明一个线程枪锁成功,没有抢到锁的线程会发生自旋,当自旋到达一定的次数(具体几次,暂时没有查到可靠的资料)之后,或者其他线程来了之后CAS的时候compare没有成功,这两种原因都会造成Mark Word里面的锁升级为重量级锁Monitor address。对象监视器Monitor:
JVM会帮我们维护一个特殊机制(对象监视器),java中每一个对象都可能存在一个对象监视器,这个监视器来监视我们的对象,最大的作用就是实现锁的机制,这个Monitor里面有一个owner,这个owner相当对一个线程的引用,它会记录是哪个线程抢到了锁,类似于owner=thread1。
未抢到锁的线程这个时候会进入到Monitor的锁池(entryList)里面等待状态为Blocked阻塞,锁池是一个队列,先进先出。当然Monitor里不光是只有锁池,还有等待池,当owner释放了锁【可以理解为owner=null】这个时候,线程如果调用wait()方法,由此可见,synchronized只有在获得锁之后才能调用wait()方法,调用wait()方法的线程就会进入等待池里面,线程状态就会变成waiting,然后线程如果调用notify()方法,就会进入锁池(entryList),状态就会重新变成Blocked状态;当然如果代码运行完了,锁就会自动释放。以上原理有点绕,但不是瞎编乱造,可以去hotspot官网下载源码来看,大部分看不懂,但是有一些是能看懂的
三、Lock详解
Lock接口的方法签名:
void lock(): 获取锁(获取不到一直获取) boolead tryLock(): 获取锁(获取不到我就不获取了) boolean tryLock(long time,TimeUnit unit) throws InterruptedException (获取锁,有等待时间,过时就不获取了) void lockInterruptibly() throws InterruptedException;获取锁 (可以被其他线程阻断获取) void unlock(); 释放锁 Condition newContidion(): 挂起或唤醒线程
其中lock()最常用;
复习一下线程通信的内容:
Object中的wait()/notify()、notifyAll()只能和synchronizde配合使用,可以唤醒一个或者多个线程;condetion需要和Lock配合使用,提供等待线程集合,可以更精准。好的来看代码演示:
package com.nipx.demo.CAS; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Condition_Demo1 { private static Lock lock = new ReentrantLock(); private static Condition condition = lock.newCondition(); public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { lock.lock(); try { System.out.println("线程开始挂起......"); condition.await(); System.out.println("线程开始释放......"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }); thread.start(); Thread.sleep(5000L); condition.signal(); } }
报错了:原因就是我们的condition不管是挂起还是唤醒,都必须和lock配合使用然后我们修改代码:package com.nipx.demo.CAS; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Condition_Demo1 { private static Lock lock = new ReentrantLock(); private static Condition condition = lock.newCondition(); public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { lock.lock(); try { System.out.println("线程开始挂起......"); condition.await(); System.out.println("线程开始释放......"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }); thread.start(); Thread.sleep(5000L); lock.lock(); condition.signal(); lock.unlock(); } }
可以了。然后我们演示死锁的代码,线程先唤醒,再挂起。
package com.nipx.demo.CAS; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Condition_Demo1 { private static Lock lock = new ReentrantLock(); private static Condition condition = lock.newCondition(); public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(5000L); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { System.out.println("5秒之后线程开始挂起等待......"); condition.await(); System.out.println("线程开始释放......"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }); thread.start(); Thread.sleep(2000L); lock.lock(); System.out.println("2秒之后线程先唤醒....."); condition.signal(); lock.unlock(); } }
然后我们用condition来实现一个阻塞队列:
/**
要求:阻塞队列只能存储n个元素
take 时:若队列有元素就直接获取;如果没有,则等待元素
put 时:若队列未满,就直接put;如果已满,则阻塞,等到再有空间去put
*/package com.nipx.demo.CAS; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Condition_Demo2 { private static Lock lock = new ReentrantLock(); private static Condition putCondition = lock.newCondition(); private static Condition takeCondition = lock.newCondition(); List<Object> list = new ArrayList(); private int length; public Condition_Demo2(int length) { this.length = length; } public static void main(String[] args) throws InterruptedException { Condition_Demo2 demo2 = new Condition_Demo2(4); new Thread(() -> { for (int i = 0; i < 20; i++) { try { demo2.put("元素:" + i); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); Thread.sleep(3000L); for (int i = 0; i < 10; i++) { demo2.take(); Thread.sleep(2000L); } } public void put(Object object) throws InterruptedException { lock.lock(); try { while (true) { if (list.size() < length) { list.add(object); takeCondition.signal(); System.out.println("put" + object); return; } else { putCondition.await(); } } } finally { lock.unlock(); } } public Object take() { lock.lock(); try { while (true) { if (list.size() > 0) { Object ob = list.remove(0); putCondition.signal(); System.out.println("take" + ob); return ob; } else { takeCondition.await(); } } } finally { lock.unlock(); return null; } } } 执行结果如下:
四、ReentrantLock底层原理
1、ReentrantLock是一把可重入锁,它的内存空间里面有3个东西
waiters: 等待池
owner: 锁持有者
count: 锁重入次数
多个线程来抢锁的时候,是根据CAS原理实现的,抢锁成功的线程,把自己的名字记 录到owner,然后执行count+1;未抢到锁的线程这个时候会进入到waiters(此时线程状态:waiting)等待池中,再次抢到锁的线程会修改count+1然后修改owner;如果是同一个线程再次抢到了锁,会直接进行count+1。2、自己实现一把可重入锁ReentrantLock代码如下:
package com.nipx.demo.CAS; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.LockSupport; public class ReentrantLock implements Lock { // 锁持有者 owner private AtomicReference<Thread> owner = new AtomicReference<>(); // 可重入次数 private AtomicInteger count = new AtomicInteger(0); // 等待池 private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue(); @Override public boolean tryLock() { int ct = count.get(); //判断count值是否为0,如果不为0,则说明锁被占用 if (ct != 0) { //判断是否是当前线程占用,做重入 if (Thread.currentThread() == owner.get()) { count.set(ct + 1); return true; } } else {// 判断count值如果为0,则通过CAS来抢锁 if (count.compareAndSet(ct, ct + 1)) { owner.set(Thread.currentThread()); } } return false; } @Override public void lock() { if (!tryLock()) { //加入等待队列 waiters.offer(Thread.currentThread()); while (true) {//自旋 //如果线程是队列的头部,则尝试加锁 Thread thread = waiters.peek(); if (thread == Thread.currentThread()) { if (!tryLock()) { LockSupport.park(); } else { waiters.poll(); return; } } else { LockSupport.park(); } } } } public boolean tryUnlock() { if (owner.get() != Thread.currentThread()) { throw new IllegalMonitorStateException(); } else { //unlock 就是将count-1 int newCount = count.get() - 1; count.set(newCount); //如果减1之后为0,说明成功释放锁 if (newCount == 0) { owner.compareAndSet(Thread.currentThread(), null); return true; } else { return false; } } } @Override public void unlock() { if (!tryUnlock()) { //获取头部的线程 Thread head = waiters.peek(); if (head != null) { LockSupport.unpark(head); } } } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public Condition newCondition() { return null; } }
五、读写锁ReentrantReadWriterLock
1、主要目的就是解决了性能问题,正常的读和写是互斥的,每次只能有一个线程去执行读或者写,这样就产生了性能问题,读写锁主要让读和写继续互斥,但是把读锁变成共享锁,写操作只需要一个线程操作,而读操作可以是多个线程来操作。
首先明确一点,Hashtable的源码解释是这样的:
【{@code Hashtable}是同步的。如果一个不需要线程安全实现,建议使用- {@link HashMap}代替了{@code Hashtable}。
- 如果一个线程安全的需要高并发的实现,然后推荐使用
*使用{@link java.util.concurrent。ConcurrentHashMap}代替】
也就是说JDK其实是不建议用hashtable的,对于高并发,它建议我们使用ConcurrentHashMap;如何用读写锁把HashMap变得线程安全?
代码如下:
package com.nipx.demo.CAS; import java.util.HashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class HashMap_Demo { private final HashMap<String, Object> map = new HashMap<>(); private final ReadWriteLock writeLock = new ReentrantReadWriteLock(); private final Lock r = writeLock.readLock(); private final Lock w = writeLock.writeLock(); public Object put(String key, Object value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } public Object get(Object key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } public void clear() { w.lock(); try { map.clear(); } finally { w.unlock(); } } }
2、读写锁的原理
读写锁是两把锁,一把是读锁,一把是写锁,他和ReentrantLock差不多,也有waiters等待池,owner和count;但是count分为readCount和writerCount。
waiters:等待池
owner:锁拥有着
readCount:读锁次数
writerCount:写锁次数假设现在又线程t1、t2、t3、t4。
如果现在t1线程去抢写锁,那么它会判断此时读锁里面的readCount是否为0,如果不为零,那么表示此时有线程在持有读锁,t1线程就会去等待池中;如果为0,那么t1线程就会抢锁成功,writerCount+1之后会把owner修改为自己,t2线程这时也要抢写锁,它首先也得判断此时读锁里面的readCount是否为0,如果为0,那么它就会进行CAS操作去抢写锁,抢锁失败就会去等待池中等待。
如果现在t3线程去抢读锁,那么它首先会判断此时写锁里面的writerCount是否为0,如果不为0,那么它说明此时有线程持有写锁,这个时候t3会对owner进行判断,如果是自己,那么进行锁降级,写锁降级为读锁;如果不是自己,那么t3就去等待池等待;如果为0,那么就去抢读锁,成功之后只会把readCount+1,并不会修改owner;t4线程这时候如果也来抢占读锁,判断此时写锁里面的writerCount是为0后,那么t4也会执行readCount+1。
这样就形成了单写多读的状态。
六、synchronized和lock的区别
synchronized和lock的区别在哪
1、synchronized:
【优点】:
a.使用简单,语义清晰,哪里需要加哪里。
b.由jvm提供,提出了很多优化(锁粗话,锁消除,偏向锁)
c.锁的释放由虚拟机提供,无人工化,降低了死锁的可能性
【缺点】
a.无法实现一些高级的功能,例如:公平锁、中断锁、超时锁、读写锁、共享锁2、lock:
【优点】:所有synchronized的缺点
【缺点】:需要手动释放锁 -
java 锁
2018-10-23 23:39:22Java对象保存在内存中时,由以下三部分组成: 1,对象头 2,实例数据 3,对齐填充字节 一,对象头 java的对象头由以下三部分组成: 1,Mark Word 2,指向类的指针 3,数组长度(只有数组对象才有) 1,...目录
一,对象头
1,Mark Word
2,指向类的指针
3,数组长度
二,实例数据
三,对齐填充字节
Java对象保存在内存中时,由以下三部分组成:
1,对象头
2,实例数据
3,对齐填充字节
一,对象头
java的对象头由以下三部分组成:1,Mark Word
2,指向类的指针
3,数组长度(只有数组对象才有)
1,Mark Word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
锁状态
25bit
4bit
1bit
2bit
23bit
2bit
是否偏向锁
锁标志位
无锁
对象的HashCode
分代年龄
0
01
偏向锁
线程ID
Epoch
分代年龄
1
01
轻量级锁
指向栈中锁记录的指针
00
重量级锁
指向重量级锁的指针
10
GC标记
空
11
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
JVM一般是这样使用锁和Mark Word的:
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
2,指向类的指针
该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。Java对象的类数据保存在方法区。
3,数组长度
只有数组对象保存了这部分数据。该数据在32位和64位JVM中长度都是32bit。
二,实例数据
对象的实例数据就是在java代码中能看到的属性和他们的值。三,对齐填充字节
因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。以上。
---------------------
作者:lkforce
来源:CSDN
原文:https://blog.csdn.net/lkforce/article/details/81128115
版权声明:本文为博主原创文章,转载请附上博文链接! -
Java锁机制
2019-06-20 12:57:09Java锁的划分 Java锁具体可分为悲观锁/乐观锁、自旋锁/适应性自旋锁、偏向锁、轻量级锁/重量级锁、公平锁和非公平锁、可重入锁/非可重入锁、共享锁/排他锁 具体划分如下: 乐观锁VS悲观锁 特征 对于同一个...Java锁的划分
Java锁具体可分为悲观锁/乐观锁、自旋锁/适应性自旋锁、偏向锁、轻量级锁/重量级锁、公平锁和非公平锁、可重入锁/非可重入锁、共享锁/排他锁
具体划分如下:
乐观锁VS悲观锁
概念
- 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
- 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据(可以使用版本号机制和CAS算法实现)。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
应用场景
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 传统的关系型数据库使用了很多悲观锁机制如行锁,表锁都是操作之前先上锁
- Java中的synchronized和ReentrantLock体现的就是悲观锁
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
- java.util.concurrent.atomic包下面的原子变量类就是基于CAS实现的乐观锁
优缺点
乐观锁缺点
-
ABA问题
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题, 它可以通过控制变量值的版本来保证 CAS 的正确性。 大部分情况下 ABA 问题不会影响程序并发的正确性, 如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 -
自旋时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用, 第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation) 而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 -
只能保证一个共享变量的原子操作 CAS只对单个共享变量有效,当操作涉及跨多个共享变量时CAS无效。 但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性, 可以把多个变量封装成对象里来进行 CAS 操作. 所以我们可以使用锁或者利用AtomicReference类把多个共享变量封装成一个共享变量来操作。
示例
// ------------------------- 悲观锁的调用方式 ------------------------- // synchronized public synchronized void testMethod() { // 操作同步资源 } // ReentrantLock private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁 public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock(); } // ------------------------- 乐观锁的调用方式 ------------------------- private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger atomicInteger.incrementAndGet(); //执行自增1
- 悲观锁显式进行锁定只同步操作
自旋锁 VS 适应性自旋锁
自旋锁概念
- 阻塞或者唤醒一个JAVA的线程需要操作系统切换CPU状态来完成,这种状态的转换需要耗费处理器时间。如果同步代码块中的内容过于简单,很可能导致状态转换消耗的时间比用户代码执行的时间还要长。所以在短暂的等待之后就可以继续进行的线程,为了让线程等待一下,需要让线程进行自旋,在自旋完成之后,前面锁定了同步资源的线程已经释放了锁,那么当前线程就可以不需要阻塞便直接获取同步资源,从而避免了线程切换的开销。这就是自旋锁。
自旋锁优缺点
- 优点: 阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。所以自旋锁就可以避免 CPU 切换带来的性能、时间等影响。
- 缺点: 自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程
适应性自旋锁
- 自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
示例
- AtomicInteger 是基于 CAS 实现的自旋锁
// JDK 8 // AtomicInteger 自增方法 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // Unsafe.class public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
无锁VS偏向锁
无锁
- 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
- 无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
- 在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
- 在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
- 偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
偏向锁的实现
偏向锁的获取
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
- 执行同步代码。
偏向锁的释放
* 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
轻量级锁VS重量级锁
概念
- 当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
轻量锁的加锁过程
- 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word
- 拷贝对象头中的Mark Word复制到锁记录中
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程
公平锁VS非公平锁
概念
- 公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
- 非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,如果的锁的状态可用,那么该线程会跳过所有等待直接获取锁,相当于有插队行为
优缺点
- 公平锁CPU唤醒阻塞线程的开销比非公平锁大,恢复一个挂起的线程到线程真正的去运行存在严重延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了
- 当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。
示例
- ReentrantLock 默认的lock()方法采用的是非公平锁
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
- 公平锁有个!hasQueuedPredecessors()条件,意思是说当前同步队列没有前驱节点(也就是没有线程在等待)时才会去compareAndSetState(0, acquires)使用CAS修改同步状态变量。所以就实现了公平锁,根据线程发出请求的顺序获取锁
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
- 非公平锁的实现在刚进入lock方法时会直接使用一次CAS去尝试获取锁,不成功才会到acquire方法,而在nonfairTryAcquire方法中并没有判断是否有前驱节点在等待,直接CAS尝试获取锁
/** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
可重入锁 VS 非可重入锁
概念
- 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
- 不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
- 不可重入锁,可能存在被当前线程所持有,且无法释放的死锁问题
独享锁VS共享锁
- 独享锁:该锁每一次只能被一个线程所持有,也叫排他锁,获得排他锁的 线程既能读数据又能写数据
- 共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占
- 独享锁与共享锁时通过AQS来实现 的
示例
- 独享锁ReentrantLock源码中可以看出无论是公平锁还是非公平锁,只要当线程不是拥有锁的线程,都不能加锁
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
- 共享锁tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态.如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥
- …
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount© != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount©;
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
引申
- CAS原理
https://blog.csdn.net/weixin_41950473/article/details/93994332 - AQS原理
- ReentrantLock锁
- 数据库的乐观锁和悲观锁
- Synchronized的底层优化
- ReentrantReadWriteLock原理及读锁与写锁
参考
-
Java锁升级
2020-04-06 01:36:52Java锁升级 对象内存布局 Java对象在内存中存储的布局可以分为3块区域: 对象头、实例数据、对齐填充。 对象头,分为两个部分,第一个部分存储对象自身的运行时数据,又称为Mark Word,32位虚拟机占32bit,64位虚拟机占...Java锁
锁概念
公平锁与非公平锁
公平锁:线程获取锁的顺序与其申请顺序一致
非公平锁:线程获取锁的顺序并不是按照申请锁顺序,有可能后申请锁的线程先获取锁。
可重入锁与不可重入锁
可重入锁:线程获取锁后,可以重复进入同步代码区,不需要重复申请锁
不可重入锁:线程每次进入同步代码区时,均需要获取锁,即使已经获取过该锁。
悲观锁与乐观锁
悲观锁:总是持有悲观的态度,认为并发冲突一般会发生
乐观锁:总是持有乐观的态度,认为并发冲突一般不会发生
锁消除与锁粗化
锁消除:消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
锁粗化:在遇到一连串地对同一锁不断进行请求和释放的操作时,把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数
自旋锁和自适应自旋
自旋锁:线程尝试获取锁,发现被占用后,不放弃cpu执行时间,进入阻塞状态,而是进入进行循环重试,重试指定次数后,仍未获取锁,则进入阻塞过程。
自适应自旋:重试次数不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
Java锁升级
对象内存布局
Java对象在内存中存储的布局可以分为3块区域: 对象头、实例数据、对齐填充。
对象头,分为两个部分,第一个部分存储对象自身的运行时数据,又称为
Mark Word
,32位虚拟机占32bit,64位虚拟机占64bit。如图所示,不同锁状态下,Mark Word的结构,理解下面要介绍的各种锁,和锁升级过程,都需要先充分了解Mark Word
的结构。第二部分是类型指针,指向类元数据指针,虚拟机通过此指针,确定该对象属于那个类的实例。
偏向锁
引入偏向锁的目的是在没有多线程竞争的前提下,进一步减少线程同步的性能消耗。
-
偏向锁的获取
开启偏向锁模式后,锁第一次被线程获取的时候,虚拟机会把对象头中
是否为偏向锁
的标志位设位1
,同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。当有另外一个线程去尝试获取这个锁时, 偏向模式就宣告结束。
根据锁对象目前是否处于被锁定的状态, 撤销偏向( Revoke Bias) 后恢复到未锁定( 标志位为“01”)或轻量级锁定( 标志位为“00”) 的状态
-
偏向锁的释放
偏向锁,并没有显式的锁释放过程,主要依靠锁的批量再偏向(Bulk Rebias)机制实现锁释放。
该机制的主要工作原理如下:
-
引入一个概念 epoch, 其本质是一个时间戳 , 代表了偏向锁的有效性,从前文描述的对象头结构中可以看到,
epoch 存储在可偏向对象的 MarkWord 中。
-
除了对象中的 epoch, 对象所属的类 class 信息中, 也会保存一个 epoch 值,每当遇到一个全局安全点时, 如果要对 class 进行批量再偏向, 则首先对 class 中保存的 epoch 进行增加操作, 得到一个新的 epoch_new
-
然后扫描所有持有 class 实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象, 仅将epoch_new 的值赋给被锁定的对象中。
-
退出安全点后, 当有线程需要尝试获取偏向锁时, 直接检查 class中存储的 epoch 值是否与目标对象中存储的 epoch 值相等,如果不相等, 则说明该对象的偏向锁已经无效了, 可以尝试对此对象重新进行偏向操作。
-
轻量级锁
轻量级锁是相对于重量级锁(
Synchrnoized
)而言的,本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。-
轻量级锁的获取
-
线程进入同步块时,如果此同步对象没有被锁定(即锁标志位为
01
,是否为偏向锁为0
),虚拟机在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的一个Mark Word
的copy ,ower设置为当前线程。 -
然后虚拟机使用CAS操作,尝试将
Mark World
更新为指向Lock Record
的指针,如果更新成功,那么线程拥有了该对象的锁,并且将锁标志位置位00
,如图所示 -
如果只有一个线程尝试获取轻量级锁,会进行自旋等待,一旦有两条以及以上的线程抢占该锁,轻量级锁会升级为重量级锁。
锁标志位置为
10
,Mark Word存储的就是指向重量级锁的指针
-
-
轻量级锁释放
- 如果对象的Mark Word仍然指向着线程的锁记录, 那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来, 如果替换成功, 整个同步过程就完成了。如果替换失败, 说明有其他线程尝试过获取该锁,轻量级锁膨胀为重量级锁,那就要在释放锁的同时, 唤醒被挂起的线程。
整个锁升级过程
参考文章
-
-
java锁消除和锁粗化
2020-06-09 16:37:49什么是java锁消除呢?其实这些都是JVM帮我们做的功能,在JVM判断一个锁不会被其他线程使用,就会把锁消除来提高性能。 比如下面代码,我们知道StringBuffer的append方法上加入了synchronized关键字,所以是加了锁的... -
Java锁消除和锁粗化
2018-06-02 12:29:49转载自:Java锁消除非商业转载,可联系本人删除概述锁消除是Java虚拟机在JIT编译是,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。实验看如下代码:package... -
Java锁之可重入锁和递归锁
2019-05-27 21:51:52Java锁之可重入锁和递归锁 目录 Java锁之可重入锁和递归锁基本概念 Java锁之可重入锁和递归锁代码验证 小结 理论,代码,小结,学习三板斧。 1. Java锁之可重入锁和递归锁基本概念 可重入锁(也... -
Java锁之乐观锁、悲观锁、自旋锁
2019-05-31 09:50:39java锁分为三大类乐观锁、悲观锁、自旋锁 乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有... -
java锁机制
2016-04-14 08:02:231.java锁种类及相关概念 1、自旋锁 2、自旋锁的其他种类 3、阻塞锁 4、可重入锁 5、读写锁 6、互斥锁 7、悲观锁 8、乐观锁 9、公平锁 10、非公平锁 11、偏向锁 12、对象锁 13、线程锁 14、锁粗化 15、轻量级锁 16、... -
Java锁的种类
2019-04-17 16:55:21Java锁的种类 内置锁 Java 提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包含两部分,一个作为锁的对象引用,一个作为由这个锁保护的代码块。以synchronized来修饰的方法就是一... -
Java锁的种类: 公平锁、乐观锁、互斥锁、分段锁、偏向锁、自旋锁等
2019-02-22 10:38:46Java锁的种类: 公平锁、乐观锁、互斥锁、分段锁、偏向锁、自旋锁等 -
Java锁汇总
2020-12-04 08:49:571、悲观锁 1.1 进程锁 1.1.1 Java Synchronized 1.1.2 Java ReentrantLock 1.1.3 Java ReadWriteLock 1.2 分布式锁 1.2.1 基于DataBase分布式锁 1.2.2 基于Zookeeper分布式锁 1.2.3 基于Redis分布式锁 2、... -
最全Java锁详解:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁
2018-12-14 14:15:23最全Java锁详解:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁乐观锁 VS 悲观锁1.乐观锁2.悲观锁3.总之公平锁 VS 非公平锁1.公平锁2.非公平锁3.典型应用独享锁 VS 共享锁1.独享锁2.共享锁3.比较4.AQS分段锁Java线程... -
Java锁之公平和非公平锁
2019-05-27 21:34:26Java锁之公平和非公平锁 目录 公平锁和非公平锁概念 公平锁和非公平锁区别 ReentrantLock和synchronized是公平锁还是非公平锁? 1. 公平锁和非公平锁概念 公平锁:是指多个线程按照申请锁的顺序来... -
【面试必备】深入浅出Java锁优化(偏向锁,轻量级锁,锁消除,锁粗化,自旋锁)
2020-02-27 23:11:14Java程序员面试必备,深入浅出Java锁优化。偏向锁,轻量级锁,锁消除,锁粗化,自旋锁最全总结 -
java并发编程之4——Java锁分解锁分段技术
2018-07-30 12:55:46转载自 java并发编程之4——Java锁分解锁分段技术 并发编程的所有问题,最后都转换成了,“有状态bean”的状态的同步与互斥修改问题。而最后提出的解决“有状态bean”的同步与互斥修改问题的方案是为所有修改... -
JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁
2018-06-25 15:45:37转载:JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁,如果需要查看具体的synchronized和lock的实现原理,请参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度) 在并发编程中,经常... -
Java锁lock源码分析(三)读写锁
2018-06-19 16:32:10Java锁lock源码分析(三)读写锁 前文Java锁Lock源码分析(一)提过在java的Lock中获取锁就表示AQS的volatile int state =1表示获取到了独占锁,state&amp;amp;amp;amp;gt;1表示当前线程重入锁(获取锁了... -
详解Java锁机制:看完你就明白的锁系列之锁的状态
2019-10-23 20:30:29详解Java锁机制:看完你就明白的锁系列之锁的状态 前言: 看完你就会知道,线程如果锁住了某个资源,致使其他线程无法访问的这种锁被称为悲观锁,相反,线程不锁住资源的锁被称为乐观锁,而自旋锁是基于 CAS 机制... -
Java锁的几种应用
2018-01-19 15:31:30Java锁常用概念:volatile/synchronized/ReentrantLock的区别 -
面试必会系列 - 1.5 Java 锁机制
2020-09-04 12:25:48Java 锁机制 概览 syncronized 锁升级过程 ReentrantLock 可重入锁 volatile 关键字 JUC 包下新的同步机制 syncronized 给一个变量/一段代码加锁,线程拿到锁之后,才能修改一个变量/执行一段代码 wait() notify... -
Java锁实现
2019-03-19 10:45:07Java编程语言允许线程访问共享变量, 为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。 volatile在多处理器开发中保证了... -
Java 锁的理解
2020-08-08 10:52:33用过并发包的朋友对这个应该不会陌生,ReentrantLock 是 Java 的 JUC(java.util.concurrent)包中提供的一种可重入锁,是一种递归无阻塞的同步机制。ReentrantLock 等同于synchronized关键字,但是 ... -
Java锁详解
2019-06-29 22:57:22文章目录什么是锁锁的实现方式锁涉及的几个重要概念类锁和对象锁(重要)synchronized实现原理 什么是锁 计算机还是单线程的时代,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就... -
Java锁Lock源码分析(二)条件锁
2018-03-01 12:06:43本篇博文主要分析条件锁的源码实现、以及状态两个队列的变化: 1)Condition的使用场景 2)lock方法的队列(FIFO双向无环链表)官方点说是同步队列 sync queue ...本文是依赖于上篇博文Java锁... -
java锁升级过程
2020-03-14 20:32:17java中对象锁有4种状态:(级别从低到高) 1.无锁状态 2.偏向锁状态 3.轻量级锁状态 4.重量级锁状态 对象头分两部分信息,第一部分用于存储哈希码、GC分代年龄等,这部分数据被称为"Mark Word"。在32位的HotSpot... -
Java锁类型
2017-03-07 14:00:43转载链接在每个锁类型后边 线程锁类型 1、自旋锁 ,自旋,jvm默认是10次吧,有jvm自己控制。for去争取锁 锁作为并发共享数据,保证一致性的工具,在...本系列文章将分析JAVA下常见的锁名称以及特性,为大家答疑解惑。