锁分类

锁分类

加锁方式

synchronized和lock的方式。
两者的区别的话:

  1. synchronized是jvm层面的,锁的释放在代码块执行完成后,自动释放
  2. Lock加锁解锁是在代码层面实现的,都需要手动的实现,否则会出现死锁的情况。所以锁的释放一定要放在finally块中,避免锁一直占用资源
  3. Lock提供了很多的方法,比如查看线程是否获得了锁,tryLock()尝试获取锁等,操作起来比synchronized更加的灵活。

ReenTrantLock,readLock,writeLock

以上三种分别对应Lock可重入锁,读锁,写锁。

  1. ReentrantLock是Lock锁的具体实现
  2. readLock读锁,多个线程可以同时获取读锁相互之间不影响,但是不能读锁和写锁共存。读锁不能可重入写锁,避免产生死锁的情况(个人理解应该是读锁允许多个线程同时拥有,避免多个线程同时进入写锁造成同时存在两个线程同时获取写锁吧)
  3. writeLock写锁,同一时刻只能有一个线程获取写锁,且不允许有读锁同时存在。但是写锁可以重入读锁,但是依旧不能违背读写不能共存的原则。

公平锁非公平锁对读写锁的影响

公平锁就是 所有线程 对于锁先到先得,不允许插队,非公平锁是允许插队,但是并不是可以随便的插队,需要满足一定的条件。
举例一种情况:
Thread1在进行读锁,Thread2要进行写锁,但是读写互斥,所以线程Thread2加入阻塞队列,Thread3此时进来,要进行读锁,但是排在他前面的Thread2正在阻塞,公平锁的情况下,Thread3也要进行阻塞队列,但是若能将Thread3插入到Thread2之前,便可以一起与Thread1进行读锁。但是这样的话,若是线程量特别多的情况下,会不断地有其他读锁插入到Thread2之前,造成Thread2迟迟得不到锁,从而造成饥饿。

所以为了避免饥饿问题产生 ReentrantReadWriteLock采用下面的非公平锁方式。
Thread1进来执行读锁,此时Thread2进来进行写锁,读写冲突进入阻塞队列。
然后Thread3进来想要操作读锁,但是此时在他前面阻塞队列头部是Thread2写锁,为了避免饥饿产生,Thread3也进入阻塞队列不去插队。但是若是阻塞对列的头部是读锁(Thread2是读锁),那么便允许插队。

总结:
公平锁情况下,不允许插队
非公平锁情况下:
写锁允许插队(读读互斥,读写互斥,插队也是白插,插了也不能执行,那就进入阻塞队列排队)
读锁只有阻塞队列头部是读锁的情况下才能插队

锁升级降级

锁升级->读锁升级为写锁:事实上是不允许升级的
锁降级->写锁降级为读锁:允许
读锁不能升级为写锁,主要目的是避免死锁。因为读锁的多个线程同时获取的,在持有读锁的情况下,两个线程在某一时刻想要同时降级到写锁,但是因为读写互斥,所以就可能出现两个线程同时等待对方释放写锁,进而造成死锁。

public void read(){
        readLock.lock();
        try {
            System.out.println("读锁开始");
            System.out.println("读锁结束");
            System.out.println("读锁开始尝试获取写锁,看看能否获取成功,事实证明,写锁可以重入读锁,但是读锁不能重入写锁");
            write1();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readLock.unlock();
        }
    }

public void write(){
        writeLock.lock();
        try {
            System.out.println("写锁开始");
            System.out.println("写锁结束");
            System.out.println("写锁开始调用读锁,事实证明可以调用成功···");
            read();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            writeLock.unlock();
        }
    }

乐观锁悲观锁

copy知乎
锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。

悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。

乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。

悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。
问题:偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
还有人对此有疑惑,我之前确实没有描述清楚,其实在升级为轻量级锁之前,虚拟机会让线程A尽快在安全点挂起,然后在它的栈中“伪造”一些信息,让线程A在被唤醒之后,认为自己一直持有的是轻量级锁。如果线程A之前正在同步代码块中,那么线程B自旋等待即可。如果线程A之前不在同步代码块中,它会在被唤醒后检查到这一情况并立即释放锁,让线程B可以拿到。这部分内容我之前也没有深入研究过,如果有说的不对的,请多多指教啊!

可中断锁(相应中断)

synchronized是不可中断锁,Lock实现类都是可中断锁
Lock中的可中断锁方法

/* Lock接口 */
public interface Lock {

    void lock(); // 拿不到锁就一直等,拿到马上返回。

    void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

    boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

    void unlock();

    Condition newCondition();
}

可中断锁即一个线程向另一个线程发出中断信号,要求另一个线程中断,但是并不是立即的中断,被发出中断信号的线程会在一定时机进行中断。如Interrupt,sleep方法等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值