【Java并发编程】synchronized(五):重量级锁原理分析(JDK6前)

1.JVM层面:monitor

JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步

1.1 两条指令:monitorenter 和 monitorexit

每一个 Java 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。当一个 monitor 被持有后,它将处于锁定状态。

  1. 在进入加锁代码块的时候加一个 monitorenter 的指令,然后针对锁对象关联的 monitor 累加加锁计数器,同时标识自己这个线程加了锁,通过 monitor 里的加锁计数器可以实现可重入的加锁。
  2. 在出锁代码块的时候,加一个 monitorexit 的指令,然后递减锁计数器,如果锁计数为0,就会标志当前线程不持有锁。

那这个 monitor 监视器到底是什么呢?又怎么和具体的对象关联起来的呢?

1.2 C++实现:ObjectMonitor

这个 monitor不是 java 实现的。是 C++ 实现的一个 ObjectMonitor 对象,里面包含了:

  • 一个 entryList,想要加锁的线程全部先进入这个 entrylist 等待获取机会尝试加锁;
  • 一个 count 计数器,当 count=0 时表示可以获取锁,count>0 就表示已经有线程获取锁了(注意重入情况)
  • 一个_owner 指针,指向了持有锁的线程;
  • 一个 waitSet,当获取锁的线程执行了wait就会进入waitset

执行的大值流程如下:

  1. 从 EntryList 中获取要拿锁的线程
  2. 如果 count=0 且线程 CAS 修改 count=1 成功,就会设置_owner 指针指向当前线程
  3. 如果获取锁的线程执行 wait,就会将计数器递减,同时_owner设置为null,然后自己进入waitset中等待唤醒,别人获取了锁执行notify的时候就会唤醒waitset中的线程竞争尝试获取锁。
  4. 释放锁是会将_count计数器递减1,如果为0了就会设置owner为null,表示自己彻底释放锁。
    在这里插入图片描述

那尝试加锁这个过程,也就是对 _count 计数器累加操作,是怎么执行的?如何保证多线程并发的原子性呢?JDK 1.6 之后,对 synchronized 内的加锁机制做了大量的优化,这里就是优化为 CAS 加锁的。

注:所以,不能说一使用 synchronized 这个线程就一定会上下文切换进入内核态,因为如果当前当前无并发,或者并发不激烈的情况下,JVM 是通过 CAS + 自旋来保证线程安全(只不过这些代码是在 JVM 的 C++ 代码中)。

2.OS层面:互斥锁

如上图,synchronized 是如何将线程阻塞在 EntryList 和 waitSet 的?答:是依赖于底层操作系统的 Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用于保护临界区,确保同一时间只有一个线程访问该对象的数据。

注:AQS 的 LockSupport#park() 也是借助互斥量实现的(参考链接…),所以,不能说我使用 JUC 的锁、并发容器什么的就全部是在用户态完成的。

2.1 临界区是什么?

一次只允许一个进程使用的共享资源称为临界资源(注:共享资源是可以同时被多个进程访问的资源),如打印机、公共变量等;而在并发进程中与共享变量有关的程序段称为临界区

对临界区的访问必须是互斥进行,以保证临界资源的安全使用,因此可以将一个访问临界资源的循环进程描述如下:

while(true){
	进入区 // 检查临界资源是否被正被访问
	临界区 // 操作临界资源的相关代码
	退出区 // 将临界区整备访问的标志恢复为未被访问的标志
	剩余区 // 进程中除上述三区外的其余部分代码
}

为了实现进程互斥的进入自己的临界区,可以使用软件方法,但更多的是在系统中设置专门的同步机制来协调各进程间的运行。所有同步机制都遵守以下四条准则:

  • 空闲让进:当无进程处于临界区时,表明临界资源处于空闲状态,营运寻一个请求进入临界区的进程立刻进入自己的临界区,以有效利用临界资源
  • 忙则等待:当已有进程进入临界区时,表明临界资源正在被访问,因而其他试图进入临界区的进程必须等待,以保证对临界资源的互斥访问
  • 有限等待:对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态
  • 让权等待:当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态

如果每次都要判断临界资源状态,然后再修改修改临界资源状态会使代码较为繁琐,并且稍有不慎就会出现调度出错。所以有没有什么能使用的互斥工具呢?

2.2 互斥量是什么?

互斥量即 mutex(mutual exclusive),也便是常说的互斥锁。它的使用思路也很简单粗暴,多个进程共享一个互斥量,然后进程之间去竞争,等得到锁的进程可以进入临界区执行代码。mutex的基本函数如下:

#include<pthread.h> // 以下方法都是成功返回0,失败返回-1
int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr *attr);  // 初始化锁
int pthread_mutex_lock(pthread_mutex_t *mutex);   // 对资源加锁,阻塞。
int pthread_mutex_trylock(pthread_mutex_t*mutex); // 对资源加锁,非阻塞
// 临界区...
// 临界区...
int pthread_mutex_unlock(pthread_mutex_t *mutex);  // 对资源解锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);  // 销毁锁                

互斥量与信号量区别

互斥锁(Mutex)与信号量(Semaphore)的函数基本相似,主要区别是信号量是一种同步机制,可以当作锁来用,但也可以当做进程/线程之间通信使用,作为通信使用时不一定有锁的概念;互斥锁是为了锁住一些资源,是为了对临界区做保护:

1)互斥量用于线程的互斥,信号量用于线程的同步

  • 互斥:指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排他性。但是互斥无法限制访问者对资源的访问顺序,所以访问是无序的
  • 同步:指在互斥的基础上(多数情况),通过其他机制实现访问者对资源的有序访问。大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况指可以允许多个访问者同时访问资源

2)互斥量值只能是0/1,信号量值可以为非负整数

  • 一个互斥量只能用于一个资源的互斥访问不能实现多个资源的多线程互斥问题
  • 一个信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,也可以完成一个资源的互斥访问

3)互斥量的加锁和解锁必须由同一线程分别对应使用;而信号量可以由一个线程释放,另外一个线程得到

PS:更多关于信号量和互斥量的分析,请看 synchronized(一):信号量、互斥量原理分析对比

猜你喜欢

转载自blog.csdn.net/qq_33762302/article/details/114297758