并发编程中同步锁的分类及性质(以Java为例)

来源:互联网 发布:西门淘宝店网址 编辑:程序博客网 时间:2024/06/01 10:14

在过去看有关于同步锁的一些知识时,经常锁前面加的各种前缀整的眼花缭乱,让人觉得锁的种类错综复杂。所以花点时间梳理一下锁的分类问题。本文将锁的类型划分4种属性:公平性、阻塞性、可重入性以及读写互斥性。无论是什么锁都有这四个属性。当取不同属性做前缀去称呼锁时,可能会让人觉得他们不是一种锁。例如我们即可以称呼ReentrantLock为可重入锁也可以称呼它为互斥锁。

根据是否公平划分

公平性是锁必有的性质,任何一种锁要么是公平的要么是不公平的。
公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。通俗的说就是每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,类似于排队吃饭。
非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待再次尝试。通俗的说是每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关。类似于随机叫号,每个排队者手里都有一个和别人不同的号码,然后在这些号码中随机抽取一个号码,抽到谁就获取资源,与谁先来无关。
事实上公平的锁机制往往没有非公平的效率高,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了,但是并不是任何场景都是以TPS作为唯一指标的,因为公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

根据是否阻塞划分

首先声明一点,无论锁是否是阻塞锁,在没有获取到锁的情况下,程序均不能继续往下执行,而是停留在获取锁的地方。阻塞锁和非阻塞锁的区别在于是否使线程进入阻塞状态。非阻塞锁可以特指自旋锁(据我所知,非阻塞的锁都是通过while或for自旋语句实现等待的)。同公平性一样,每种锁都可根据阻塞性进行分类,要么是阻塞锁要么是非阻塞锁。
阻塞锁:可以说是让线程进入阻塞状态进行等待(放弃自己的剩余CPU时间片),当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态,如果获取锁成功,则继续执行接下来的任务,如果没有获取锁(例如超时唤醒的情况下),则可以接着睡眠等待或者不再参与资源的竞争。
JAVA中,能够使线程进入或退出阻塞状态以及包含阻塞锁的方法有:synchronized 关键字,ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(J.U.C并发框架经常使用)
自旋锁(非阻塞锁):当线程参与竞争锁资源时,若没有获取锁时,其独占一个线程,通过自旋轮询,进行下一次竞争,直到竞争到锁,或者直到分配给所在线程的时间片用尽,进入阻塞状态。它和阻塞锁的主要区别是当竞争不到锁时,不会主动使线程进入阻塞状态,而是继续参与锁的竞争。
采用旋转锁时,等待线程并不静态地阻塞在同步点,而是必须“旋转”,不断尝试直到最终获得该锁。旋转锁多用于多处理器系统中。这是因为,如果在单核处理器中采用旋转锁,当一个线程正在“旋转”时,将没有CPU资源可供另一释放锁的线程使用。旋转锁适合于任何锁持有时间少于将一个线程阻塞和唤醒所需时间的场合。线程控制的变更,包括线程上下文的切换和线程数据结构的更新,可能比旋转锁需要更多的指令周期。旋转锁的持有时间应该限制在线程上下文切换时间的50%到100%之间(Kleiman,1996年)。在线程调用其他子系统时,线程不应持有旋转锁。对旋转锁的不当使用可能会导致线程饿死,因此需谨慎使用这种锁机制。旋转锁导致的饿死问题可使用排队技术来解决,即每个等待线程按照先进先出的顺序或者队列结构在一个独立的局部标识上进行旋转

根据是否可重入划分

可重入锁(递归锁):指可以被当前持有该锁的线程重复获取,而不会导致该线程产生死锁的锁类型。对可重入锁而言,只有在当前锁的持有线程的获取锁操作和释放锁操作对应时,其他线程才可以获取该锁。因此,在使用递归锁时,必须要用足够的释放锁操作来平衡获取锁操作,实现这一目标的最佳方式是在单入口单出口代码块的两头一一对应地使用获取、释放操作,做法和在普通锁中一样。递归锁在递归函数中最有用。但是,总的来说,递归锁比非递归锁速度要慢。需要注意的是:调用线程获得几次递归锁必须释放几次递归锁,其示例代码如下:

Recursive_Lock Lvoid recursiveFunction (int count) {    L->acquire()    if (count > 0) {        count = count - 1;        recursiveFunction(count);    }    L->release();}

不可重入锁:当同一个线程在对获取的锁没有释放的情况下,再一次调用获取该锁的操作时,会产生死锁。当然这是我们都不愿看到的情况,所以这并不是我们期望的锁的性质,早期的时候很多锁时不可重入的,后来经过慢慢的改进,出现了很多可重入的锁。

根据是否是读写锁划分

读写锁(Read-Write lock):又称共享独占锁(shared-exclusive lock)、多读单写锁(multiple-read/single-write lock)或者非互斥信号量(non-mutual exclusion semaphore),读写锁允许多个线程同时进行读访问,但是在某一时刻却最多只能由一个线程执行写操作。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程能在没有写操作的情况下同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(读-读能共存,读-写和写-写都不能共存)。读写锁就是为了实现这样的操作。
Java语言在JDK1.5以上版本提供了读写锁的实现:ReentrantReadWriteLock
互斥锁:不区分读和写操作,无论是读还是写,都只能串行化地去访问共享资源,和读写锁相比就是,不仅读-写、写-写不能共存,读-读也不能共存。我们平时使用的大多数锁都是互斥锁,像synchronized,ReentrantLock都是互斥锁。

Java中常用锁的属性

锁 公平性 阻塞性 可重入性 读写锁? synchronized 统计上的公平性 阻塞锁 可重入 不是 ReentrantLock 默认不公平,可以设置为公平 阻塞 可重入 不是 ReentrantReadWriteLock 默认不公平,可以设置为公平 阻塞 可重入 是

以后在慢慢收集其他锁的信息。

原创粉丝点击