Java synchronized

synchronized是java提供线程间同步的重要机制
保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果

java内存模型:
image

先通过一个生产者消费者例子来了解如何使用synchronized

package com.example.demo;

public class SyncTest {
    static Object obj = new Object();
    static boolean flag = false;
    public static void main(String[] args) throws InterruptedException {
        Thread c1 = new Thread(new Consumer());
        Thread p1 = new Thread(new Producer());
        c1.start();
        Thread.sleep(1000);
        p1.start();

        c1.join();
        p1.join();
    }

    static class Consumer implements Runnable {

        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("Consumer executing...");
                while (!flag) {
                    System.out.println("no product, waiting");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Consumer waiting end....");
                }
                flag = false;
                System.out.println("consuming.....");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("exit consumer.....");
            }
        }
    }

    static class Producer implements Runnable {

        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("Producer executing......");
                try {
                    System.out.println("Producing.....");
                    Thread.sleep(2000);
                    flag = true;
                    obj.notify();
                    Thread.sleep(2000);
                    System.out.println("Producer exit......");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

当然上面是代码块的方式,另一种方式是用在方法上。

  • synchronized锁的是谁?
  • 锁是如何实现的?
  • wait()和notify()发生了什么?

synchronized锁的是谁?

synchronized有两种使用方式,分别来进行讨论

  1. 代码块
    我们知道,使用代码块的时候synchronized后面要传入一个对象(Object)或者类(obj.class),所以结论就是传的是谁锁的就是谁。
  2. 方法
    方法又分为静态方法和非静态方法
  • 静态方法:锁的是方法所在的类
  • 非静态方法:锁的是调用该方法的对象

锁是如何实现的?

关键词:Monitor对象,monitorenter和monitorexit指令,markword
monitor:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权。
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。
再具体一点,Monitor是由ObjectMonitor实现的,其源码是用C++语言编写的,

// src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁的计数器,获取锁时count数值加1,释放锁时count值减1
    _waiters      = 0,  //等待线程数
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;  // 存储Monitor对象
    _owner        = NULL;  // 持有当前线程的owner
    _WaitSet      = NULL;  // wait状态的线程列表
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 阻塞在EntryList上的单向线程列表
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁状态block状态的线程列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

image

获取Monitor和释放Monitor的流程如下:

当多个线程同时访问同步代码块时,首先会找到对象或类对应的Monitor对象,进入到EntryList中,然后通过CAS的方式尝试将Monitor中的owner字段设置为当前线程,如果成功count加1;如果之前的owner的值就是指向当前线程的,owner和recursions都需要加1。如果CAS尝试获取锁失败,则进入到EntryList中。

当获取锁的线程调用wait()方法(只有获取到锁才能用wait方法),则会将owner设置为null,同时count减1,recursions减1,当前线程加入到WaitSet中,等待被唤醒。

当前线程执行完同步代码块时,则会释放锁,count减1,recursions减1。当recursions的值为0时,说明线程已经释放了锁。

wait(), notify()

Jdk1.6为什么要对synchronized进行优化?

  因为Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,而Monitor是依靠底层操作系统的Mutex Lock来实现的,操作系统实现线程之间的切换需要从用户态转换到内核态,这个切换成本比较高,对性能影响较大。

锁升级

以32位机器为例
当我们创建一个对象之后,对象头部的markword中有25bit hashcode,4bit分代年龄,1bit偏向锁标记位,2bit锁标记位

Q: 一个对象new出来之后,markword里面的hashcode位置有没有真正存放对象的hashcode?

A: 如果仅仅new了一个对象,并没有隐式或显示的调用Object的hashcode()方法,那么对象hashcode并不存在,即存在的前提是调用父类Object的hashcode()方法,否则这25个bit就是0。
显示调用:比如在构造函数里面执行super.hashcode()
隐式调用:new对象之后马上添加到HashMap/HashSet里,因为hashmap的put方法的第一个参数就是hash值,来源就是调用了Object的hashcode方法。

Q: 如果类重写了hashcode方法,并且在构造函数中调用hashcod方法,那么markword里面hashcode的25个bit是否存在?

A:不会,只有调用Object的hashcode方法才行

锁标志位

无锁:偏向锁标志位0+锁标志位01
偏向锁:偏向锁标志位1+锁标志位01

无锁升级为偏向锁

前提条件:markword里面没有存放对象的hashcode,如果有就不能加偏向锁(不然hashcode会覆盖)
升级之后,markword里面的25bit hashcode部分变成了23bit线程id+2bit epoch,且偏向锁标记位从0变为1

偏向锁升级为轻量级锁

必要条件:发生竞争
但不是充分条件
eg
执行完毕的争抢:
线程A获取到对象的偏向锁,线程B来访问看到当前对象已经有偏向,检测一下线程A的存活状态,如果A已经在临界区之外了(不在执行同步代码),B直接将对象置为无锁状态,再去抢锁,如果B抢到了,那么就获取了对象的偏向锁,偏向B;如果B争抢失败(比如C进入竞争且成功),那么C就获取了对象的偏向锁。

偏向锁

轻量级锁

重量级锁

posted @ 2023-01-05 14:15  antidogmatist  阅读(19)  评论(0编辑  收藏  举报