JDK内部锁及其原理整理,笔记

it2022-05-05  117

目录

 

1、自旋锁

2、阻塞锁

3、可重入锁

4、读写锁

5、互斥锁

6、悲观锁、乐观锁

7、公平锁、非公平锁

8、偏向锁

9、锁粗化

10、轻量级锁

11、锁消除

12、锁膨胀


1、自旋锁

1、自旋锁(spinlock): 是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

2、自旋锁的优点 :

自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

3、自旋锁应用 :

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。

如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

4、简单自旋锁的实现 :

public class SimpleSpinLock {     /**      * 持有锁的线程,null表示锁未被线程持有      */     private static AtomicReference<Thread> ref = new AtomicReference<>();     public void Lock() {         Thread currentThread = Thread.currentThread();         // 当ref为null的时候compareAndSet返回true,反之为false         // 通过循环不断的自旋判断锁是否被其他线程持有         while (!ref.compareAndSet(null, currentThread)) {         }     }     public void unLock() {         Thread currentThread = Thread.currentThread();         if (ref.get() != currentThread) {         }         ref.set(null);     } } test: public class SimpleSpinLockTest {     private static int n = 0;     public static void main(String[] args) throws InterruptedException {         ThreadPoolExecutor pool = new ThreadPoolExecutor(100, 100, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new DefaultNameThreadFactory("SimpleSpinLock"));         CountDownLatch countDownLatch = new CountDownLatch(100);         SimpleSpinLock simpleSpinLock = new SimpleSpinLock();         for (int i = 0; i < 100; i++) {             pool.submit(() -> {                 simpleSpinLock.Lock();                 n++;                 simpleSpinLock.unLock();                 // 计数减一                 countDownLatch.countDown();             });         }         // 要求主线程等待所有任务全部准备好才一起并行执行         countDownLatch.await();         System.out.println(n);     } }

5、可重入的自旋锁和不可重入的自旋锁 :仔细分析一下上述就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。

由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数----》其他章节可重入锁

6、  在自旋锁中 另有三种常见的锁形式 :

TicketLock ,CLHlock 和 MCSlock:https://www.cnblogs.com/stevenczp/p/7136416.html

2、阻塞锁

1、阻塞锁,与自旋锁不同,改变了线程的运行状态。

      在JAVA环境中,线程Thread有如下几个状态:

新建状态就绪状态运行状态阻塞状态死亡状态

      阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。       JAVA中,能够进入 / 退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLockObject.wait() / notify() LockSupport.park() / unpart() 

 

2、阻塞锁的优势:在于,阻塞的线程不会占用CPU时间, 不会导致 CPU占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。

在竞争激烈的情况下 阻塞锁的性能要明显高于自旋锁。

3、阻塞锁应用:理想的情况则是, 在线程竞争不激烈的情况下,使用自旋锁;竞争激烈的情况下使用,阻塞锁。

4、阻塞锁的简单实现:

public class ClhLock {     /**      * 定义一个节点,默认的lock状态为true      */     public static class ClhNode {         private volatile Thread isLocked;     }     /**      * 尾部节点,只用一个节点即可      */     @SuppressWarnings("unused")     private volatile ClhNode tail;     private static final ThreadLocal<ClhNode> LOCAL = new ThreadLocal<>();     private static final AtomicReferenceFieldUpdater<ClhLock, ClhNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(ClhLock.class, ClhNode.class, "tail");     public void lock() {         // 新建节点并将节点与当前线程保存起来         ClhNode node = new ClhNode();         LOCAL.set(node);         // 将新建的节点设置为尾部节点,并返回旧的节点(原子操作),这里旧的节点实际上就是当前节点的前驱节点         // 个人理解=>大概相当于把AtomicReferenceFieldUpdater中原有的tail取出,并用新建的节点将原有的tail替代,这个操作是原子性的。         // 操作原子性的由来:AtomicReferenceFieldUpdater是一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(这个字段不能是private的)。         ClhNode preNode = UPDATER.getAndSet(this, node);         if (preNode != null) {             preNode.isLocked = Thread.currentThread();             LockSupport.park(this);             preNode = null;             LOCAL.set(node);         }         // 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁     }     public void unLock() {         // 获取当前线程对应的节点         // 对应博客中的这句话:申请线程只在本地变量上自旋,避免了多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum         // 每次读写操作都必须在多个处理器缓存之间进行缓存同步         ClhNode node = LOCAL.get();         // 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态职位false,表示当前线程释放了锁         if (!UPDATER.compareAndSet(this, node, null)) { //            System.out.println("unlock\t" + node.isLocked.getName());             LockSupport.unpark(node.isLocked);         }         node = null;     } } demo: public class ClhLockTest {     private static int num = 0;     public static void main(String[] args) throws InterruptedException {         ThreadPoolExecutor pool = new ThreadPoolExecutor(1000, 1000, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new DefaultNameThreadFactory("SimpleSpinLock"));         CountDownLatch countDownLatch = new CountDownLatch(1000);         ClhLock clhLock = new ClhLock();         for (int i = 0; i < 1000; i++) {             pool.submit(() -> {                 clhLock.lock();                 num++;                 clhLock.unLock();                 // 计数减一                 countDownLatch.countDown();             });         }         // 要求主线程等待所有任务全部准备好才一起并行执行         countDownLatch.await();         System.out.println(num);     } }

3、可重入锁

1、可重入锁:也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。                         “独占”,就是在同一时刻只能有一个线程获取到锁,而其它获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

                        “可重入”,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

                        在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。

2、Synchronized和ReentrantLock

1)性能区别:

         在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实    synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

2)原理区别:

         Synchronized: 进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

         ReentrantLock: 是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

等待可中断持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

3) demo 

public class SynchronizedTest implements Runnable {     public synchronized void get() {         System.out.println(Thread.currentThread().getName());         set();     }     public synchronized void set() {         System.out.println(Thread.currentThread().getName());     }     @Override     public void run() {         get();     }     public static void main(String[] args) {         SynchronizedTest synchronizedTest = new SynchronizedTest();         new Thread(synchronizedTest).start();         new Thread(synchronizedTest).start();         new Thread(synchronizedTest).start();     } } public class ReentrantLockTest implements Runnable {     ReentrantLock lock = new ReentrantLock();     public void get() {         lock.lock();         System.out.println(Thread.currentThread());         set();         lock.unlock();     }     public void set() {         lock.lock();         System.out.println(Thread.currentThread());         lock.unlock();     }     @Override     public void run() {         get();     }     public static void main(String[] args) {         ReentrantLockTest lock = new ReentrantLockTest();         new Thread(lock).start();         new Thread(lock).start();         new Thread(lock).start();     } }

4、读写锁

1、读写锁介绍:

        ReadWriteLock同Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁。

        理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。

        一些业务场景中,大部分 只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。 针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。

        读-读能共存,         读-写不能共存,         写-写不能共存。

连接:https://blog.csdn.net/j080624/article/details/82790372、https://ifeve.com/read-write-locks/

2、总结:

公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁

3、写锁的获取:

        写锁是独占式锁,而实现写锁的同步语义是通过重写 AQS 中的 tryAcquire() 方法实现的,源码:

protected final boolean tryAcquire(int acquires) {     Thread current = Thread.currentThread();     // 1. 获取 写锁 当前的同步状态     int c = getState();     // 2. 获取 写锁 获取的次数     int w = exclusiveCount(c);     if (c != 0) {         // (Note: if c != 0 and w == 0 then shared count != 0)         // 3.1 当 读锁 已被读线程获取 或者 当前线程不是已经获取 写锁 的线程的话         // 当前线程获取 写锁失败         if (w == 0 || current != getExclusiveOwnerThread())             return false;         if (w + exclusiveCount(acquires) > MAX_COUNT)             throw new Error("Maximum lock count exceeded");         // Reentrant acquire         // 3.2 当前线程 获取写锁,支持可重复加锁         setState(c + acquires);         return true;     }     // 3.3 写锁 未被任何线程获取,当前线程可获取 写锁     if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))         return false;     setExclusiveOwnerThread(current);     return true; }  static int exclusiveCount(int c) {         return c & EXCLUSIVE_MASK;  }

其中EXCLUSIVE_MASK为:  static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;      EXCLUSIVE _MASK为1左移16位然后减1,即为0x0000FFFF。

而exclusiveCount方法是将同步状态(state为int类型)与0x0000FFFF相与,即取同步状态的低16位。那么低16位代表什么呢?

根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论同步状态的低16位用来表示写锁的获取次数

static int sharedCount(int c)    {         return c >>> SHARED_SHIFT; }

该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,现在我们可以得出另外一个结论同步状态的高16位用来表示读锁被获取的次数。

当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。

4、写锁的释放:

    写锁释放通过重写AQS的tryRelease方法,源码为:

protected final boolean tryRelease(int releases) {     if (!isHeldExclusively())         throw new IllegalMonitorStateException();     //1. 同步状态减去写状态     int nextc = getState() - releases;     //2. 当前写状态是否为0,为0则释放写锁     boolean free = exclusiveCount(nextc) == 0;     if (free)         setExclusiveOwnerThread(null);     //3. 不为0则更新同步状态     setState(nextc);     return free; }

    减少写状态int nextc = getState() - releases,只需要用当前同步状态直接减去写状态的原因:写状态是由同步状态的低16位表示的。

5、读锁的获取

        读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。

protected final int tryAcquireShared(int unused) {     Thread current = Thread.currentThread();     int c = getState();     //1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前     // 线程获取读锁失败返回-1     if (exclusiveCount(c) != 0 &&         getExclusiveOwnerThread() != current)         return -1;     int r = sharedCount(c);     if (!readerShouldBlock() &&         r < MAX_COUNT &&         //2. 当前线程获取读锁         compareAndSetState(c, c + SHARED_UNIT)) {         //3. 下面的代码主要是新增的一些功能,比如getReadHoldCount()方法         //返回当前获取读锁的次数         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;     }     //4. 处理在第二步中CAS操作失败的自旋已经实现重入性     return fullTryAcquireShared(current); }

    当写锁被其他线程获取后,读锁获取失败,否则获取成功利用CAS更新同步状态。

6、读锁的释放

protected final boolean tryReleaseShared(int unused) {     Thread current = Thread.currentThread();     // 前面还是为了实现getReadHoldCount等新功能     if (firstReader == current) {         // assert firstReaderHoldCount > 0;         if (firstReaderHoldCount == 1)             firstReader = null;         else             firstReaderHoldCount--;     } else {         HoldCounter rh = cachedHoldCounter;         if (rh == null || rh.tid != getThreadId(current))             rh = readHolds.get();         int count = rh.count;         if (count <= 1) {             readHolds.remove();             if (count <= 0)                 throw unmatchedUnlockException();         }         --rh.count;     }     for (;;) {         int c = getState();         // 读锁释放 将同步状态减去读状态即可         int nextc = c - SHARED_UNIT;         if (compareAndSetState(c, nextc))             // Releasing the read lock has no effect on readers,             // but it may allow waiting writers to proceed if             // both read and write locks are now free.             return nextc == 0;     } }

 7、锁降级

        读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:

void processCachedData() {         rwl.readLock().lock();         if (!cacheValid) {             // Must release read lock before acquiring write lock             rwl.readLock().unlock();             rwl.writeLock().lock();             try {                 // Recheck state because another thread might have                 // acquired write lock and changed state before we did.                 if (!cacheValid) {                     data = ...             cacheValid = true;           }           // Downgrade by acquiring read lock before releasing write lock           rwl.readLock().lock();         } finally {           rwl.writeLock().unlock(); // Unlock write, still hold read         }       }         try {         use(data);       } finally {         rwl.readLock().unlock();       }     } }

5、互斥锁

1、关于“互斥”和“同步”的概念

互斥 : 就是线程A访问了一组数据,线程BCD就不能同时访问这些数据,直到A停止访问了同步 : 就是ABCD这些线程要约定一个执行的协调顺序,比如D要执行,B和C必须都得做完,而B和C要开始,A必须先得做完。

互斥 :就是不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止访问冲突,在有限的时间内只允许其中之一独占性的使用共享资源。如不允许同时写

同步 :关系则是多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问。如先生产后使用

总的来说,两者的区别就是:

互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。

同步是协调多个相互关联线程合作完synchronized不同用法锁对象说明

2、JAVA中synchronized和Lock是互斥锁

修饰在静态方法上,锁对象是当前类的Class对象 修饰在实例方法上,锁对象是当前实例对象 同步块中,锁对象是synchronized括号后面的对象成任务,彼此之间知道对方存在,执行顺序往往是有序的。

2、synchronized的用法

/** 如下demo的4个方法展示了不同使用方法下锁对象 **/ public class SynchronizedDemo {     private static final Object LOCK = new Object();     public static synchronized void s1(){         System.out.println("类同步方法,锁对象是当前Class对象");     }     public synchronized void s2() {         System.out.println("实例同步方法,锁对象是当前对象");     }     public void s3() {         synchronized (LOCK) {             System.out.println("同步块,锁对象是LOCK对象");         }     }     public void s4() {         synchronized (SynchronizedDemo.class) {             System.out.println("同步块,锁对象和静态同步方法的锁对象一样都是当前Class对象");         }     } }

6、悲观锁、乐观锁

1、悲观锁:假设会发生并发冲突,屏蔽一切可能违反数据完整性的操作(具有强烈的独占和排他性)

           依赖数据库的锁机制实现,以保证操作最大程度的独占性。

     百度百科:正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

2、缺点:数据库性能的大量开销,特别是对长事务而言,这样的开销无法承受

3、实现方法:

    Mysql中 :

    在sql后面加上 for update或者for update nowait

    for update和for update nowait区别:

         1. for update 锁定当前操作数据,其他事务等待

         2. for update nowait 锁定当前数据,其他事务发现数据被锁定,立即返回"ORA-00054错误,内容是资源正忙, 但指定以 NOWAIT 方式获取资源"

         例如:select * from account where name="123" for update

         优点:无论是在单机还是分布式中,只要使用的是同一个数据库,那么悲观锁就能起到作用。

         缺点:锁定数据后,必将影响其他操作,在大流量的情况下,操作速度变慢

    JAVA中 :

        独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

 

4、使用场景举例:以MySQL InnoDB为例

   Demo:

        begin;

        select amount from item where item_id = 1 for update;

        // 通过amount来做出一些行为,例如告诉用户库存不足,购买失败,然后只有amount > 1才进入更新库存操作

        update item set amount = amount - 1 where item_id = 1;

        commit;

    由于是串行执行,其他事务的for update必须等该当前事务的 for update 语句执行,所以我们不必担心我们获得的amount被修改过,因为它永远是最新的

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

0、乐观锁:不是真正的锁,而是一种实现 : 是一种实现的

1、乐观锁:假设不会发生并发冲突,只有在提交操作时检查是否违反数据完整性,乐观锁不能解决脏读问题

            乐观锁大多都基于数据版本(version)记录机制实现,何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据表增加一个“version”字段来实现。读取出数据时,将此版本一同读出,之后更新时,对此版本后 +1。此时,将提交的版本数据与数据库表对应记录的当前版本信息对比时,如果提交的数据版本号大于数据库当前版本号,则予以更新,否则认为是过期数据。

 2、优缺点:

        优点 :可以多个事务同时进行,然后根据返回的不同结果做相应的操作,避免了长事务中的数据库加锁开销

        缺点 :乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。

在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

3、步骤 :

        // 1.查询出商品信息

        select (status,status,version) from t_goods where id=#{id}

       // 2.根据商品信息生成订单

       // 3.修改商品

        update t_goods

        set status=2,version=version+1 where id=#{id} and versio{139}};

7、公平锁、非公平锁

1、概念:

        公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。       公平所:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。

        更多的是直接使用非公平锁:非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数。

        原理 : https://www.cnblogs.com/little-fly/p/10365109.html

        https://www.jianshu.com/p/06340f8feb05

2、Java语言中:

    公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。

    两者的区别:https://www.jianshu.com/p/c7d17b5c6be3

8、偏向锁

0、从偏向锁到重量锁

    在java同步代码快中,synchronized的使用方式无非有两个 :   

    1)通过对一个对象进行加锁来实现同步

synchronized(lockObject){     //代码 }

     2)对一个方法进行synchronized声明,进而对一个方法进行加锁来实现同步。

public synchornized void test(){     //代码 }

     无论是对一个对象进行加锁还是对一个方法进行加锁,实际上,都是对对象进行加锁。

1、先了解一下对象在JVM内存中的布局,如下图

        Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。在32位系统占4字节,在64位系统中占8字节;

         Class Pointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;

         Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;

         对齐填充:Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。

        从上图我们可以看出,对象中关于锁的信息是存在Markword里的。

2、锁的创建

// 随便创建一个对象

LockObject lockObject = new LockObject(); synchronized(lockObject){     //代码 }

    1)当我们创建一个对象LockObject时,该对象的部分Markword关键数据如下。

 bit fields

 是否偏向锁

锁标志位 

 hash

 0

 01

         从图中可以看出,偏向锁的标志位是“01”,状态是“0”,表示该对象还没有被加上偏向锁。(“1”是表示被加上偏向锁)。

         该对象被创建出来的那一刻,就有了偏向锁的标志位,这也说明了所有对象都是可偏向的,但所有对象的状态都为“0”,也同时说明所有被创建的对象的偏向锁并没有生效

    2)不过,当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。

          此时的Mark word的结构信息如下:

 bit fields

 

是否偏向锁 

 锁标志位

hash

epoch

1

 01

          此时偏向锁的状态为“1”,说明对象的偏向锁生效了,同时也可以看到,哪个线程获得了该对象的锁。   

    3)这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。

    4)在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:

        a、Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.         b、如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.         c、如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。         d、如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。

    5)如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。可以看出,偏向锁是针对于一个线程而言的,线程获得锁之后就不会再有解锁等操作了,这样可以省略很多开销。假如有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。

   6为什么要这样做呢?因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。这也是为什么会有偏向锁出现的原因。在Jdk1.6之后,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景

 

9、锁粗化

转自:https://blog.csdn.net/qq_26222859/article/details/80546917

参考:https://www.jianshu.com/p/f05423a21e78

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

public void doSomethingMethod(){     synchronized(lock){         //do some thing     }     //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕     synchronized(lock){         //do other thing     } }

上面的代码是有两块需要同步操作的,但在这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后的代码如下 :

public void doSomethingMethod(){     //进行锁粗化:整合成一次锁请求、同步、释放     synchronized(lock){         //do some thing         //做其它不需要同步但能很快执行完的工作         //do other thing     } }

 注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。

另一种需要锁粗化的极端的情况是:

for(int i=0;i<size;i++){     synchronized(lock){     } }

 上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在jdk内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。

锁粗化后的代码如下:

synchronized(lock){     for(int i=0;i<size;i++){     } }

10、轻量级锁

锁撤销升级为轻量级锁之后,那么对象的Markword也会进行相应的的变化。

    下面先简单描述下锁撤销之后,升级为轻量级锁的过程:

    a) 线程在自己的栈桢中创建锁记录 LockRecord。     b) 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。     c) 将锁记录中的Owner指针指向锁对象。     d) 将锁对象的对象头的MarkWord替换为指向锁记录的指针。

11、锁消除

由于偏向锁失效了,那么接下来就得把该锁撤销,锁撤销的开销花费还是挺大的,其大概的过程如下:

    a) 在一个安全点停止拥有锁的线程。

    b) 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。

    c) 唤醒当前线程,将当前锁升级成轻量级锁。

所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭

12、锁膨胀

当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。这也是我们经常所说的锁膨胀

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

 


最新回复(0)