Java锁

java的锁机制

Java的锁按照是否锁住同步资源可以分为两种:

悲观锁和乐观锁
  1. 悲观锁认为自己在使用数据的时候一定会有别的线程来修改数据,所以他会在自己操作数据的时候先加锁,确保数据不会被别的线程修改。我们Java中的synchronized和lock的实现类都是悲观锁。
  2. 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

乐观锁在我们Java中通常采用的是无锁编程来实现的,最常用的是CAS算法, Java原子类中的递增操作就通过CAS自旋实现的。

悲观锁一般多用于写操作多的时候,先加锁可以确保我们的数据进行写操作时正确。

乐观锁一般多用于读操作的时候, 不加锁的特点能够使其读操作的性能大幅提升

什么是CAS

CAS是compare and swap的缩写,即我们所说的比较交换。

CAS有三个基本的操作数:内存位置(V)、旧预期值(E)、新值(N)

首先当我们的线程修改值之前,会去内存地址V里面的值是否与旧预期值一样,一样的话就把内存里面的值更新为B不一样的话就会提交失败。然后该线程会把旧的预期值更新到自己的内存位置,又重新来一次上面说到的操作,这个重新尝试的过程就是我们常常天听到的自旋。

自旋锁和适应性自旋锁

  • 自旋锁:当一个线程在获取锁的时候,如果锁已经被其他线程获取,那个该线程将会循环等待,然后不断的判断是否可以成功获取锁,知道获取到锁才会退出循环。CAS算法就用到了自旋锁
  • 适应性自旋锁:JDK1.6 对自旋进行了改进,引入了自适应自旋锁,伴随着程序的运行·和性能的监控,JVM会对锁的情况进行预测,从而给出适合的自旋时间,更加智能。

在这里插入图片描述

自旋锁可能引发的问题:

1、如果某个线程持有的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

2、非公平的情况下,无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

注意事项:

1、自旋锁使CPU处于等待状态,因此临界区执行时间应该尽量短。

2、并发写、竞争激烈的场景下,资源冲突概率高,一般尽量少用自旋锁。

独占锁和共享锁

  • 独占锁(互斥锁):同时只能有一个线程获得锁。例如synchronized、ReentrantLock
  • 共享锁:可以有多个线程同时获得锁。ReadWriteLock中的写锁是独占的,读锁是共享的。Semaphore(信号量)、CountDownLatch都是共享锁。

synchronized(悲观锁,同步锁)

synchronized是IVM层面的锁,他是JAVA的一个关键字,通过monitor对象来完成。使用过后会自动的释放锁,而且他不能中断,只有当抛出异常或者执行结束之后才会中断。他是一种互斥锁。并且是不公平锁。

synchronized的实现涉及到了锁的升级,分为无锁、偏向锁、轻量锁、重量锁

ReentrantLock

ReentrantLock是基于AQS框架实现的,用CAS的自旋实现了原子性,用volatile实现了可见性,是JDK中的一种线程并发访问的同步手段。是一种独占锁具备如下特点

  • 可中断 可通过 trylock(long timeout,TimeUnit unit) 设置超时方法;或者将 lockInterruptibly() 放到代码块中

  • 可以设置超时时间

  • 可以设置是否为公平锁

    //true-公平策略 false非公平策略

    public ReentrantLock(boolean fair) {

    sync = fair ? new FairSync() : new NonfairSync();

    }

  • 支持多个条件变量

  • 支持可重入

ReentranReadWriteLock

分为读锁和写锁,读锁为共享锁,允许多个线程持有,写锁为独占锁只允许一个线程持有,并且,读写,写读,写写都是互斥的。

  • 读锁的获取实际定义在内部同步器Sync的tryAcquire方法中

  • 读锁的释放在内部同步器Sync的tryReleaseShared方法中

  • 写锁的获取实际定义在内部同步器Sync的tryAcquire方法中

  • 写锁的释放实际定义在内部同步器Sync的tryRelease方法中

锁降级:写锁降级成为读锁。持有写锁的同时,获取到读锁,然后释放写锁。避免读到被其他线程修改的数据。

 /**
 * @Description  锁降级
 */
public class CachedData {
    Object data;
    //volatile修饰,保持内存可见性
    volatile boolean cacheValid;
    //可重入读写锁
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        //首先获取读锁
        rwl.readLock().lock();
        //发现没有缓存数据则放弃读锁,获取写锁
        if (!cacheValid) {
            // 在获取写锁之前必须释放读锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // 重新检查状态,因为另一个线程可能有
                // 在执行操作之前获取了写锁定并更改了状态。
                if (!cacheValid) {
                    data = new Object();//数据赋值
                    cacheValid = true;
                }
                // 通过在释放写锁之前获取读锁来降级
                rwl.readLock().lock();
            } finally {
                //进行锁降级
                rwl.writeLock().unlock();
                // 解锁写入,仍保持读取状态
            }
        }
        try {
            //使用数据
            //use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}

ReentranReadWriteLock 会造成线程饥饿问题。 在读线程非常多,写线程很少的情况下,****很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。

StampedLock

jdk 1.8 新加入的锁,可以解决ReentranReadWriteLock的锁饥饿问题,因为它内部有三个锁

readLock() 悲观读

tryOptimisticRead() 乐观读 (其实内部是无锁)

writeLock() 写锁

它的 悲观读和写锁与ReentranReadWriteLock相似,不同在于乐观锁

StampedLock提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有写操作都被阻塞

class Point {
    private int x, y;
    final StampedLock sl = new StampedLock();
 
    //计算到原点的距离  
    double distanceFromOrigin() {
        // 乐观读
        long stamp = sl.tryOptimisticRead();
        // 读入局部变量,
        // 读的过程数据可能被修改
        int curX = x, curY = y;
        //判断执行读操作期间,
        //是否存在写操作,如果存在,
        //则sl.validate返回false
        if (!sl.validate(stamp)) {
            // 升级为悲观读锁
            stamp = sl.readLock();
            try {
                curX = x;
                curY = y;
            } finally {
                //释放悲观读锁
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(curX * curX + curY * curY);
    }
}

synchronized和ReentrantLock的区别

  • synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
  • synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
  • synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
  • synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
  • 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;
  • ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
  • synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;

死锁是什么?遇到死锁问该怎么解决?

死锁:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。

(1)产生死锁的4个必要条件:
互斥条件:所谓互斥就是线程在某一个时间内独占资源
请求与保持条件:⼀个线程因请求资源⽽阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得资源,在未使用完之前,不能强行剥夺。
循环等待条件:若⼲线程之间形成⼀种头尾相接的循环等待资源关系。
(2)如何避免线程死锁?
产⽣死锁的四个必要条件,为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。

破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件 :⼀次性申请所有的资源。
破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
避免死锁最简单的方法就是 阻止循环等待条件 ,将系统中所有的资源设置标志位、排序,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值