java中的锁机制

  1. 悲观锁
  2. 乐观锁
  3. 自旋锁与自适应自旋
  4. 偏向锁//TODO
  5. 轻量级锁//TODO
  6. 重量级锁//TODO
    //剩下三个等空了再更新 …
悲观锁

总是假设最坏的情况,每次取数据都认为别人会改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到他拿完。传统的关系型数据库里面就用了很多这种锁,比如行锁,表锁,读锁,写锁等,都是在操作之前加锁。java中Synchronized和reentrantlock等独占锁就是悲观锁思想的实现

乐观锁

总是设想最好的情况,每次取数据都认为别人不会修改,所以不会上锁,但是在更新的时候回去判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,提高吞吐量,像数据库提供的类似于write-confition机制,其实就是乐观锁。java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种方式CAS实现的

乐观锁常见的两种实现方式
版本号控制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值加1.当线程A要更新数据值时,在读取数据的同事也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version时才更新,否则重试更新操作直到更新成功

CAS

compare and swap,无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。
CAS的语义是我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉V的实际值为多少。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。一般情况下是一个自旋操作,即不断的重试

int compare_and_swap (int* reg, int oldval, int newval) 
{
  ATOMIC();
  int old_reg_val = *reg;
  if (old_reg_val == oldval) 
     *reg = newval;
  END_ATOMIC();
  return old_reg_val;
}
乐观锁的常见问题
ABA问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很显然是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那么CAS操作就会误认为它从来没有被修改过,这个问题被称为CAS操作的ABA问题
JDK1.5以后的AtomicStampedReference类就提供了此种能力,其中的compareAndSet方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将引用和该标志的值设置为给定的更新值。

循环开销大

自旋CAS(也就是不成功就一直循环执行直到成功),如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升。pause指令有两个作用,第一它卡伊延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二可以避免在退出循环的时候因为内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率

只能保证一个共享变量的原子操作

CAS只对单个共享变量有效,当操作涉及跨多个共享变量时CAS无效。
从JDK1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

自旋锁

如果持有锁的线程能够在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态(一直处于用户态,即active,被阻塞后进入内核调度状态Linux),只需要等一等(自旋),等持有锁的线程释放完锁后立即获取锁,这样就避免用户线程和内核的切换的消耗
但是线程自旋是需要消耗CPU的,说白了就是让CPU做无用功,如果一直获取不到锁,那线程也不能一直占有CPU自旋做无用功,所以需要设定一个自旋等待的最大时间
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。减少了不必要的上下文切换,执行速度快
但是如果锁竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用CPU做无用功。
是不公平锁,即无法满足等待时间最长的线程优先获取锁,不公平的锁就会存在”线程饥饿“问题(某个线程一直获取不到锁一直陷入阻塞状态)

自旋锁的开启

JDK1.6中 -XX:+UseSpinning
-XX:PreBlockSpin=10为自选次数
JDK1.7后去掉此参数,由JVM控制

自旋锁的简单例子
public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}
自旋锁的变种——TicketLock

TicketLock主要解决的是公平性问题
每当有线程获取锁的时候,就给该线程分配一个递增的id,我们称之为排队号,同时,锁对应一个服务号,每当有线程释放锁,服务号就会递增,此时如果服务号与某个线程排队号一致,那么该线程就获得锁,由于排队号是递增的,所以就保证了最先请求锁的线程可以最先获取到锁,实现了公平性

public class TicketLock {
    /**
     * 服务号
     */
    private AtomicInteger serviceNum = new AtomicInteger();
    /**
     * 排队号
     */
    private AtomicInteger ticketNum = new AtomicInteger();
    /**
     * lock:获取锁,如果获取成功,返回当前线程的排队号,获取排队号用于释放锁. <br/>
     *
     * @return
     */
    public int lock() {
        int currentTicketNum = ticketNum.incrementAndGet();
        while (currentTicketNum != serviceNum.get()) {
            // Do nothing
        }
        return currentTicketNum;
    }
    /**
     * unlock:释放锁,传入当前持有锁的线程的排队号 <br/>
     *
     * @param ticketnum
     */
    public void unlock(int ticketnum) {
        serviceNum.compareAndSet(ticketnum, ticketnum + 1);
    }
}

这种实现方式是线程获取锁之后,将它排队号返回,等线程释放锁的时候需要将排队号传入。但是一旦排队号被修改那么锁不能被正确释放。可以将线程的排队号放到ThreadLocal中

public class TicketLockV2 {
    /**
     * 服务号
     */
    private AtomicInteger serviceNum = new AtomicInteger();
    /**
     * 排队号
     */
    private AtomicInteger ticketNum = new AtomicInteger();
    /**
     * 新增一个ThreadLocal,用于存储每个线程的排队号
     */
    private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>();
    public void lock() {
        int currentTicketNum = ticketNum.incrementAndGet();
        // 获取锁的时候,将当前线程的排队号保存起来
        ticketNumHolder.set(currentTicketNum);
        while (currentTicketNum != serviceNum.get()) {
            // Do nothing
        }
    }
    public void unlock() {
        // 释放锁,从ThreadLocal中获取当前线程的排队号
        Integer currentTickNum = ticketNumHolder.get();
        serviceNum.compareAndSet(currentTickNum, currentTickNum + 1);
    }
}
锁升级

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了”偏向锁“和”轻量级锁“,所以在jdk1.6里锁一共有四种状态

  1. 无锁状态
    
  2. 偏向锁状态
    
  3. 轻量级锁状态
    
  4. 重量级锁装填
    
    它会随着竞争情况逐渐升级。锁可以升级但不能降级。
偏向锁
轻量级锁
重量级锁

TODO
参考:
https://blog.csdn.net/zqz_zqz/article/details/70233767
https://smallbug-vip.iteye.com/blog/2275743
https://segmentfault.com/a/1190000015795906

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值