java.util.concurrent 同步器框架详解

本文详细介绍了Java并发编程中的AQS(AbstractQueuedSynchronizer)同步器框架,包括线程安全、上下文切换的基础知识。AQS提供了同步状态的原子管理、线程阻塞与唤醒以及排队机制。文章通过ReentrantLock为例,解释了AQS在独占模式和共享模式下的工作原理,阐述了公平锁与非公平锁的区别,并展示了lock()、lockInterruptibly()、tryLock(long,long)等方法的实现细节。此外,还讨论了ReentrantLock相对于synchronized的优劣,帮助读者理解并发编程中的锁机制和AQS的设计思想。" 106944330,9808178,达梦数据库学习与实践,"['数据库', '达梦数据库', '安装配置', 'Oracle兼容', '数据管理']
摘要由CSDN通过智能技术生成

作者简介

善光,一个半年才打一次篮球的程序猿。一直从事 Java 化开发,目前负责物流压力平衡、千里眼和风暴眼 Java 化项目。

引言

一般的应用系统中,存在着大量的计算和大量的 I/O 处理,通过多线程可以让系统运行得更快。但在 Java 多线程编程中,会面临很多的难题,比如线程安全、上下文切换、死锁等问题。

线程安全

引用 《Java Concurrency in Practice》 的作者 Brian Goetz 对线程安全的定义:

线程安全,当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的

那么如何实现线程安全呢?互斥同步是最常见的一种线程安全保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。

在 Java 中,最基本的互斥同步手段就是 synchronized 关键字,synchronized 有两种使用形式,分别为同步方法和同步代码块:

/**
 * 同步方法在执行前先获取一个监视器,如果是一个静态方法,监视器关联这个类,如果是一个实例方法, 
 * 则关联这个调用方法的对象。同步方法是隐式的,同步方法常量池中会有一个 ACC_SYNCHRONIZED 标 
 * 志,当某个线程访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有,则需要先获得监 
 * 视器锁,然后开始执行方法,方法执行之后再释放监视器锁,这时候如果有其他线程来请求执行该方法, 
 * 会因为无法获得监视器锁而被阻塞住。
 */
class Test {
    int count;
    synchronized void bump(){
        count++;
    }
    static int classCount;
    static synchronized void classBump(){
        classCount ++;
    }
}
/** 
 * 同步块在执行前先获取一个监视器,监视器关联括号里面 expression 引用的对象。 同步代码块则是使用 
 * monitorenter 和 monitorexit 两个指令实现的,monitorenter 可以理解为加锁,monitorexit 可以理解为 
 * 释放锁,每个对象自身维护着一个被加锁次数的计数器,当计数器为 1 时,只有获得锁的线程才能再次获 
 * 得锁,即可重入锁,当计数器为0时表示任意线程可以获得该锁。
 * synchronized 的 expression 只能是引用类型,否则会发生编译错误,如果为空,则会抛出空指针异常,
 * 不管同步块中的代码是否正常执行完,监视器都会解锁。
 */
synchronized (expression){
   // block code
}

那么对象如何与监视器关联呢,在 Java 中,对象包含三块:对象头、实例数据、填充数据,其中对象头中就包含 Mark Word,Mark Word 一般存储对象的 hashCode、GC分代年龄以及锁信息,锁信息就包含指向互斥量(重量级锁)的指针,指向了一个监视器;监视器是通过 ObjectMonitor 来实现的,代码如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

从上面代码可以看到有 ObjectMonitor 两个队列,分别是 _WaitSet 和 _EntryList,_owner 指向持有 ObjectMonitor 对象的线程,当多个线程获取到对象 monitor 后进入 _owner 区域,并把 _owner 设置为指向当前线程,并把 _count 数量加1;当调用 wait() 方法后,将释放当前持有的 monitor,_owner 置为空,_count 减 1 操作,同时,将该线程进入 _WaitSet 集合中等待唤醒,总结如下图:
image

上下文切换

现代操作系统中,运行一个程序,系统会为它创建一个进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享内存变量。

主要有三种方式实现线程:内核线程实现、用户线程实现、用户线程加轻量级进程混合实现。Java 里面的 Thread 类,它的所有关键方法都是声明 Native 的,和平台有关的方法。从 JDK 1.2 起,对于 Sun JDK 来说,它的 Windows 版与 Linux 版都使用一对一的线程模型实现,一条 Java 线程就映射到一条轻量级进程中,程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程,轻量级进程就是我们通常意义上所说的线程,由于每个轻量级进程都由一个内核线程支持,因此,只有先支持内核级线程,才能有轻量级进程。这种轻量级进程与内核线程之间 1:1 的关系称为一对一的线程模型。
image

由于有内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程阻塞了,也不会影响整个进程的工作,但是轻量级进程有它的局限性,每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源,因此一个系统支持轻量级进程的数量是有限的,其次,系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernal Mode)中来回切换,线程上下文切换直接的损耗CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉。对于抢占式操作系统来说:

  • 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务
  • 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务
  • 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务
  • 用户代码挂起当前任务,让出CPU时间
  • 硬件中断

综上所述,在 JDK 1.5 之前,通过 synchronized 关键字是保证线程安全的一种重要手段,但是 synchronized 是一个重量级锁,为什么说 synchronized 是一个重量级锁呢?因为 synchronized 依赖于操作系统的 MutexLock(互斥锁)来实现的,且等待获取锁的线程将会阻塞,被阻塞的线程不会消耗 CPU,但是阻塞或唤醒一个线程都需要涉及到上下文切换,涉及到用户态和内核态的切换,所以是比较耗时的。

那除了 synchronized 之外,我们还有其他的方案吗?答案是肯定的,我们可以使用 java.util.concurrent 的 ReentrantLock,但是在了解 ReentrantLock 之前,我们先了解下同步器框架。

同步器框架

概要

在 JDK1.5 的 java.util.concurrent 包中,大部分的并发类都是基于 AbstractQueuedSynchronizer(简称 AQS)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值