Synchronized 解析

Synchronized

1、可用范围
  • public void demo() {
        synchronized (synchronizedDemo.class) {
                
        }
    }
    
    public synchronized static void demo() {
        
    }
    
  • 方法

    public synchronized void demo() {
            
     }
    
    public void demo() {
            synchronized (this) {
                
            }
    }
    
2、Markwork锁信息存储

  1. 为什么每个 java 对象都可以作为锁对象

    Java 中的对象,都是派生自 Object 类,每个 Java Object 在 JVM 源码中都有一个 native 对象与之对应

  2. synchronized (lock) ,关于锁的操作,都是对 lock 这个对象的生命周期进行操作

  3. Mark word,记录了对象和锁的相关信息

    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 // 偏向锁的时间戳
      };
    
4、锁的升级

经过 3. 分析,发现在内核态和用户态之间来回切换,对性能有所影响,所以在 JDK 1.6 之后进行了更新优化

4.1 锁的升级步骤

偏向锁 -> 轻量级锁 -> 重量级锁

其中,偏向锁和轻量级锁,可以说是一种无锁状态,其中轻量级锁也可以说是自旋锁

4.2 偏向锁
4.2.1 大体理论

偏向锁的大体理论就是,当一个线程访问同步代码的时候,会在对象头里存储自己当前的线程ID,当要获取锁的时候和锁对象的 Mark word 信息进行对比,若一致则直接进入,执行同步,当后续这个线程在进入的时候不会进行加锁和解锁操作,若对比时不一致,代表此时有线程和它进行竞争,这时会对已经获取到偏向锁的线程进行偏向锁的撤销,并将其升级为轻量级锁

偏向锁一般只适用于一个线程访问的情况

4.2.1 使用前提
  • biased_lock_bits 偏向锁标记为 1

  • ThreadId 为空

    表示为可偏向状态,信息存储在 Mark word 中

4.2.3 执行流程
  • 获取对象的 Mark word 信息,确定当前为可偏向状态
  • 如果是可偏向状态,通过 CAS 操作,将线程ID等信息,写入到锁对象的 Mark word 中
    • 如果 CAS 写入成功,则当前锁对象的 Mark word 存储的 ThreadID 就是当前线程的 ThreaId ,可以执行同步代码块中的内容
    • 如果 CAS 写入失败,则表示当前锁对象已经有线程获取到了,也就是现在存在了锁竞争的情况,这时会将先前已经获取锁的对象的偏向锁进行撤销,并将其升级为轻量级锁(这些操作会等待到一个全局安全点,也就是没有执行字节码的时候进行操作)
  • 如果是已偏向状态,则直接对比 ThreadId 信息即可
    • 相等,直接进行执行操作
    • 不相等,则表明竞争,和上述相同的操作

4.2.4 设置关闭

-XX:-UseBiasedLocking=false 关闭

4.3 轻量级锁
4.3.1 大体理论

在偏向锁阶段出现竞争的话,会将锁信息撤销,并将 Mark Work 中的 ThreadID 信息置为空,将锁升级为轻量级锁,轻量级锁也可以说是自旋锁,它适用于同步代码块中的代码执行速度很快的情况下,在ThreadA已经1获得对象锁开始执行同步代码块的时候,ThreadB获取锁失败,开始进行自旋尝试获得锁,直到获取成功,但这样一直自旋也会带来性能上的开销,所以轻量级锁分为自旋锁和自适应自旋锁

  • 自旋锁:一直自旋,直到获取成功

    for(;;){
        // TODO 获得锁
    }
    
  • 自适应自旋锁:由系统自己判断,若之前对这个锁对象获取成功率都比较高,则认为这一次会获取成功,会继续进行自旋,若之前成功率都很低而且需要很长时间才可以获取到,系统会降低自旋的次数,甚至不进行自旋,直接将线程挂起阻塞

4.3.2 执行流程
  • 首先会先在栈中开辟出来一块空间,存储Lock 的相关信息 (Lock Record)

  • 将Mark Word 中的信息,复制到 Lock Record 当中

  • 将设置好的 Lock Record 赋值到 Mark Word ,并将Lock Record 的 owner 设置为当前锁对象

    • 成功:表示线程已经获取到锁,并将 Mark word 的锁标记设置为 00
    • 失败:若更新失败会首先检查 Mark Word 是否指向当前线程的栈帧,若指向则继续执行,若不是,则表明存在竞争,会膨胀为重量级锁,锁标记为10

  • 执行完毕后,要通过 CAS 将线程栈帧中的 Lock Record 信息,替换回 Mark Word 中,若失败,表示有线程在竞争锁,锁升级为膨胀锁

5、重量级锁
  1. // 获取到 ObjectMonitor 对象,通过该对象进行加锁
      ObjectMonitor* monitor() const {
        assert(has_monitor(), "check");
        // Use xor instead of &~ to provide one extra tag-bit check.
        return (ObjectMonitor*) (value() ^ monitor_value);
      }
    

    我们通过 javap -v 类名.class 可以看到

monitorenter 指令尝试获取到对象锁

monitorexit 指令对锁进行释放

第二个 monitorexit 在抛出异常时,执行

基本流程就是,每一个 java 对象都会与一个 monitor 监视器对象进行关联,线程A 通过执行 monitorenter 指令,尝试获取 ObjectMonitor 监视器对象,若获取成功,则执行同步代码块,执行完毕后执行 monitorexit 指令,进行 监视器对象的释放,若没有获得 ObjectMonitor 对象,则会将其添加到一个同步队列中,等待之前的线程执行完毕后,释放ObjectMonitro 对象,同时对同步队列进行唤醒,后者再次尝试执行 monitorenter 执行,对监视器对象进行获取。

而加锁解锁的操作,是基于底层操作系统去实现的,像 Linux 是 mutexLock 一个互斥锁,线程在被堵塞后就会进入 内核态等待系统调度,这个就会导致在系统态和内核态之间来回切换,影响效率。

6、wait / notify 线程通信
  • wait:表示放弃当前线程持有的 CPU 资源,并释放锁权限,进入等待状态
  • notify:对进行等待状态的线程进行唤醒,唤醒之后立即获得锁权限
  • notify / notifyAll:前者唤醒一个,立马获得锁权限,后者唤醒全部,竞争锁资源

需要注意的是:三个方法都必须在 synchronized 同步关键字锁限定的作用域中执行 ,否则会抛出java.lang.IllegalMonitorStateException

7、join 操作
  • join 操作可以用来实现线程的顺序执行
    ThreadA -> ThreadB -> ThreadC
    可以通过 ThreadA.join(),ThreadB().join() 来实现

  • join 的底层也是调用的 wait 方法

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) { // 如果 线程还存活,调用 wait 方法
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

当线程生命周期结束的时候

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
。。。 。。。
  java_lang_Thread::set_thread(threadObj(), NULL);
  // 会调用 notify_all 进行唤醒
  lock.notify_all(thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值