Synchronized详解、同步互斥自旋锁分析及MonitorJVM底层实现原理

开发 后端
Synchronized代码块锁定多个成员对象 和this对象 此时成员对象和this对象之间是互不影响的,只有当前代码块锁定的是同一个对象时才会等待。

状态对象

如果一个对象有被修改的成员变量 被称为有状态的对象相反如果没有可被修改的成员变量 称为无状态的对象。

示例:

public class MyThreadTest {

    public static void main(String[] args) {
        Runnable r = new MyThread();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

class MyThread implements Runnable {
    /**
     * 如果一个对象有被修改的成员变量 被称为有状态的对象
     * 相反如果没有可被修改的成员变量 称为无状态的对象
     *
     * 由于两个线程同时访问有状态的对象 当一个线程x++完
     * 此时另外一个线程又将x变成0 此时就会输出两次0
     */
    int x;

    @SneakyThrows
    @Override
    public void run() {
        x = 0;
        while (true) {
            System.out.println("result: " + x++);

            Thread.sleep((long) Math.random() * 1000);
            if (x == 30) {
                break;
            }
        }
    }
}
/*
result: 0
result: 0
result: 1
result: 2
result: 3
result: 4
result: 5
result: 6
result: 7
result: 8
result: 9
....
 */

示例2:

/**
 * 类锁和对象锁互相不干扰 线程可以获取对象锁不影响其他的线程获取类锁 反之亦然
 */
public class MyThreadTest2 {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        MyClass myClass2 = new MyClass();

        Thread t1 = new Thread1(myClass);
        Thread t2 = new Thread2(myClass);

        t1.start();

        try {
            System.out.println("name: "+Thread.currentThread().getName());
            Thread.sleep(700);//睡眠main线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t2.start();

    }
}

class MyClass {

    public synchronized void hello() {

        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello");

    }

    public synchronized void world() {
        System.out.println("world");
    }
}

class Thread1 extends Thread {
    private MyClass myClass;

    public Thread1(MyClass myClass) {
        this.myClass = myClass;
    }

    @Override
    public void run() {
        myClass.hello();
    }
}

class Thread2 extends Thread {
    private MyClass myClass;

    public Thread2(MyClass myClass) {
        this.myClass = myClass;
    }

    @Override
    public void run() {
        myClass.world();
    }
}

结论:每个实例对象都有一个唯一的Monitor(锁)。

synchronized修饰代码块

当我们用synchronized修饰代码块时字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。

/**
 * 当我们使用synchronized关键字来修饰代码块时,
 * 字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。
 * monitorenter跟monitorexit 是一对多的关系
 *
 * 当线程进入到monitorenter指令后,线程将会持有Monitor对象,执行monitorexit指令,线程将会释放Monitor对象
 */
public class MyTest1 {

    private Object object = new Object();

    public void method() {
        int i = 1;
        /*
         此处的不是只能锁object 所有的都可以
          此处synchronized 它会尝试去获取该对象的锁 有执行无则阻塞
         */
        synchronized (object) {
            System.out.println("hello world!");
            //当应用主动抛出异常此时字节码 会直接执行并且直接执行monitorexit解锁
            throw new RuntimeException();
        }
    }

    public void method2() {
        synchronized (object) {
            System.out.println("welcome");
        }
    }
}

synchronized代码块修饰多个成员对象和this对象

结论:synchronized代码块锁定多个成员对象 和this对象 此时成员对象和this对象之间是互不影响的,只有当前代码块锁定的是同一个对象时才会等待。

注意: 成员属性对象加锁,若该类属于单例,那么该属性值全局并发修改始终以最新的值为主(volatile 关键字就是用来辅助线程读取最新的值 ),例如 A B C线程 线程修改(每次+1)某类的 sum =0 属性值 A最先修改为0+1 = 1 后续B接着修改就会是1+1 =2 以此类推 如果想让每个线程访问都是默认值0 需要使用Spring 的scope 的protype作用域 或者ThreadLocal 或者将其放置在方法中。

/**
 * synchronized代码块 锁定多个成员对象 和this对象  此时成员对象和this对象互不影响 
 */
public class Test {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        Thread t1 = new Thread1(myClass);
        Thread t2 = new Thread2(myClass);
        Thread t3 = new Thread3(myClass);

        t3.start();//5000
        t1.start();//4000

        try {
            System.out.println("name: " + Thread.currentThread().getName());
            Thread.sleep(700);//睡眠main线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t2.start(); //t1 t2 t3
    }
}

class MyClass {
    final Object o1 = new Object();
    final Object o2 = new Object();

    public void hello() {
        //只锁o1的对象  由于o1和o2 是不同的对象两个方法互不影响
        synchronized (o1) {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("hello");

    }

    public void world() {
        //只锁o2的对象
        synchronized (o2) {
            System.out.println("world");
        }
    }

    public synchronized void test() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test");
    }
}

class Thread1 extends Thread {
    private MyClass myClass;

    public Thread1(MyClass myClass) {
        this.myClass = myClass;
    }

    @Override
    public void run() {
        myClass.hello();
    }
}

class Thread2 extends Thread {
    private MyClass myClass;

    public Thread2(MyClass myClass) {
        this.myClass = myClass;
    }

    @Override
    public void run() {
        myClass.world();
    }

}

class Thread3 extends Thread {
    private MyClass myClass;

    public Thread3(MyClass myClass) {
        this.myClass = myClass;
    }

    @Override
    public void run() {
        myClass.test();
    }

}

输出:

name: main
world
hello
test
/*
 0 iconst_1
 1 istore_1
 2 aload_0
 3 getfield #3 <com/example/demo/com/concurrecy/concurrency3/MyTest1.object>
 6 dup
 7 astore_2
 8 monitorenter
 9 getstatic #4 <java/lang/System.out>
12 ldc #5 <hello world!>
14 invokevirtual #6 <java/io/PrintStream.println>
17 aload_2
18 monitorexit
19 goto 27 (+8)

22 astore_3
23 aload_2
24 monitorexit //22-26为了保证抛出异常也能释放锁
25 aload_3
26 athrow //IO流输出 存在这种异常情况发生
27 return

 */

/*
    当加了throw new RuntimeException();
    code 字节码生成的助记符 只生成一个monitorexit

 0 iconst_1
 1 istore_1
 2 aload_0
 3 getfield #3 <com/example/demo/com/concurrecy/concurrency3/MyTest1.object>
 6 dup
 7 astore_2
 8 monitorenter
 9 getstatic #4 <java/lang/System.out>
12 ldc #5 <hello world!>
14 invokevirtual #6 <java/io/PrintStream.println>
17 new #7 <java/lang/RuntimeException>
20 dup
21 invokespecial #8 <java/lang/RuntimeException.<init>>
24 athrow
25 astore_3
26 aload_2
27 monitorexit//此时在方法体中抛出了异常 异常结束 直接释放了锁
28 aload_3
29 athrow //表示该方法一定会以异常结束
 */

而其对应的标识符如下:

此时就没有通过monitorebter和moniterexit 来获取锁而是通过ACC_SYNCHRONIZED标识符来尝试获取锁synchronized修饰静态方法。

当synchronized修饰静态方法其实跟修饰成员方法一样 只不过方法标识符多了个ACC_STATIC,并且其锁的是类锁。

/**
 * 当synchronized修饰静态方法其实跟修饰成员方法一样 只不过方法标识符多了个ACC_STATIC
 * 其次锁的是 类锁
 */
public class MyTest3 {
    /**
     * static静态方法不存在this局部变量
     * 原因直接类名.就能调用
     */
    public static synchronized void method() {
        System.out.println("hello world!");
    }

}
/*
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <hello world!>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return

 */

Monitor设计的概念

互斥与同步定义

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

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

synchronized底层原理

JVM中的同步是基于进入与退出监视器对象(管程对象)(Monitor)来实现的,每个对象实例都会有一个Monitor对象(每个Class生成时都会有且只有一个Monitor对象(锁) ), Monitor对象会和Java对象一同创建并销毁。Monitor对象是由C++来实现的。

当多个线程同时访问一段同步代码时,这些线程会被放到一个EntryList集合当中,处于阻塞状态(未获取对象锁 要区别WaitSet)的线程都会被放到该列表当中。 接下来,当线程获取到对象的Monito时,Monitor是依赖于底层操作系统的mutex lock(互斥锁)来实现互斥的,线程获取mutex成功。则会持有该mutex,这时其他线程就无法获取到该mutex.。

如果线程调用了wait方法(意思的调用wait方法才会进入WaitSet 竞争monitor时是和entryList 公平竞争),那么该线程就会释放掉所持有的mutex, 并且该线程会进入到WaitSet集合(等待集合)中,等待下一次被其他该对象锁线程调用notify/notifyAll唤醒(此处注意如果在WaitSet中被唤醒的线程没有竞争到锁该线程会进入entryList阻塞集合)。如果当前线程顺利执行完毕方法。那么它也会释放掉所持有的mutex。

用户态和内核态资源调度

总结一下:同步锁在这种实现方式当中,因为Monitor是依赖底层的操作系统实现,这样就存在用户态(如程序执行业务代码在用户端)与内核态(Monitor是依赖于底层操作系统 此时阻塞就是内核执行)之间的切换,所以会增加性能开销。 采用自旋作为回退机制当线程自旋时还是用户态占用的是CPU资源==(自旋太久也会造成CUP资源的浪费) 当自旋时间超过预期值还是会进入内核态。

通过对象互斥锁的概念来保证共享数据操作的完整性。每个对象都对应与一个可称为【互斥锁】的标记,这个标记用于保证在任何时刻,只能有一个线程访问该对象。

存在问题

那些处于EntryList与WaitSet中的线程均处于阻塞状态(两个集合都属于Monitor对象的成员变量),阻塞操作是由操作系统来完成的,在Linux下是通过pthread_mutex_lock函数实现的。 线程被阻塞后便会进入到内核调度状态,这会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

解决方案

解决上述问题的办法便是自旋(Spin)。其原理是:当发生对Monitor的争用时,若Owner(拥有线程或BasicLock指针)能够在很短的时间内释放掉锁,则哪些正在争用的线程就可以稍微等待一下(即所谓的自旋),在Owner线程释放锁之后,争用线程可能会立刻获取到锁,从而避免了系统阻塞(内核态).不过,当Owner运行的时间超过了临界值后。争用线程自旋一段时间后依然无法获取到锁,这时争用线程则会停止自旋而进入到阻塞状态(内核态)。所有总体的思想:先自旋,不成功再进行阻塞,尽量降低阻塞的可能性,这对执行时间很短的代码块来时有极大的性能提升。显然自旋在多处理器(多核心)上才有意义。

互斥锁属性

PTHREAD_MUTEX_TIMED_NP: 这是省缺值,也就是普通锁,当一个线程加锁以后,其余请求锁的线程将会形成一个等待队列,并且在解锁后按照优先级获取到锁,这种策略可以确保资源分配的公平性。

PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁.允许一个线程对同一个锁成功获取多次,并通过unlock解锁。如果是不同线程请求,则在加锁线程解锁时重写进行竞争。

PTHREAD_MUTEX_ERRORCHECK_NP:检错锁。如果一个线程请求同一把锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMEDNP类型动作相同,这样就能保证了当不允许多次加锁时不会出现最简单的死锁。

PTHREAD_MUTEX_ADAPTIVE_NP:适应锁.动作最简单的锁类型,仅仅等待解锁后重新竞争。

Monitor

JVM中的同步是基于进入与退出监视器对象(管程对象)(Monitor)来实现的,每个对象实例都会有一个Monitor对象(每个Class生成对象时都会有且只有一个Monitor对象(锁)伴生 ), Monitor对象会和Java对象一同创建并销毁。Monitor对象是由C++来实现的。

Monitor对象是啥?

jdk8u/jdk8u-dev/hotspot: 3b255f489efa src/share/vm/runtime/objectMonitor.hpp

通过OpenJDK翻看JVM底层的一些C++代码。

点击进入hpp后缀文件找到如下的方法,ObjectWaiter对当前线程的封装 底层通过链表来记录。

//截取如下
class ObjectWaiter : public StackObj {
 public:
  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
  enum Sorted  { PREPEND, APPEND, SORTED } ;
  ObjectWaiter * volatile _next;//指向下一个ObjectWaiter 
  ObjectWaiter * volatile _prev;//指向上游的ObjectWaiter 
  Thread*       _thread;

这么做的好处。

我们可以从一个ObjectWaiter 知道其他ObjectWaiter 的位置可以根据对应的策略选择性的唤醒对应的ObjectWaiter 如首位 中间指定等。

当waitset中唤醒的线程没有获取到monitor 就会将唤醒的线程放到entryList(也是链表格式)当中当entryList当中拿到锁就将对应线程从entryList中移除。

当没有遇到wait()方法时直接进入EntryList集合当中。

注意:WaitSet线程只是那些调用了wait()的线程,而EntryList是用来存储阻塞线程。

Wait JVM底层核心代码解析。

class ObjectMonitor {
 public:
  enum {
    OM_OK,                    // no error 没有错误
    OM_SYSTEM_ERROR,          // operating system error 操作系统错误
    OM_ILLEGAL_MONITOR_STATE, // IllegalMonitorStateException  异常
    OM_INTERRUPTED,           // Thread.interrupt()
    OM_TIMED_OUT              // Object.wait() timed out  超时
  };

对应成员变量。

// initialize the monitor, exception the semaphore, all other fields
  // 初始化monitor,  
  // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0, 
    _recursions   = 0;//嵌套锁 递归嵌套
    _object       = NULL;
    _owner        = NULL;//拥有线程或BasicLock指针
    _WaitSet      = NULL;//wait等待集合
    _WaitSetLock  = 0 ; //自旋锁标识字段 保护等待队列
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //阻塞集合
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

如下文档注释。

protected:
  ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor
  									//Monitor上的所有线程等待()集合

 protected:
  ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry.	
  											//线程在进入或返回时被阻塞。

 protected:                         // protected for jvmtiRawMonitor
  void *  volatile _owner;          // pointer to owning thread OR BasicLock
  									//指向拥有线程或BasicLock的指针

由JVM底层C++代码和文档注释我们可知_WaitSet和_EntryList 其实是其Monitor对应的的成员变量 初始值都为NULL。

在objectMonitor.cpp文件当中如wait方法实际对应与java Object基类当中的wait(0)所对应的方法。

// Wait/Notify/NotifyAll
//
// Note: a subset of changes to ObjectMonitor::wait()
// will need to be replicated in complete_exit above
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
	  .....
	ObjectWaiter node(Self);//被包装的线程节点
   node.TState = ObjectWaiter::TS_WAIT ; 
   Self->_ParkEvent->reset() ;
   OrderAccess::fence();          // ST into Event; membar ; LD interrupted-flag

 // Enter the waiting queue, which is a circular doubly linked list in this case
//输入等待队列,在本例中是一个循环的双链接列表
// but it could be a priority queue or any data structure.
//但它可以是优先级队列或任何数据结构。(链表优势)
// _WaitSetLock protects the wait queue. Normally the wait queue is accessed only
//_WaitSetLock保护等待队列。通常只访问等待队列
// by the the owner of the monitor *except* in the case where park()
//由监视器的所有者*except*在park()的情况下
// returns because of a timeout of interrupt. Contention is exceptionally rare
//由于中断超时而返回。争论异常罕见
// so we use a simple spin-lock instead of a heavier-weight blocking lock.
//所以我们使用了一个简单的自旋锁,而不是一个更重的重量级锁。
   Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;//自旋捕获 锁
   AddWaiter (&node) ;//用来更换指针引用
   ......
   exit (true, Self) ;      // exit the monitor 退出monitor

更换内容如下:

inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
  assert(node != NULL, "should not dequeue NULL node");
  assert(node->_prev == NULL, "node already in list");
  assert(node->_next == NULL, "node already in list");
  // put node at end of queue (circular doubly linked list)
  if (_WaitSet == NULL) {
    _WaitSet = node;
    node->_prev = node;
    node->_next = node;
  } else {
    ObjectWaiter* head = _WaitSet ;
    ObjectWaiter* tail = head->_prev;
    assert(tail->_next == head, "invariant check");
    tail->_next = node;
    head->_prev = node;
    node->_next = head;
    node->_prev = tail;
  }
}

在这我们看出当调用了waitSet方法时底层C++时,先进行SpinAcquire (自旋捕获)尝试获取锁,没获取到则将对应线程添加到waitSet当中以链表的形式,当完成上述操作时exit monitor。

notify底层核心代码解析

void ObjectMonitor::notify(TRAPS) {
  CHECK_OWNER();
  if (_WaitSet == NULL) {//_WaitSet 为null 直接返回
     TEVENT (Empty-Notify) ;
     return ;
  }
  ....
   Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
  //DequeueWaiter 根据不同的调度策略获取waitSet集合链表中目标线程
  ObjectWaiter * iterator = DequeueWaiter() ;
  .....
   if (Policy == 0) {       // prepend to EntryList
       if (List == NULL) {
           iterator->_next = iterator->_prev = NULL ;
           _EntryList = iterator ;
       } else {
           List->_prev = iterator ;
           iterator->_next = List ;
           iterator->_prev = NULL ; 
           _EntryList = iterator ; //此时如果目标线程未获取到monitor则放入ENtryList当中
      }

总结notify底层会先根据不同调度策略获取waitSet集合链表中目标线程,此时如果目标线程未获取到monitor则放入ENtryList当中。

notifyAll底层核心代码解析

void ObjectMonitor::notifyAll(TRAPS) {
  CHECK_OWNER();
  ObjectWaiter* iterator;
  if (_WaitSet == NULL) { //WaitSet null 直接返回
      TEVENT (Empty-NotifyAll) ;
      return ;
  }
  DTRACE_MONITOR_PROBE(notifyAll, this, object(), THREAD);

  int Policy = Knob_MoveNotifyee ;
  int Tally = 0 ;
  Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notifyall") ;

  for (;;) { //遍历所有
     iterator = DequeueWaiter () ; //拿到对应的WaitSet 全部唤醒
     if (iterator == NULL) break ;
     TEVENT (NotifyAll - Transfer1) ;
     ++Tally ;
     ....

总结:notifyAll底层通过死循环唤醒WaitSet 所有的ObjectWaiter 目标线程。

责任编辑:姜华 来源: 今日头条
相关推荐

2021-01-08 08:34:09

Synchronize线程开发技术

2024-08-28 08:00:00

2022-12-26 09:27:48

Java底层monitor

2024-07-25 11:53:53

2009-11-28 20:24:13

Linux互斥锁同步移植

2022-12-19 08:00:00

SpringBootWeb开发

2024-10-14 08:51:52

协程Go语言

2023-07-11 08:00:00

2024-04-11 11:04:05

Redis

2021-02-07 07:40:31

Synchronize用法

2021-12-14 14:50:12

synchronizeJava

2020-09-16 07:56:28

多线程读写锁悲观锁

2024-03-15 15:12:27

关键字底层代码

2023-07-17 08:02:44

ZuulIO反应式

2019-10-16 16:33:41

Docker架构语言

2020-08-26 08:59:58

Linux线程互斥锁

2024-01-05 09:00:00

SpringMVC软件

2024-06-28 08:45:58

2024-09-06 11:52:47

2011-04-14 13:27:53

Synchronize多线程
点赞
收藏

51CTO技术栈公众号