0
点赞
收藏
分享

微信扫一扫

【笔记】java中线程和同步件的实现原理

迪莉娅1979 2022-02-21 阅读 52

线程

  • 线程就是一个执行控制流(进程是关于内存空间隔离)
  • 每个线程都有自己的程序计数器和栈指针,合称为线程上下文
  • linux内核中所有任务共享内存空间,所以严格来说内核级别没有进程,只有线程。进程只存在用户空间,用户空间为每个进程建立了隔离的虚拟内存空间

线程在os的设计

硬件线程<–>内核线程<–(用户空间->)–>本地线程

  • 本地线程也称为os线程
  • 本地线程之上的线程库通常称为用户层线程或绿色线程

java api中,调用Thread.start()会启动线程Thread实例的run()方法开始执行。java vm会启动如下方法:
请添加图片描述

多线程间的同步件

多个线程要相互合作,至少需要同步两个地方:共享数据的互斥访问,共享数据的条件访问

以经典的生产者-消费者为例,条件检查入队操作都应该用锁保护起来

while( true) {
    //生产者锁住队列进行检查
    lock(Queue);
    while(Queue is full) {
        unlock(Queue);
        // 方法一:null,效率不高。锁很快就被锁住,消费者找不到机会来锁住队列
        // 方法二:插入yiled(n)或sleep(n)等一会
        // 方法三:sleep_waiting(Queue is not full) 不会浪费cpu周期
        lock(Queue);
    }
    enqueue(Queue, Item); 
    unlock( Queue);
}

java中线程的同步件

  • jvm中,每个对象都与一个monitor关联
  • 线程用字节码指令monitorenter锁住monitorexit解锁这个monitor(java程序中用synchronized块或方法将这对指令封装起来)
  • 并且这个锁是可重入的

为了支持条件访问,每个对象上还有一个线程等待队列。线程在这个对象上调用wait()就会被添加到这个队列上并休眠;然后其他线程在这个对象上调用notify()notifyAll()时,这个线程就会被唤醒。(说明java中每个对象都有wait()notify()notifyAll()`方法)

  • 在一个对象上调用wait()时,进行以下操作:休眠该对象的线程,并释放锁;notify()时,唤醒对象,并获得锁
  • wait()的单个原子操作:
object.wait():
    monitorexit(object);
    sleep_waiting(object);
    monitorenter(object);

例子如下:

while( true) {
    //生产者锁住队列进行检查
    monitorenter(Queue);
    while(Queue is full) {
        monitorexit(Queue);
        sleep_waiting(Queue is not full);
        monitorenter(Queue);
    }
    enqueue(Queue, Item); 
    monitorexit(Queue);
}

使用关键字synchronized取代一对monitorentermonitorexit–>

while( true) {
    //生产者锁住队列进行检查
    synchronized {
        while(Queue is full) {
            monitorexit(Queue);
            sleep_waiting(Queue is not full);
            monitorenter(Queue);
        }
        enqueue(Queue, Item); 
    }
}

进一步使用wait()简化—>

while( true) {
    //生产者锁住队列进行检查
    synchronized {
        while(Queue is full) {
            Queue.wait();
        }
        enqueue(Queue, Item); 
    }
}

线程同步在jvm中的实现

请添加图片描述

  • 每个线程都有一个已进入的monitor列表(locked_obj_list),该线程因无法锁定而阻塞的一个对象(blocked_lock),以及它等待条件的一个对象(waited_condition)
  • 我们用对象头元数据中的1位LOCK_BIT来指示这个对象是否被某个线程锁住。如果它被一个线程锁住,那么它就会被记录在这个线程的locked_obj_list列表中
  • locked_obj_list列表的节点类型如下:
struct Locked_obj {
    object* jobject;  //锁住的monitor对象
    int recursion; //  重复锁定的次数
    Locked_obj *next;//列表中的下一个节点
}

请添加图片描述

下面分别介绍monitorexit(object) monitorenter(object) object.wait() object.notify()四个方法在jvm中的实现:

1. monitorenter

monitorenter的操作语义如下:

  • 步骤1:检查monitor(对象头LOCK_BIT)是否已被锁定。
  • 步骤2:如果monitor没有锁定,锁住它然后返回。(原子操作)
  • 步骤3:如果monitor已经锁定,检查它是否被本线程锁定。如果是的话,递增重入锁并返回。
  • 步骤4:如果monitor由其他线程锁定,等待以后再次锁定它。(阻塞,等待其他线程发送SIG_UNLOCK信号唤醒

monitorenter的伪代码如下:
请添加图片描述
请添加图片描述
lock_non_blocking()的伪代码如下。其逆操为lock_release(),清除对象头中的LOCK_BIT,表示未锁。注意只有锁的拥有者可以释放锁,所以lock_release()不需要原子化操作。
请添加图片描述
lock_blocking()的伪代码如下:
请添加图片描述

2. monitorexit

monitorexit是monitorenter的反向操作。它的操作语义如下:

  • 步骤1:检查锁是否由自身持有。
  • 步骤2:如果不是由自身锁定,抛出一个异常指示IllegalMonitorState
  • 步骤3:如果由自身锁定,检查重入次数,如果重入次数大于零,递减它然后返回。
  • 步骤4:如果重入次数为零,释放锁。
  • 步骤5:检查是否有任何线程阻塞等待锁定这个对象。如果没有等待线程就返回。如果有等待线程,唤醒它然后返回。(发送SIG_UNLOCK信号到被阻塞的线程唤醒

解锁函数实现起来很直观,不需要担心竞争状态

monitorexit的伪代码如下:
请添加图片描述
请添加图片描述
notify_blocking_thread()伪代码如下,注意SIG_UNLOCK信号不要与后面object.wait()的SIG_NOFITY信号混淆。
请添加图片描述

3.object.wait()

请添加图片描述

4.object.notify()

notify_blocking_thread()非常相似
请添加图片描述

monitor与原子

  • monitor是阻塞同步的,原子化可以认为是非阻塞同步的。但本质区别在与锁的粒度
  • 使用monitor,互斥通过检查内存中的共享数据实现,等待在os层级通过线程调度实现;使用原子指令,互斥通过处理器断言内存总线实现,等待在处理器层级通过指令流水线调度实现
  • 对monitor同步来说,中央控制点是monitor对象;对原子指令,中央控制点是总线。
  • 阻塞锁通过非阻塞锁加上等待实现。对于等待时长不确定的,阻塞还是需要的。

参考

  • 《虚拟机设计与实现》
举报

相关推荐

0 条评论