文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-11-30 00:51

关注

状态对象

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

示例:

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 {
    
    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;
            }
        }
    }
}

示例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指令来实现的锁的获取与释放动作。


public class MyTest1 {

    private Object object = new Object();

    public void method() {
        int i = 1;
        
        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 或者将其放置在方法中。


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


而其对应的标识符如下:

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

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


public class MyTest3 {
    
    public static synchronized void method() {
        System.out.println("hello world!");
    }

}

Monitor设计的概念

互斥与同步定义

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

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 目标线程。

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯