线程
- 线程就是一个执行控制流(进程是关于内存空间隔离)
- 每个线程都有自己的程序计数器和栈指针,合称为线程上下文
- 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取代一对monitorenter和monitorexit–>
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对象;对原子指令,中央控制点是总线。
- 阻塞锁通过非阻塞锁加上等待实现。对于等待时长不确定的,阻塞还是需要的。
参考
- 《虚拟机设计与实现》