Java SDK并发包的锁------Lock

并发包里的管程 Lock

我们提到过在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题

什么是死锁

现实世界里的死等,就是编程领域的死锁了。死锁的一个比较专业的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象

如何预防死锁

要避免死锁就需要分析死锁发生的条件,有个叫 Coffman 的牛人早就总结过了,只有以下这四个条件都发生时才会出现死锁:

1、互斥,共享资源 X 和 Y 只能被一个线程占用;

2、占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;

3、不可抢占,其他线程不能强行抢占线程 T1 占有的资源;

4、 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

反过来分析,也就是说只要我们破坏其中一个,就可以成功避免死锁的发生。

其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

占用且等待

我们可以一次性申请所有的资源,这样就不存在等待了。

不可抢占

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

循环等待

对于这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

并发包解决死锁方案

前面提出了一个破坏不可抢占条件方案,但是这个方案 synchronized 没有办法解决。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:

对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

Lock如何解决死锁问题

能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。

支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

体现在 API 上,就是 Lock 接口的三个方法。详情如下:

// 支持中断的 API
void lockInterruptibly() 
  throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) 
  throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();

Lock如何保证可见性

Java SDK 里面 Lock 的使用,有一个经典的范例,就是try{}finally{},需要重点关注的是在 finally 里面释放锁。这个范例无需多解释,你看一下下面的代码就明白了。但是有一点需要解释一下,那就是可见性是怎么保证的。你已经知道 Java 里多线程的可见性是通过 Happens-Before 规则保证的,而 synchronized 之所以能够保证可见性,也是因为有一条 synchronized 相关的规则:synchronized 的解锁 Happens-Before 于后续对这个锁的加锁。那 Java SDK 里面 Lock 靠什么保证可见性呢?例如在下面的代码中,线程 T1 对 value 进行了 +=1 操作,那后续的线程 T2 能够看到 value 的正确结果吗?

class X {
  private final Lock rtl =
  new ReentrantLock();
  int value;
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value+=1;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

答案必须是肯定的。Java SDK 里面锁的实现非常复杂,这里我就不展开细说了,但是原理还是需要简单介绍一下:它是利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:

1、 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();

2、volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;

3、传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。

class SampleLock {
  volatile int state;
  // 加锁
  lock() {
    // 省略代码无数
    state = 1;
  }
  // 解锁
  unlock() {
    // 省略代码无数
    state = 0;
  }
}

Lock的API(interface)
方法名称描述
void lock()获取锁,调用该方法该线程会获取锁,当锁获得后,从该方法返回
void lockInterruptibly() throws InterruptedException可中断地获取锁,该方法会响应中断,当获取锁的线程被中断时,中断异常会抛出,同时锁会被释放
boolean tryLock()尝试非阻塞的获取锁,调用该方法时会立即返回,如果能够获取则返回true,否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException超时的获取锁,当前线程会在一下3中情况下返回:1、当前线程在超时时间内获取到锁;2、当前线程在超时时间内中断;3、当前线程超时时间结束,返回false
void unlock()释放锁
Condition newCondition()获取等待通知组件,该组件和当前的锁是绑定关系,当前线程只有获得锁,才能调用该组件的wait()方法,而调用该方法以后,当前线程则释放锁
Condition相关API(interface)
方法名称描述
void await() throws InterruptedException当前线程处于等待状态,直到收到唤醒的信号或者线程中断
void awaitUninterruptibly()当前线程处于等待状态,直到收到唤醒信号或者中断,或者指定的等待时间已过
long awaitNanos(long nanosTimeout) throws InterruptedException当前线程处于等待状态,直到收到唤醒信号或者中断,或者指定的等待时间已过
boolean await(long time, TimeUnit unit) throws InterruptedException当前线程处于等待状态,直到收到唤醒信号或者中断,或者指定的等待时间已过,类似于awaitNanos(long nanosTimeout)
boolean awaitUntil(Date deadline) throws InterruptedException当前线程处于等待状态,直到收到唤醒信号或者中断,或者指定的截至时间已过
void signal()唤醒一个等待的线程
void signalAll()唤醒所有的等待线程
接口函数说明 /*============================================================================= 函数名: TP_Configuration ; 功 能:动态库初始化配置, 完成门类型选择/发卡器连接等 输 入:lock_type -- 门类型(也就是使用的卡片类型): 4-RF57门; 5-RF50门 输 出: 无 返回值:错误类型 =============================================================================*/ int __stdcall TP_Configuration(int lock_type); /*============================================================================= 函数名: TP_MakeGuestCardEx ; 功 能:制作宾客卡 输 入:room_no -- 门号: 字符串, 例如 "1.2.8102"等 checkin_time -- 入住时间: 预留. 制卡时入住时间被强制为当前时间!!! checkout_time -- 预离时间: 年月日时分秒, 字符串格式 "YYYY-MM-DD hh:mm:ss" iflags -- 宾客卡选项, 一般置0. iflags的各种赋值解释如下: 1: 表示允许开反 8: 表示复制卡, 不顶替前卡(可以跟之前发的卡片一起用, 允许一个房间发多张卡片) 32: 表示一次性开门, 开门后失效 128: 表示检测卡片的入住时间(如果卡片入住时间 > 门当前时间, 则不能开门, 不建议用这个选项) 上述各属性值可以相加进行组合. 如果iflags ==0, 表示会顶替前卡,刷卡后之前的卡片失效; 不是一次性开门卡; 不检测卡片的入住时间 输 出: card_snr -- 卡号: 字符串, 至少预分配20字节 例 子1: room_no="1.2.8102", SDateTime="", EDateTime="2008-06-07 12:00:00" iFlags=8 !!!!!!!!(复制卡) 返回值:错误类型 说明: Room要输入 门号!! 其格式视门管理软件而定, 请查看门管理软件的"客房设置"界面. 如果门管理软件中没有门号, 则请输入房号. =============================================================================*/ int __stdcall TP_MakeGuestCardEx(char *card_snr, char *room_no, char *checkin_time,char *checkout_time, int iflags); /*============================================================================= 函数名: TP_ReadGuestCardEx ; 功 能:读宾客卡信息 输 入:无。 输 出: card_snr -- 卡号: 字符串, 至少预分配20字节 room_no -- 房号: 字符串, 至少预分配20字节 checkin_time -- 入住时间: 年月日时分秒, 字符串格式 "YYYY-MM-DD hh:mm:ss", 至少预分配30字节 checkout_time -- 预离时间: 年月日时分秒, 字符串格式 "YYYY-MM-DD hh:mm:ss", 至少预分配30字节 iFlags -- 卡片标志字节 返回值:错误类型 =============================================================================*/ int __stdcall TP_ReadGuestCardEx(char *card_snr,char *room_no, char *checkin_time, char *checkout_time, int *iFlags); /*============================================================================= 函数名: TP_CancelCard ; 功 能:注销卡片/卡片回收 输 入: 无 输 出: 输 出: card_snr -- 卡号: 字符串, 至少预分配20字节 返回值:错误类型 =============================================================================*/ int __stdcall TP_CancelCard(char *card_snr); /*============================================================================= 函数名: TP_GetCardSnr ; 功 能:读取卡号(卡片的唯一的序列号) 输 入: 无 输 出: card_snr -- 卡号: 字符串, 至少预分配20字节 返回值:错误类型 =============================================================================*/ int __stdcall TP_GetCardSnr(char *card_snr); ////////////////////// 以下函数供酒管软件进行会员卡管理 /////////////////////// /*============================================================================= 函数名: TP_M1Active 功 能:卡片激活(读取卡号) 参 数:无 输 出: card_snr -- 卡号, 4字节(字符串表示为8个字符) 返回值:错误类型 描 述:卡片激活 =============================================================================*/ int __stdcall TP_M1Active(char *card_snr); /*============================================================================= 函数名: TP_M1AuthKey 功 能:验证卡片密钥 参 数:keyA: 密钥, 六字节, 卡片的默认密钥是 "ffffffffffff" sector_no: 扇区号, 1~40 返回值:错误类型 描 述:验证卡片的相应扇区的密钥, 验证过后才可以读写 例 子:keyA = "ffffffffffff", sector_no = 3 =============================================================================*/ int __stdcall TP_M1AuthKey(char *keyA, UINT sector_no); /*============================================================================= 函数名: TP_M1SetKeyA 功 能:修改密钥A 参 数:NewKeyA: 新密钥A, 六字节, 用字符串表示(12个字符) sector_no: 扇区号 返回值:错误类型 描 述:修改卡片密钥A, 注意要先用AuthKey验证原密钥. 注意函数调用顺序: TP_M1Active → TP_M1AuthKey → TP_M1SetKeyA 例 子:newKeyA = "ffffffffffff", sector_no = 3 =============================================================================*/ int __stdcall TP_M1SetKeyA(char *newKeyA, UINT sector_no); /*============================================================================= 函数名: TP_M1WriteBlock 功 能:写数据 参 数:block_no: 块号(一般是扇区号*4 + 扇区内块号) data: 要写入的16字节数据, 用字符串表示(32个字符,只能是0~9和A~F, a~f ) 返回值:错误类型 描 述:写一块数据. 必须先用TP_M1AuthKey验证密钥 ; 例如, 扇区9可以读写的块号括: 9x4 9x4+1 9x4+2, 即: 36 37 38块. 注意函数调用顺序: TP_M1Active → TP_M1AuthKey → TP_M1WriteBlock 例 子:block_no = 3x4 = 12, data = "112233445566778899AABBCCDDEEFF" =============================================================================*/ int __stdcall TP_M1WriteBlock(UINT block_no, char *data); /*============================================================================= 函数名: TP_M1ReadBlock 功 能:读数据 参 数:block_no: 块号(一般是扇区号*4 + 扇区内块号) 输 出: data: 16字节数据, 用字符串表示(32个字符) 返回值:错误类型 描 述:读取一块. 必须先用TP_M1AuthKey验证密钥. 注意函数调用顺序: TP_M1Active → TP_M1AuthKey → TP_M1ReadBlock =============================================================================*/ int __stdcall TP_M1ReadBlock(UINT block_no, char *data); 关于M1卡的简单说明: 1. 卡片有16个扇区,编号从0~15;每个扇区可以设置独立的密码; 2. 每个扇区里面有4个块,编号从0~3, 只可以读写0~2块。 3. 读卡和写卡时要用 "扇区号x4 + 扇区内块号", 扇区9可以读写的块号括: 9x4 9x4+1 9x4+2, 即: 36 37 38块. 4. 要避开门使用的扇区,门一般使用扇区1、扇区11、扇区15等, 具体可咨询门厂家;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值