(转)Java中的几种锁机制

出自:Java中的几种锁机制
今天跟着blog整理一下几种锁,比如说 乐观锁和悲观锁,可重入锁和不可重入锁,自旋锁…

乐观锁和悲观锁
悲观锁:
总是假设最坏的情况,每次去拿数据的时候总是会假设自己在修改数据的时候别人也会修改数据,所以在每次获取数据的时候都会上锁。传统的关系型数据库就会用到锁机制,比如行锁、表锁、读锁、写锁等等。Java中 synchronized 和 ReentrantLock 等独占所就是悲观锁的思想。

乐观锁:
总是假设最好的情况,自己在获取数据的时候总是假设不会有人来修改数据,所以不会上锁,但是在更新的时候会判断此期间有没有人去修改数据,可以使用版本号机制和CAS算法实现,乐观锁适用于多读的引用类型,这样可以提高吞吐量,而Java中的java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁使用的场景

乐观锁适用于写少读多的场景,即冲突会很少,这样可以省去大量开销
悲观锁适用于多写的场景。因为冲突很多的时候乐观锁要不断进行retry,性能大大降低。
乐观锁实现的两种方式

版本号控制
一般是在数据表中加上version字段,表示数据被修改的次数,当数据被修改时,version会加1。当线程A要更新数据时,会读取该数据中的version,等到操作完提交更新的时候,若刚才读取到的version值和当前数据库中的version值相等则更新数据库,否则重新更新操作,直到更新成功。
CAS算法
即compare and swap算法,在不适用锁的情况下实现多线程同步,也就是实现在没有线程被阻塞的情况下实现线程同步。
CAS算法涉及到的3个操作数:
(1)需要读写的内存值 V
(2)预期的值 A
(3)拟写入的值B
当且仅当V的值等于A时,CAS采用原子的方式用新值B来更新V的值,否则不会进行任何操作。一般情况下会一直自旋,不断的重试。
乐观锁的缺点

ABA问题
如果一个变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍是A,那我们能说明这个A值没有被修改过吗?明显是不能的,这段时间它仍然可能是被改成其他值,然后又改回A,这就是ABA问题。
解决:JDK1.5之后的AtomicStampedReference类就可以解决,其中的compareAndSet方法就是首先检查当前引用是否等于预期引用,并且当前标识是否等于预期的标识,如果全部相等,则以原子的方式将该引用和该标识的值设置为拟写入的值。
循环时间开销大
自旋CAS(就是不成功就一直循环直到成功),如果长时间不成功,则会因为长期占有CPU而带了巨大的开销。
只能保证一个共享变量的原子操作
CAS只对单个共享变量有效,当操作涉及到跨多个共享变量时,CAS无效。
在jdk1.5后通过AtomicReference类可以将多个变量放入到一个对象中集体进行CAS操作。
CAS和synchronized的使用情景
之前有写过synchronized的优化,jdk1.6后synchronized在竞争不是很激烈的情况下用偏向锁和轻量锁,其底层都是基于硬件的CAS实现,而竞争激励时,CAS自旋严重影响CPU性能,所以换成了重量锁。这样追求了吞吐量。

自旋锁
什么是自旋锁

就是线程因为未获取线程而在一直循环等待的状态就是自旋锁状态。

自旋锁机制和互斥锁类似,都是保证了同一个时间只有一个线程能够获取共享变量。

自旋锁存在的问题

当某个线程占用锁的时间过长,也会导致自旋锁的线程一直在循环等到,消耗CPU。
自旋锁是不公平,无法满足等待时间最长的线程优先获取锁,会出现“饥饿线程”的问题。
自旋锁的优点

自旋锁不会使线程的状态发生切换,一直处于用户态,线程一直是活跃的,不会进入阻塞状态,减少了上下文的切换,执行速度快。

自旋锁和互斥锁的异同

两者都是保证线程安全资源共享的机制
两者保证只有一个线程能够获得共享变量
获取互斥锁的线程,如果线程已经被占用,则进入睡眠状态,而自旋锁则是一直自旋不会睡眠。
自旋锁不支持重入
即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。

自旋锁的变种

TicketLock
TicketLock主要是解决公平性的问题
可以理解为排队业务,每当一个线程在自旋的时候能拿到一个排队的id号,当一个线程释放了资源后,会根据id号,排队排的越久的优先获取锁,这个排队的id号都放入了线程的Threadlocal中
多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。下面的CLHLock和MCSLock就是解决这个问题的。
CLHLock
CLH锁是一种基于链表的可扩展、、高性能、公平的自旋锁。自旋时只在本地变量上进行自旋,不断轮询前驱的状态,如果前驱释放了锁就结束自旋,获得锁。
MCSLock则是对本地变量的节点进行循环
可重入锁和不可重入锁
什么是不可重入锁
如果当前线程执行某个方法时已经获取了该锁,那么在该方法中尝试再次获取锁时,就会获取不到然后被阻塞,我们设计一个不可重入锁:

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

使用该锁:

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

print中先用lock()获取一个锁,在doAdd中再去获取锁,但是这个时候就无法执行doAdd中的逻辑了,因为while中一直让线程阻塞,必须要先释放锁。这个例子很好的说明了不可重入锁。

可重入锁
接下来我们设计一个可重入锁:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是lockedCount自增,并记录lockBy为第一个线程。接着第一个线程进入doAdd()方法,由于同一进程,所以不会进入while而挂起,接着增量lockedCount,当第二个线程尝试lock,由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将lockCount递减为0,才将标记为isLocked设置为false。

这就是可重入锁和不可重入锁的区别,Java中的可重入锁ReentrantLock就是这样的设计思路。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容