Operation System: Locks Classification Perspectives

本文主要参考:http://www.importnew.com/19472.html


对于锁的认识,我个人认为需要分为几个角度进行总结。首先,是基础角度,从最普遍的分类开始说起。接着,从公平锁/非公平锁、可重入锁、乐观锁(CAE)/悲观锁和共享锁/排它锁。


基础分类:


互斥锁 (mutex)、信号量锁 (semaphore)、读写锁(read/write lock)以及自旋锁(spin lock)。


互斥锁 & 信号量:


互斥锁是一种特殊的信号量锁。信号量锁的本质是维护一个表示当前核心区域(锁保护区域)的线程空间余量,也就是允许多少个线程进入。当线程检测到余量不为0时,进入核心区域,余量自减1。反之,如果检测到为0,则等待核心区域中的线程退出。


互斥锁就是初始余量为1的信号量锁。核心区域只允许一个线程进入,只有当目前线程离开核心区域后,第二个线程方可进入。


读锁 & 写锁:


读、写锁是根据线程共享的数据的形式来进行调整适当“粗化”。有以下几种形式:


1)当前所有的线程都是在读数据,那么可以有任意多的线程同时读,读锁是共享锁。

2)写锁则是独占锁。所以,有线程想写数据,但是也存在线程在读数据,必须等到所有的读线程完成后,才能加上写锁,因为它需要独占。

3)有线程在写数据时,所有的线程都不能接触数据,因为它需要独占。


总而言之,读写锁允许多个线程同时进行读访问,但是在某一时刻却最多只能由一个线程执行写操作。对于多个线程需要同时读共享数据却并不一定进行写操作的应用来说,读写锁是一种高效的同步机制。


自旋锁:


自旋锁适合在多核处理器上使用。旋转锁是一种非阻塞锁,由某个线程独占。采用旋转锁时,等待线程并不静态地阻塞在同步点,而是必须“旋转”,不断尝试直到最终获得该锁。旋转锁多用于多处理器系统中。这是因为,如果在单核处理器中采用旋转锁,当一个线程正在“旋转”时,将没有执行资源可供另一正在使用锁的线程使用。旋转锁适合于任何锁持有时间少于将一个线程阻塞和唤醒所需时间的场合。


同时需要说明的是,自旋锁能发挥其功效也是基于线程是内核线程的前提上。因为只有内核线程才能真正用到多核,从而不浪费当前正在使用资源的线程(同核切换会占用其计算资源)。


在Java当中,线程是映射到操作系统的原生线程之上的,是内核线程。之所以使用自旋锁,意义在于:如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态装换需要耗费很多的处理器时间,对于代码简单的同步块(synchronized掉的块),状态转换消耗的时间有可能比用户代码执行的时间还要长。


自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK6中已经变为默认开启,并且引入了自适应的自旋锁。自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋当代的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会拜拜浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。


公平锁/非公平锁角度:


公平锁是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。Java的ReentrantLock是可以实现公平锁的,通过构造方法ReentrantLock(ture)来要求使用公平锁。synchronized中的锁时非公平锁,ReentrantLock默认情况下也是非公平锁。


公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。


可重入锁:

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。意思是,如果在调用某个对象的锁的临界区里头调用同一个对象锁的临界区函数,则可以不受任何影响。


在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。


乐观锁/悲观锁:


悲观锁假设最坏的情况,只有在确保当前线程已经获取正确的锁,才会执行临界区代码。常见实现如mutex等。安全性更高,但在中低并发程度下的效率更低。


乐观锁先进行冲突检查,如果与预期值相同,证明没有别的线程修改,给该内存区赋予新值。如果与预期值不同,则返回不成功,可以设置重试,当然也可以设置放弃。部分乐观锁削弱了一致性,但中低并发程度下的效率大大提高。


乐观锁的常见实现是:CAS. 全称是Compare and Swap. CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。


Java对CAS的支持是用java.util.concurrent实现。参考以下代码:


public class AtomicInteger extends Number implements java.io.Serializable {  
 
    private volatile int value;  
 
    public final int get() {  
        return value;  
    }  
 
    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  
 
    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}


注意,unsafe.compareAndSwapInt()就是CAS。 相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。在没有锁的机制下需要字段value要借助volatile原语,保证线程间的数据是可见的。这样在获取变量的值的时候才能直接读值。


关于乐观锁的详细介绍和ABA问题,请参考:http://www.importnew.com/20472.html


共享锁/排它锁:


共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
排它锁:如果事务T对数据A加上排它锁后,则其他事务不能再对A加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。








  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值