当前位置: 澳门新濠3559 > 编程 > 正文

线程B调用对象lock的notifyAll()方法,AQS还提供了这

时间:2019-10-07 13:12来源:编程
简书 占小狼转载请注明原创出处,谢谢! synchronized可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区,同时它还保证了共享变量的内存可见性。 Synchronized是

简书 占小狼转载请注明原创出处,谢谢!

synchronized可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区,同时它还保证了共享变量的内存可见性。

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。

java提供了内置锁,即synchronized,除此以外,还提供了显式锁,下面我们分别分析其实现的机制,并讨论如何在这两者之间进行选择。

接上一篇《Java并发编程系列之volatile》,这是第二篇,说的是关于并发编程的synchronized元素。

Java中的每个对象都可以作为锁。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

AQS


AQS即AbstractQueuedSynchronizer,一般用于管理同步类中的状态,它管理了一个整数状态信息,可以使用getState setState以及compareAndSetState来获取或修改状态。比如Semaphore可以用这个状态表示剩余的许可数目,ReentrantLock可以用它来表示当前持有锁的线程已经加锁的次数等等。其获取和释放操作的伪代码如下:

boolean acquire() throws InterruptedException {
  while(当前状态不允许获取) {
    if (需要阻塞获取请求) {
      当前线程不在队列中,则将其插入等待队列
      将当前调用线程阻塞
    } else {
      返回失败
    }
  }
  获取成功 修改状态
  将当前线程移出等待队列
  返回成功
}

void release() {
  更新状态
  if (新的状态允许别的线程获取资源) {
    选择一个或多个线程唤醒
  }
}

比如一个简单的二元闭锁(代码来自java并发编程实战)

public class OneShotLatch {
  private final Sync sync = new Sync();

  public void signal() { sync.releaseShared(0); }

  public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(0);
  }

  private class Sync extends AbstractQueuedSynchronizer {
    protected int tryAcquireShared(int ignored) {
      return (getState()) == 1 ? 1 : -1;
    }

    protected boolean tryReleaseShared(int ignored) {
      setState(1);
      return true;
    }
  }
}

AQS state为1时表示打开,0表示关闭。await调用acquireSharedInterruptibly,然后会去调用tryAcquireShared,如果state是1则tryAcquireShared返回成功并允许线程通过,否则线程将进入等待线程队列中去。

signal调用releaseShared,然后调用tryReleaseShared,然后让所有等待中的线程都再次尝试请求该同步器,从而通过闭锁。另外,AQS还提供了这些操作的限时版本,从而可以实现有时限的等待操作。

  1. 使用方法
  1. 普通同步方法,锁是当前实例对象。
  2. 静态同步方法,锁是当前类的class对象。
  3. 同步代码块,锁是括号中的对象。

1、普通同步方法,锁是当前实例对象

acquire

下面我们来看看acquire的代码:

  public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

上面的代码是一个模板方法,先调用了tryAcquire,如果失败,则调用addWaiter将当前线程加入等待队列中,然后再使用acquireQueued来尝试获取资源。完成后如果有中断,则调用selfInterrupt传递中断状态。

在addWaiter中,使用Node类封装了当前线程,Node的状态有:

        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;

使用prev和next可以形成一个链表:

 *       ------   prev  -----         ----- 
 * head |      | <---- |     | <---- |     |  tail
 *       ------         -----         ----- 

关于状态,直接摘抄jdk8源码的注释:

     *   SIGNAL:     The successor of this node is (or will soon be)
     *               blocked (via park), so the current node must
     *               unpark its successor when it releases or
     *               cancels. To avoid races, acquire methods must
     *               first indicate they need a signal,
     *               then retry the atomic acquire, and then,
     *               on failure, block.
     *   CANCELLED:  This node is cancelled due to timeout or interrupt.
     *               Nodes never leave this state. In particular,
     *               a thread with cancelled node never again blocks.
     *   CONDITION:  This node is currently on a condition queue.
     *               It will not be used as a sync queue node
     *               until transferred, at which time the status
     *               will be set to 0. (Use of this value here has
     *               nothing to do with the other uses of the
     *               field, but simplifies mechanics.)
     *   PROPAGATE:  A releaseShared should be propagated to other
     *               nodes. This is set (for head node only) in
     *               doReleaseShared to ensure propagation
     *               continues, even if other operations have
     *               since intervened.
     *   0:          None of the above
  • SIGNAL 这个记号表示当前node之后的节点已经或即将被阻塞,需要再release或者cancel后唤醒一个或若干个后续节点

  • CANCELLED表示这个线程已经cancel掉了

  • CONDITION 说明这个node在一个condition queue上 和sync queue没什么鸟关系

  • PROPAGATE releaseShared需要被所有等待中的node得知,doReleaseShared中会使用该状态

了解了Node类后,我们来看看addWaiter

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
  1. 如果目前队列非空,则尝试快速入队,使用CAS把自己设置为tail,并且返回自己对应的node
  2. 如果快速入队失败了 就调用enq将当前node入队
  3. 首先,如果队列为空的话会在队列中添加第一个dummy的节点,此时head == tail ==dummy节点 如果失败说明已经有别的线程设置过tail了 就再循环之前的操作
  4. tail非空,则自旋的将自己入队

入队以后,就调用acquireQueued。

  1. 获取该节点前驱 如果前驱是head,说明前面已经没有等待中的节点了,就尝试tryAcquire,成的话讲当前节点设置为head,返回
  2. 没有tryAcquire成功,则查看是否应当block当前线程 唤醒后检查thread.interrupted()状态
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

下面看看shouldParkAfterFailedAcquire,这个方法检查一个acquire失败的线程的状态,看看是否需要park(block)它。先看看node的prev的状态

  1. 如果是SIGNAL说明已经做好稍后被唤醒的准备了,返回true表明可以被park。
  2. 如果大于0说明被cancel了,那么一直找prev直到找到状态小于等于0的节点
  3. 如果都不是 那么把pred的状态设置为SIGNAL 为之后的park做准备,进入下一次acquireQueued的循环中
  4. 每次唤醒后,都会检查并传递中断状态 用acquireQueued返回,如果有中断 则抛出中断异常
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

synchronized 是 java 中最常用的保证线程安全的方式,synchronized 的作用主要有三方面:

先看一个场景等待 / 通知机制直接上代码:

public class SynchronizedTest {
 4     public synchronized void method1(){
 5         System.out.println("Method 1 start");
 6         try {
 7             System.out.println("Method 1 execute");
 8             Thread.sleep(3000);
 9         } catch (InterruptedException e) {
10             e.printStackTrace();
11         }
12         System.out.println("Method 1 end");
13     }
14 
15     public synchronized void method2(){
16         System.out.println("Method 2 start");
17         try {
18             System.out.println("Method 2 execute");
19             Thread.sleep(1000);
20         } catch (InterruptedException e) {
21             e.printStackTrace();
22         }
23         System.out.println("Method 2 end");
24     }
25 
26     public static void main(String[] args) {
27         final SynchronizedTest test = new SynchronizedTest();
28 
29         new Thread(new Runnable() {
30             @Override
31             public void run() {
32                 test.method1();
33             }
34         }).start();
35 
36         new Thread(new Runnable() {
37             @Override
38             public void run() {
39                 test.method2();
40             }
41         }).start();
42     }
43 }

release

然后看看release的过程,会调用子类的tryRelease,这是一个模板方法模式。如果处理成功且head不为空且不是开始时添加的一个dummy head,则尝试调用unparkSuccessor唤起后继的线程们。

  1. 如果head的后继是null或已经被cancel了 则从tail开始向前找到一个等待中的线程。

  2. 如果有可以唤醒的线程,唤醒之,让其重新回到acquireQueued的无限循环中去

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

确保线程互斥的访问代码块,同一时刻只有一个方法可以进入到临界区

import java.util.concurrent.TimeUnit;/** * Created by j_zhan on 2016/7/6. */public class WaitNotify { static boolean flag = true; static Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread A = new Thread(new Wait(), "wait thread"); A.start(); TimeUnit.SECONDS.sleep; Thread B = new Thread(new Notify(), "notify thread"); B.start(); } static class Wait implements Runnable { @Override public void run() { synchronized  { while  { try { System.out.println(Thread.currentThread()   " flag is true"); lock.wait(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread()   " flag is false"); } } } static class Notify implements Runnable { @Override public void run() { synchronized  { flag = false; lock.notifyAll(); try { TimeUnit.SECONDS.sleep; } catch (InterruptedException e) { e.printStackTrace(); } } } }}

2、静态同步方法,锁是当前类的class对象

synchronized

被动锁,即synchronized方法或代码块的原理与基于AQS的主动锁有所区别,先来一段代码:

public class TestSynchronized {
    public static void main(String[] args) {
        synchronized (TestSynchronized.class) {
        }
    }

    public synchronized void method() {
    }
}

javap -c -verbose 以后看到:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class test/TestSynchronized
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any
      LineNumberTable:
        line 7: 0
        line 8: 5
        line 9: 15
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 10
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 12: 0

可以看到,同步代码块前后有monitorenter 和 monitorexit 指令,而同步方法是在修饰符上添加了ACC_SYNCHRONIZED。

那么,synchronized到底是靠什么实现的呢?要了解这些,我们首先要明确两个概念:对象头和monitor。

保证共享变量的修改能及时可见

其相关方法定义在java.lang.Object上,线程A在获取锁后调用了对象lock的wait方法进入了等待状态,线程B调用对象lock的notifyAll()方法,线程A收到通知后从wait方法处返回继续执行,线程B对共享变量flag的修改对线程A来说是可见的。

public class SynchronizedTest {
 4      public static synchronized void method1(){
 5          System.out.println("Method 1 start");
 6          try {
 7              System.out.println("Method 1 execute");
 8              Thread.sleep(3000);
 9          } catch (InterruptedException e) {
10              e.printStackTrace();
11          }
12          System.out.println("Method 1 end");
13      }
14  
15      public static synchronized void method2(){
16          System.out.println("Method 2 start");
17          try {
18              System.out.println("Method 2 execute");
19              Thread.sleep(1000);
20          } catch (InterruptedException e) {
21              e.printStackTrace();
22          }
23          System.out.println("Method 2 end");
24      }
25  
26      public static void main(String[] args) {
27          final SynchronizedTest test = new SynchronizedTest();
28          final SynchronizedTest test2 = new SynchronizedTest();
29  
30          new Thread(new Runnable() {
31              @Override
32              public void run() {
33                  test.method1();
34              }
35          }).start();
36  
37          new Thread(new Runnable() {
38              @Override
39              public void run() {
40                  test2.method2();
41              }
42          }).start();
43      }
44  }

对象头

HotSpot的每个对象都有一个头部,包括两部分信息:Mark Word(标记字段)和 Klass Pointer(类型指针)。

其中:

  • Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。如果对象是一个数组,对象头中还会有一块用于记录数组长度的数据。在不同的情况下,mark word可以变长的表示不同的含义,例如在 32 位的HotSpot 虚拟机中对象未被锁定的状态下,Mark Word 的 32个Bits 空间中的 25Bits 用于存储对象哈希码(HashCode),4Bits 用于存储对象分代年龄,2Bits 用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示:
存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

注意偏向锁、轻量级锁、重量级锁等都是jdk 1.6以后引入的哦。

澳门新濠3559 1

synchronized的细分

有效解决重排序问题

澳门新濠3559,整个运行过程需要注意一下几点:

3、同步方法块,锁是括号里面的对象

monitor record

每一个被锁住的对象都与一个monitor record关联。每一个线程都有一个可用monitor record列表,同时还有一个全局的可用monitor record列表。每个monitor record的结构如下所示:

Monitor Record
Owner
EntryQ
RcThis
Nest
HashCode
Candidate
  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor失败的线程。
  • RcThis:表示blocked或waiting在该monitor上的所有线程的个数。
  • Nest:用来实现重入锁的计数。
  • HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
  • Candidate:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

在 java 虚拟机中,线程一旦进入到被synchronized修饰的方法或代码块时,指定的锁对象通过某些操作将对象头中的LockWord指向monitor 的起始地址与之关联,同时monitor 中的Owner存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。

语义上来讲,synchronized主要有三种用法:

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁,调用wait()方法后会释放锁。
  2. 调用wait()方法之后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列中。
  3. notify()或notifyAll()方法调用后,等待线程不会立刻从wait()中返回,需要等该线程释放锁之后,才有机会获取锁之后从wait()返回。
  4. notify()方法将等待队列中的一个等待线程从等待队列中移动到同步队列中;notifyAll()方法则是把等待队列中的所有线程都移动到同步队列中;被移动的线程状态从WAITING变为BLOCKED。
  5. 从wait()方法返回的前提是,改线程获得了调用对象的锁。
public class SynchronizedTest {
 4     public void method1(){
 5         System.out.println("Method 1 start");
 6         try {
 7             synchronized (this) {
 8                 System.out.println("Method 1 execute");
 9                 Thread.sleep(3000);
10             }
11         } catch (InterruptedException e) {
12             e.printStackTrace();
13         }
14         System.out.println("Method 1 end");
15     }
16 
17     public void method2(){
18         System.out.println("Method 2 start");
19         try {
20             synchronized (this) {
21                 System.out.println("Method 2 execute");
22                 Thread.sleep(1000);
23             }
24         } catch (InterruptedException e) {
25             e.printStackTrace();
26         }
27         System.out.println("Method 2 end");
28     }
29 
30     public static void main(String[] args) {
31         final SynchronizedTest test = new SynchronizedTest();
32 
33         new Thread(new Runnable() {
34             @Override
35             public void run() {
36                 test.method1();
37             }
38         }).start();
39 
40         new Thread(new Runnable() {
41             @Override
42             public void run() {
43                 test.method2();
44             }
45         }).start();
46     }
47 }

偏向锁

public class TestSynchronized {
    private static Object lock = new Object();
    public static void main(String[] args) {
        method1();
        method2();
    }
    synchronized static void method1() {}
    synchronized static void method2() {}
}

偏向所是指若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式。比如上面,联系两个method都去获取了关于TestSynchronized.class的锁,就是一种偏向锁。

修饰普通方法,锁的是当前对象实例

那么,它是如何实现线程之间的互斥性和可见性?

synchronize底层原理:

轻量级锁

通过膨胀一个处于01状态的对象的对象头,或者是将已处于膨胀状态但monitor record中Owner为NULL的monitor record通过CAS置换为当前线程,可以获取锁。
轻量级锁会不断的自旋来尝试CAS获取当前的锁

修饰静态方法,锁的是当前 Class 对象(静态方法是属于类,而不是对象)

互斥性

先看一段代码:

public class SynchronizedTest { private static Object object = new Object(); public static void main(String[] args) throws Exception{ synchronized { } } public static synchronized void m() {}}

上述代码中,使用了同步代码块和同步方法,通过使用javap工具查看生成的class文件信息来分析synchronized关键字的实现细节。

 public static void main(java.lang.String[]) throws java.lang.Exception; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 // Field object:Ljava/lang/Object 3: dup 4: astore_1 5: monitorenter //监视器进入,获取锁 6: aload_1 7: monitorexit //监视器退出,释放锁 8: goto 16 11: astore_2 12: aload_1 13: monitorexit 14: aload_2 15: athrow 16: return public static synchronized void m(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=0, locals=0, args_size=0 0: return LineNumberTable: line 9: 0

从生成的class信息中,可以清楚的看到

  1. 同步代码块使用了 monitorentermonitorexit 指令实现。
  2. 同步方法中依靠方法修饰符上的 ACC_SYNCHRONIZED 实现。

无论哪种实现,本质上都是对指定对象相关联的monitor的获取,这个过程是互斥性的,也就是说同一时刻只有一个线程能够成功,其它失败的线程会被阻塞,并放入到同步队列中,进入BLOCKED状态。

我们继续深入了解一下锁的内部机制一般锁有4种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。

在进一步深入之前,我们先认识下两个概念:对象头和monitor。

什么是对象头?在hotspot虚拟机中,对象在内存的分布分为3个部分:对象头,实例数据,和对齐填充。mark word被分成两部分,lock word和标志位。Klass ptr指向Class字节码在虚拟机内部的对象表示的地址。Fields表示连续的对象实例字段。

澳门新濠3559 2对象.png

mark word 被设计为非固定的数据结构,以便在及小的空间内存储更多的信息。比如:在32位的hotspot虚拟机中:如果对象处于未被锁定的情况下。mark word 的32bit空间中有25bit存储对象的哈希码、4bit存储对象的分代年龄、2bit存储锁的标记位、1bit固定为0。而在其他的状态下(轻量级锁、重量级锁、GC标记、可偏向)下对象的存储结构为

澳门新濠3559 3Paste_Image.png

什么是monitor?monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,同时还有一个全局的可用列表,先来看monitor的内部

澳门新濠3559 4Paste_Image.png

  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor失败的线程。
  • RcThis:表示blocked或waiting在该monitor上的所有线程的个数。
  • Nest:用来实现重入锁的计数。
  • HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
  • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

那么monitor的作用是什么呢?在 java 虚拟机中,线程一旦进入到被synchronized修饰的方法或代码块时,指定的锁对象通过某些操作将对象头中的LockWord指向monitor 的起始地址与之关联,同时monitor 中的Owner存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。

接下去,我们可以深入了解下在锁各个状态下,底层是如何处理多线程之间对锁的竞争。

Java 虚拟机中的同步(Synchronization)基于进入和退出Monitor对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法表结构的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。

重量级锁

当自旋一定次数以后仍然没获取锁,那么就需要调用操作系统重量级的互斥锁了,此后,在锁被释放前所有试图获取锁的线程都将被挂起。

修饰代码块,锁的是括号里的对象

偏向锁

下述代码中,当线程访问同步方法method1时,会在对象头(SynchronizedTest.class对象的对象头)和栈帧的锁记录中存储锁偏向的线程ID,下次该线程在进入method2,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行CAS操作进行加锁和解锁(因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟)。

/** * Created by j_zhan on 2016/7/6. */public class SynchronizedTest { private static Object lock = new Object(); public static void main(String[] args) { method1(); method2(); } synchronized static void method1() {} synchronized static void method2() {}}

同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

synchronized的用处

synchronized主要有两方面的用途:

  1. 防止竞争 保证某些代码同时只有一个线程执行,防止由于竞争导致逻辑出错
  2. 内存可见性 即获取锁时,线程会将本地缓存无效,从主内存中获取最新的数据;释放锁时,会将本地缓存刷新到主内存中,保证其他线程看到最新的数据
  1. 实现原理
轻量级锁

利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG)。

线程可以通过两种方式锁住一个对象:

  1. 通过膨胀一个处于无锁状态的对象获得该对象的锁;
  2. 对象处于膨胀状态,但LockWord指向的monitor的Owner字段为NULL,则可以直接通过CAS原子指令尝试将Owner设置为自己的标识来获得锁。

获取锁(monitorenter)的大概过程:

  1. 对象处于无锁状态时(LockWord的值为hashCode等,状态位为001),线程首先从monitor列表中取得一个空闲的monitor,初始化Nest和Owner值为1和线程标识,一旦monitor准备好,通过CAS替换monitor起始地址到LockWord进行膨胀。如果存在其它线程竞争锁的情况而导致CAS失败,则回到monitorenter重新开始获取锁的过程即可。
  2. 对象已经膨胀,monitor中的Owner指向当前线程,这是重入锁的情况(reentrant),将Nest加1,不需要CAS操作,效率高。
  • 对象已经膨胀,monitor中的Owner为NULL,此时多个线程通过CAS指令试图将Owner设置为自己的标识获得锁,竞争失败的线程则进入第4种情况。
  • 对象已经膨胀,同时Owner指向别的线程,在调用操作系统的重量级的互斥锁之前自旋一定的次数,当达到一定的次数如果仍然没有获得锁,则开始准备进入阻塞状态,将rfThis值原子加1,由于在加1的过程中可能被其它线程破坏对象和monitor之间的联系,所以在加1后需要再进行一次比较确保lock word的值没有被改变,当发现被改变后则要重新进行monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

释放锁(monitorexit)的大概过程:

  1. 检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常。
  • 检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到步骤3。
  • 检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到步骤4。
  • 缩小一个对象,通过将对象的LockWord置换回原来的HashCode等值来解除和monitor之间的关联来释放锁,同时将monitor放回到线程私有的可用monitor列表。
/** * Created by j_zhan on 2016/7/6. */public class SynchronizedTest implements Runnable { private static Object lock = new Object(); public static void main(String[] args) { Thread A = new Thread(new SynchronizedTest; A.start(); Thread B = new Thread(new SynchronizedTest; B.start(); } @Override public void run() { method1(); method2(); } synchronized static void method1() {} synchronized static void method2() {}}

在JVM中,对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。如下:

synchronized和显示锁的选择

从功能上看,显示锁明显比synchronized更为丰富,可以选择获取锁超时时间等,也可以自由的选择加锁的区域和锁是否是公平锁等特征。但是,用synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。其次,作为一种内置的锁机制,可能会随着jdk的升级而得到优化,比如jdk 1.6以后的synchronized就比之前执行效率提高了很多。所以,在满足需求的情况下,建议优先使用synchronized来加锁。

2.1. 监视器锁

重量级锁

当锁处于这个状态下,其他线程试图获取锁都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程。

澳门新濠3559 5

synchronized 同步代码块的语义底层是基于对象内部的监视器锁,分别是使用 monitorenter 和 monitorexit 指令完成。其实 wait/notify 也依赖于 monitor 对象,所以其一般要在 synchronized 同步的方法或代码块内使用。monitorenter 指令在编译为字节码后插入到同步代码块的开始位置,monitorexit 指令在编译为字节码后插入到方法结束处和异常处。JVM 要保证每个 monitorenter 必须有对应的 moniorexit。

内存可见性

对java内存模型不熟悉的同学,可以参考这边文章java内存模型。

  1. 线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  2. 线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

END。我是占小狼。在魔都艰苦奋斗,白天是上班族,晚上是知识服务工作者。读完我的文章有收获,记得关注和点赞哦,如果非要打赏,我也是不会拒绝的啦!

实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

monitorenter :每个对象都有一个监视器锁,当 monitor 被某个线程占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获得 monitor 的所有权,即尝试获取对象的锁。过程如下:

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

如果 monitor 的进入数为0,则该线程进入 monitor,然后将进入数设置为1,该线程即为 monitor 的所有者;

对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

如果线程已经占有monitor,只是重新进入,则monitor的进入数 1;

Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

如果其他线程已经占用 monitor,则该线程处于阻塞状态,直至 monitor 的进入数为0,再重新尝试获得 monitor 的所有权

Monior:我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:

monitorexit :执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。执行指令时,monitor 的进入数减1,如果减1后进入数为0,则线程退出 monitor,不再是这个 monitor 的所有者,其他被这个 monitor 阻塞的线程可以尝试获取这个 monitor 的所有权。

澳门新濠3559 6

2.2. 线程状态和状态转化

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

在 HotSpot JVM 中,monitor 由 ObjectMonitor 实现,其主要数据结构如下:

 Java虚拟机对synchronize的优化:

ObjectMonitor() { _header =NULL; _count =0;//记录个数_waiters =0, _recursions =0; _object =NULL; _owner =NULL;//持有monitor的线程_WaitSet =NULL;//处于wait状态的线程,会被加入到_WaitSet_WaitSetLock =0; _Responsible =NULL; _succ =NULL; _cxq =NULL; FreeNext =NULL; _EntryList =NULL;//处于等待锁block状态的线程,会被加入到该列表_SpinFreq =0; _SpinClock =0; OwnerIsThread =0; }

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。

ObjectMonitor 中有两个队列, _WaitSet 和 _EntryList ,用来保存 ObjectWaiter 对象列表(每个等待锁的线程都会被封装成 ObjectWaiter 对象), _owner 指向持有 ObjectMonitor 对象的线程。

偏向锁

当多个线程同时访问一段同步代码时,首先会进入 _EntryList,等待锁处于阻塞状态。

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

当线程获取到对象的 monitor 后进入 The Owner 区域,并把 ObjectMonitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 count 加1。

轻量级锁

若线程调用 wait() 方法,将释放当前持有的 monitor,_owner 变量恢复为 null,count 减1,同时该线程进入 _WaitSet 集合中等待被唤醒,处于 waiting 状态。

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

若当前线程执行完毕,将释放 monitor 并复位变量的值,以便其他线程进入获取 monitor。

自旋锁

过程如下图所示:

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

澳门新濠3559 7

锁消除

  1. 锁优化

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

在 JDK1.6 之后,出现了各种锁优化技术,如轻量级锁、偏向锁、适应性自旋、锁粗化、锁消除等,这些技术都是为了在线程间更高效的解决竞争问题,从而提升程序的执行效率。

/**
 * Created by zejian on 2017/6/4.
 * Blog : http://blog.csdn.net/javazejian 
 * 消除StringBuffer同步锁
 */
public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i  ) {
            rmsync.add("abc", "123");
        }
    }

}

通过引入轻量级锁和偏向锁来减少重量级锁的使用。锁的状态总共分四种:无锁状态、偏向锁、轻量级锁和重量级锁。锁随着竞争情况可以升级,但锁升级后不能降级,意味着不能从轻量级锁状态降级为偏向锁状态,也不能从重量级锁状态降级为轻量级锁状态。

synchronize的可重入性:

无锁状态 → 偏向锁状态 → 轻量级锁 → 重量级锁

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:

3.1. 对象头

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j  ){

            //this,当前实例对象锁
            synchronized(this){
                i  ;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j  ;
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

要理解轻量级锁和偏向锁的运行机制,还要从了解对象头(Object Header)开始。对象头分为两部分:

  正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

1、 Mark Word :存储对象自身的运行时数据,如:Hash Code,GC 分代年龄、锁信息。这部分数据在32位和64位的 JVM 中分别为 32bit 和 64bit。考虑空间效率,Mark Word 被设计为非固定的数据结构,以便在极小的空间内存储尽量多的信息,32bit的 Mark Word 如下图所示:

线程中断:正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法

澳门新濠3559 8

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

2、存储指向方法区对象类型数据的指针,如果是数组对象的话,额外会存储数组的长度

 等待唤醒机制与synchronize:所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

3.2. 重量级锁

 

monitor 监视器锁本质上是依赖操作系统的 Mutex Lock 互斥量 来实现的,我们一般称之为重量级锁。因为 OS 实现线程间的切换需要从用户态转换到核心态,这个转换过程成本较高,耗时相对较长,因此 synchronized 效率会比较低。

本篇参考资料:

重量级锁的锁标志位为'10',指针指向的是 monitor 对象的起始地址,关于 monitor 的实现原理上文已经描述了。

3.3. 轻量级锁

轻量级锁是相对基于OS的互斥量实现的重量级锁而言的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用OS的互斥量而带来的性能消耗。

轻量级锁提升性能的经验依据是:对于绝大部分锁,在整个同步周期内都是不存在竞争的。如果没有竞争,轻量级锁就可以使用 CAS 操作避免互斥量的开销,从而提升效率。

轻量级锁的加锁过程:

1、线程在进入到同步代码块的时候,JVM 会先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象当前 Mark Word 的拷贝(官方称为 Displaced Mark Word),owner 指针指向对象的 Mark Word。此时堆栈与对象头的状态如图所示:

澳门新濠3559 9

2、JVM 使用 CAS 操作尝试将对象头中的 Mark Word 更新为指向 Lock Record 的指针。如果更新成功,则执行步骤3;更新失败,则执行步骤4

3、如果更新成功,那么这个线程就拥有了该对象的锁,对象的 Mark Word 的锁状态为轻量级锁(标志位转变为'00')。此时线程堆栈与对象头的状态如图所示:

澳门新濠3559 10

4、如果更新失败,JVM 首先检查对象的 Mark Word 是否指向当前线程的栈帧

如果是,就说明当前线程已经拥有了该对象的锁,那就可以直接进入同步代码块继续执行

如果不是,就说明这个锁对象已经被其他的线程抢占了,当前线程会尝试 自旋一定次数来获取锁 。如果自旋一定次数 CAS 操作仍没有成功,那么轻量级锁就要升级为重量级锁(锁的标志位转变为'10'),Mark Word 中存储的就是指向重量级锁的指针,后面等待锁的线程也就进入阻塞状态

轻量级锁的解锁过程:

1、通过 CAS 操作用线程中复制的 Displaced Mark Word 中的数据替换对象当前的 Mark Word

2、如果替换成功,整个同步过程就完成了

3、如果替换失败,说明有其他线程尝试过获取该锁,那就在释放锁的同时,唤醒被挂起的线程

3.4. 偏向锁

轻量级锁是在无多线程竞争的情况下,使用 CAS 操作去消除互斥量;偏向锁是在无多线程竞争的情况下,将这个同步都消除掉。

偏向锁提升性能的经验依据是:对于绝大部分锁,在整个同步周期内不仅不存在竞争,而且总由同一线程多次获得。偏向锁会偏向第一个获得它的线程,如果接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程不需要再进行同步。这使得线程获取锁的代价更低。

偏向锁的获取过程:

1、线程执行同步块,锁对象第一次被获取的时候,JVM 会将锁对象的 Mark Word 中的锁状态设置为偏向锁(锁标志位为'01',是否偏向的标志位为'1'),同时通过 CAS 操作在 Mark Word 中记录获取到这个锁的线程的 ThreadID

2、如果 CAS 操作成功。持有偏向锁的线程每次进入和退出同步块时,只需测试一下 Mark Word 里是否存储着当前线程的 ThreadID。如果是,则表示线程已经获得了锁,而不需要额外花费 CAS 操作加锁和解锁

3、如果不是,则通过CAS操作竞争锁,竞争成功,则将 Mark Word 的 ThreadID 替换为当前线程的 ThreadID

偏向锁的释放过程:

1、当一个线程已经持有偏向锁,而另外一个线程尝试竞争偏向锁时,CAS 替换 ThreadID 操作失败,则开始撤销偏向锁。偏向锁的撤销,需要等待原持有偏向锁的线程到达全局安全点(在这个时间点上没有字节码正在执行),暂停该线程,并检查其状态

2、如果原持有偏向锁的线程不处于活动状态或已退出同步代码块,则该线程释放锁。将对象头设置为无锁状态(锁标志位为'01',是否偏向标志位为'0')

3、如果原持有偏向锁的线程未退出同步代码块,则升级为轻量级锁(锁标志位为'00')

3.5. 总结

偏向锁、轻量级锁、重量级锁之间的状态转换如图所示(概括上文描述的锁获取和释放的内容):

澳门新濠3559 11

下面是这几种锁的比较:

澳门新濠3559 12

3.6. 其他优化

1、适应性自旋

自旋锁:互斥同步时,挂起和恢复线程都需要切换到内核态完成,这对性能并发带来了不少的压力。同时在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段较短的时间而去挂起和恢复线程并不值得。那么如果有多个线程同时并行执行,可以让后面请求锁的线程通过自旋(CPU忙循环执行空指令)的方式稍等一会儿,看看持有锁的线程是否会很快的释放锁,这样就不需要放弃 CPU 的执行时间了 。

适应性自旋:在轻量级锁获取过程中,线程执行 CAS 操作失败时,需要通过自旋来获取重量级锁。如果锁被占用的时间比较短,那么自旋等待的效果就会比较好,而如果锁占用的时间很长,自旋的线程则会白白浪费 CPU 资源。解决这个问题的最简答的办法就是:指定自旋的次数,如果在限定次数内还没获取到锁,就按传统的方式挂起线程进入阻塞状态。JDK1.6 之后引入了自适应性自旋的方式,如果在同一锁对象上,一线程自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么 JVM 会认为这次自旋也有可能再次成功获得锁,进而允许自旋等待相对更长的时间。另一方面,如果某个锁自旋很少成功获得,那么以后要获得这个锁时将省略自旋过程,以避免浪费 CPU。

2、锁消除

锁消除就是编译器运行时,对一些被检测到不可能存在共享数据竞争的锁进行消除。如果判断一段代码中,堆上的数据不会逃逸出去从而被其他线程访问到,则可以把他们当做栈上的数据对待,认为它们是线程私有的,不必要加锁。

publicStringconcatString(Strings1,Strings2,Strings3) {StringBuffersb =newStringBuffer(); sb.append; sb.append; sb.append;returnsb.toString();}

在 StringBuffer.append() 方法中有一个同步代码块,锁就是sb对象,但 sb 的所有引用不会逃逸到 concatString() 方法外部,其他线程无法访问它。因此这里有锁,但是在即时编译之后,会被安全的消除掉,忽略掉同步而直接执行了。

3、锁粗化

锁粗化就是 J VM 检测到一串零碎的操作都对同一个对象加锁,则会把加锁同步的范围粗化到整个操作序列的外部。以上述 concatString() 方法为例,内部的 StringBuffer.append() 每次都会加锁,将会锁粗化,在第一次 append() 前至 最后一个 append() 后只需要加一次锁就可以了。

文末福利:

想要了解更多并发编程知识点的,可以关注我一下,我后续也会整理更多关于并发编程这一块的知识点分享出来,另外顺便给大家推荐一个交流学习群:650385180,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化以及并发编程这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多,以下学习资源都在群的共享区。

澳门新濠3559 13

编辑:编程 本文来源:线程B调用对象lock的notifyAll()方法,AQS还提供了这

关键词: 澳门新濠3559