likes
comments
collection
share

Synchronized原理分析

作者站长头像
站长
· 阅读数 10

我们先看一个例子

public class SynchronizedDemo  {
    static Integer count=0;
    public static void incr(){
        count++;
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()->SynchronizedDemo.incr()).start();
        }
        Thread.sleep(2000);
        System.out.println("result:"+count);
    }
}

最后的输出我们可以看一下 Synchronized原理分析 一定是一个小于等于1000的值,这是一个比较经典的代码场景了,小于1000的原因也简单就是可见性和原子性的原因 可见性:线程看不到变量的最新值,因为它还没有被另一个线程写回主内存,这个问题被称为“可见性”问题。一个线程的更新对其他线程是不可见的。 原子性: 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行原子性就是指该操作是不可再分的。 count++看着像是一句代码好像也不能够再怎么拆分步骤,但其实底层是分为好几步的,我们可以使用 javap -v xx.class指令查看对应字节码如下: Synchronized原理分析 发现先是访问这个值然后声明一个常量压入到栈中,然后再加加操作最后就是赋值给静态变量(大概的流程其实就是需要在内存中声明一个变量把原来变量的值赋值给新变量对这个新值进行++最后赋值给初始变量),所以这里就会出现原子性问题如下图所示: Synchronized原理分析

Synchronized的使用

针对上面的问题呢解决方法其实也比较简单就是加锁,加锁之后就能实现多个线程不能同时执行同一个任务修改后的代码如下:

public class SynchronizedDemo  {
    static Integer count=0;
    public static void incr(){
        synchronized (SynchronizedDemo.class) {
            count++;
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()->SynchronizedDemo.incr()).start();
        }
        Thread.sleep(2000);
        System.out.println("result:"+count);
    }
}

可以发现打印结果一定是1000 Synchronized原理分析 当然Synchronized的使用方法不止这一种还有如下三种方式:

  1. 使用对象示例的锁
    public class SynchronizedDemo  {
            static Integer count=0;
            static Object obj = new Object();
            public static void incr(){
                synchronized (obj) {
                    count++;
                }
            }
            public static void main(String[] args) throws IOException, InterruptedException {
                for(int i=0;i<1000;i++){
                    new Thread(()->SynchronizedDemo.incr()).start();
                }
                Thread.sleep(2000);
                System.out.println("result:"+count);
            }
        }
    
  2. Synchronized修饰在类方法上
    public class SynchronizedDemo  {
             static Integer count=0;
             public synchronized static void incr(){
                 count++;
             }
             public static void main(String[] args) throws IOException, InterruptedException {
                 for(int i=0;i<1000;i++){
                     new Thread(()->SynchronizedDemo.incr()).start();
                 }
                 Thread.sleep(2000);
                 System.out.println("result:"+count);
             }
         }
    
  3. 修改在实例方法上
    public class SynchronizedDemo  {
              static Integer count=0;
              public static synchronized void incr(){
                  count++;
              }
              public static void main(String[] args) throws IOException, InterruptedException {
                  SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
                  for(int i=0;i<1000;i++){
                      new Thread(()->synchronizedDemo.incr()).start();
                  }
                  Thread.sleep(2000);
                  System.out.println("result:"+count);
              }
          }
    

Synchronized的作用范围

通过上文我们可以知道Synchronized使用方式有四种

使用synchronized包装一个代码块但是需要传入一个类对象

这个时候锁的作用是这个代码块也就是进入到这个代码块是需要当前对象的锁的,而这个对象是程序开始加载会回放到内存中的,知道这个程序的解除才会释放的,所以可以说在这一次程序运行的过程中所有线程访问这个同步代码块都是需要获得对象的锁的。

使用synchronized包装一个代码块但是需要传入一个实例对象

这个锁同样也是作用在代码块但是对实例加锁,所以它的作用范围是同一个实例,如果锁传入的实例都是不同的那么加锁也是没有用的

使用synchronized修饰静态方法

这个就是作用整个方法,进入到方法需要获取当前对象的锁,实则就是synchronized(当前对象){}

使用synchronized修饰实例方法

这也是作用整个方法,进入到方法需要获取当前实例的锁,实则就是synchronized(this){}

Synchronized的存储

首先我们可以看一下对象的存储结构如下图所示 Synchronized原理分析 这里我们可以通过查看jvm源码进行验证:

instanceOopDesc源码

首先是java类的源码:

// An instanceOop is an instance of a Java Class

// Evaluating "new HashTable()" will create an instanceOop.
class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

从这个备注就可以知道instanceOop就是一个java class 这里还看不出什么东西此时我们继续看父类oopDesc

oopDesc源码

class oopDesc {
  friend class VMStructs;
 private:
  //这个就是对象头
  volatile markOop  _mark;
  //这里就是类对象的元数据信息
  //需要注意的是这个是个共同体也就是压缩指针和不压缩的只会有一个存在,具体是否使用压缩指针需要开启压缩指令就行
  union _metadata {
    //实例对应的 Klass (实例对应的类)的指针
    Klass*      _klass;
    //压缩指针 
    narrowKlass _compressed_klass;
  } _metadata;

  // Fast access to barrier set.  Must be initialized.
  static BarrierSet* _bs;

 public:
  markOop  mark() const         { return _mark; }
  markOop* mark_addr() const    { return (markOop*) &_mark; }

  void set_mark(volatile markOop m)      { _mark = m;   }

  void    release_set_mark(markOop m);
  ......
  下面还有很多这里就不多赘述了
}

这里就可以发现对象的大致结构就和上图一样主要分为三部分对象头、实例数据、对齐填充 然后我们再看一下对象头的源码markOop:

markOop源码

#ifndef SHARE_VM_OOPS_MARKOOP_HPP
#define SHARE_VM_OOPS_MARKOOP_HPP

#include "oops/oop.hpp"

// The markOop describes the header of an object.
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
//  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)
//
//  - hash contains the identity hash value: largest value is
//    31 bits, see os::random().  Also, 64-bit vm's require
//    a hash value no bigger than 32 bits because they will not
//    properly generate a mask larger than that: see library_call.cpp
//    and c1_CodePatterns_sparc.cpp.
//
//  - the biased lock pattern is used to bias a lock toward a given
//    thread. When this pattern is set in the low three bits, the lock
//    is either biased toward a given thread or "anonymously" biased,
//    indicating that it is possible for it to be biased. When the
//    lock is biased toward a given thread, locking and unlocking can
//    be performed by that thread without using atomic operations.
//    When a lock's bias is revoked, it reverts back to the normal
//    locking scheme described below.
//
//    Note that we are overloading the meaning of the "unlocked" state
//    of the header. Because we steal a bit from the age we can
//    guarantee that the bias pattern will never be seen for a truly
//    unlocked object.
//
//    Note also that the biased state contains the age bits normally
//    contained in the object header. Large increases in scavenge
//    times were seen when these bits were absent and an arbitrary age
//    assigned to all biased objects, because they tended to consume a
//    significant fraction of the eden semispaces and were not
//    promoted promptly, causing an increase in the amount of copying
//    performed. The runtime system aligns all JavaThread* pointers to
//    a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
//    to make room for the age bits & the epoch bits (used in support of
//    biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
//
//    [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
//                                               not valid at any other time
//
//    We assume that stack/thread pointers have the lowest two bits cleared.

class BasicLock;
class ObjectMonitor;
class JavaThread;

class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }

 public:
  // Constants
  enum { age_bits                 = 4,
         lock_bits                = 2,
         biased_lock_bits         = 1,
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2
  };

  // The biased locking code currently requires that the age bits be
  // contiguous to the lock bits.
  enum { lock_shift               = 0,
         biased_lock_shift        = lock_bits,
         age_shift                = lock_bits + biased_lock_bits,
         cms_shift                = age_shift + age_bits,
         hash_shift               = cms_shift + cms_bits,
         epoch_shift              = hash_shift
  };

  enum { lock_mask                = right_n_bits(lock_bits),
         lock_mask_in_place       = lock_mask << lock_shift,
         biased_lock_mask         = right_n_bits(lock_bits + biased_lock_bits),
         biased_lock_mask_in_place= biased_lock_mask << lock_shift,
         biased_lock_bit_in_place = 1 << biased_lock_shift,
         age_mask                 = right_n_bits(age_bits),
         age_mask_in_place        = age_mask << age_shift,
         epoch_mask               = right_n_bits(epoch_bits),
         epoch_mask_in_place      = epoch_mask << epoch_shift,
         cms_mask                 = right_n_bits(cms_bits),
         cms_mask_in_place        = cms_mask << cms_shift
#ifndef _WIN64
         ,hash_mask               = right_n_bits(hash_bits),
         hash_mask_in_place       = (address_word)hash_mask << hash_shift
#endif
  };
  ......
  bool has_monitor() const {
    return ((value() & monitor_value) != 0);
  }
  ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
  }
  ......
};

#endif // SHARE_VM_OOPS_MARKOOP_HPP

可以看到有很多注释,大致内容就是告诉你怎么读取对应的信息,比如是否有锁、什么锁总结一下如下图所示: Synchronized原理分析

打印类结构

这里我们可以使用jol-core查看

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

使用方式如下图所示:

public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    synchronized (synchronizedDemo) {
        System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
    }
}

Synchronized原理分析 把这个十六进制转换成二进制 Synchronized原理分析 发现后三位是000对应上文也就是一个轻量级锁

再看下面的代码:

SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(() -> {
    synchronized (synchronizedDemo) {
        System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}).start();
new Thread(() -> {
    synchronized (synchronizedDemo) {
        System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
    }
}).start();

Synchronized原理分析 此时就是一个重量级锁

Synchronized的升级

偏向锁

在大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设 计了偏向锁。偏向锁,顾名思义,就是锁偏向于某个线程。 (类似于乐观锁) 当—个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的1D,后续这个线程进入和退出这 段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线 程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了 在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。(偏向锁的目的是消除数据在无竞争情 况下的同步块,进一步提高程序的运行性能。)

需要注意的是java 7之后默认是关闭偏向锁的,可以通过参数打开如下: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -client -Xmx1024m -Xms1024m

轻量级锁

其实就是一个自旋锁会不断的循环访问锁是否被释放,如果偏向锁被关闭或者当前偏向锁已经已经被其他线程获取,那么这个时候如果有线程去抢占同步锁时,锁会升级到轻量级锁。

重量级锁

其实就是由系统实现的锁,在多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程并且在目标锁被释放的时候唤醒这些线程,至于阻塞和唤醒都是直接调用系统方法(这个是最耗性能的)

每一个AVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行 一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应 的monitor。 monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他 被阻塞的线程可以尝试去获得这个监视器 monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这 个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能 任意线程对Object (Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失 败,线程进入同步队列,线程状态变为BLOCKED。当访问Objec 了锁的线程)释放了 锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试

锁升级流程

Synchronized原理分析

线程的通信(wait/notify)

在java中提供了wait/notify这个机制,用来实现条件等待和唤醒。比如以抢占锁为例,假设线程A持有锁,线程B再去抢占锁时,它需要等待持有锁的线程释放之后才能抢占,那线程B怎么知道线程A什么时候释放呢?这个时候就可以采用通信机制。

这里需要注意的是notify之后并不是直接让对应的线程获取锁而是加入到竞争队列中重新竞争锁 Synchronized原理分析