面试复习-Java锁

原帖 Java中的各种锁 - 陈晨辰 - 博客园 (cnblogs.com)

锁分类:

是否需要对资源加锁:悲观锁和乐观锁

资源已被锁定,线程是否阻塞:自旋锁

多个线程并发访问资源,Synchronizd:无锁,偏向锁,轻量级锁,重量级锁

锁的公平性:公平锁,非公平锁

锁是否可重复获取:可重入锁,不可重入锁

多个线程是否可获取同一把锁:共享锁,排他锁

是否需要对资源加锁

悲观锁

持有数据时总会把把资源锁住,行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。

Synchronized 和 ReentrantLock 等独占锁(排他锁)也是一种悲观锁思想的实现

乐观锁

读取不会上锁,写入操作的时候会判断当前数据是否被修改过。通过版本号机制 和 CAS实现 。乐观锁多适用于多读的应用类型,提高吞吐量。

原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

悲观锁因为对读写都加锁,所以它的性能比较低

乐观锁实现方式

版本号机制:在数据表中加上一个 version 字段来实现的,表示数据被修改的次数

CAS算法:无锁算法,非阻塞同步,一些原子类都使用 CAS 作为其实现方式

CAS 中涉及三个要素:
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B

当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

问题:

1.ABA问题

如果一个变量第一次读取的值是 A,准备好需要对 A 进行写操作的时候,发现值还是 A,那么这种情况下,能认为 A 的值没有被改变过吗?可以是由 A -> B -> A 的这种情况

解决方法:增加计数器,计数被修改的次数

2.循环开销大

乐观锁在进行写操作的时候会判断是否能够写入成功,如果写入不成功将触发等待 -> 重试机制,这种情况是一个自旋锁,简单来说就是适用于短期内获取不到,进行等待重试的锁,它不适用于长期获取不到锁的情况,另外,自旋循环对于性能开销比较大

CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)

资源已被锁定,线程是否阻塞

自旋锁

当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)

若持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗

但等待时间太长的话,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度,解决这种情况一个很好的方式是给自旋锁设定一个自旋时间,等时间一到立即释放自旋锁

实现:

public class SpinLockTest {

    private AtomicBoolean available = new AtomicBoolean(false);

    public void lock(){

        // 循环检测尝试获取锁
        while (!tryLock()){
            // doSomething...
        }

    }

    public boolean tryLock(){
        // 尝试获取锁,成功返回true,失败返回false
        return available.compareAndSet(false,true);
    }

    public void unLock(){
        if(!available.compareAndSet(true,false)){
            throw new RuntimeException("释放锁失败");
        }
    }

}

无法保证多线程竞争的公平性

实现排队自旋锁,如TicketLock,MCSLock,CLHLock

(详解:Java中的各种锁 - 陈晨辰 - 博客园 (cnblogs.com)

多个线程并发访问资源

针对 synchronized 关键字设置了四种状态,它们分别是:无锁、偏向锁、轻量级锁和重量级锁

---------------------------------------------------------------------------------------------------------------------------------

Java对象头:主要包括两部分数据:Mark Word(标记字段) 和 class Pointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

class Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  • 无状态也就是无锁的时候,对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01
  • 偏向锁 中划分更细,还是开辟25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01
  • 轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00
  • 重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11
  • GC标记开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。
    其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

---------------------------------------------------------------------------------------------------------------------------------

无锁

没有对资源进行锁定,所有的线程都可以对同一个资源进行访问,但是只有一个线程能够成功修改资源。

无锁的特点就是在循环内进行修改操作,线程会不断的尝试修改共享资源,直到能够成功修改资源并退出,在此过程中没有出现冲突的发生

偏向锁

大多数情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能

轻量级锁

当前锁是偏向锁的时候,资源被另外的线程所访问,那么偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

重量级锁

自旋锁尝试获取锁的次数达到阈值时,锁会进入重量级模式。重量级锁采用操作系统的互斥量实现

锁的公平性与非公平性

公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁

使用ReetrantLock实现公平性

新建一个ReetrantLock对象,传入参数true

public class MyFairLock extends Thread{

    private ReentrantLock lock = new ReentrantLock(true);
    public void fairLock(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()  + "正在持有锁");
        }finally {
            System.out.println(Thread.currentThread().getName()  + "释放了锁");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyFairLock myFairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "启动");
            myFairLock.fairLock();
        };
        Thread[] thread = new Thread[10];
        for(int i = 0;i < 10;i++){
            thread[i] = new Thread(runnable);
        }
        for(int i = 0;i < 10;i++){
            thread[i].start();
        }
    }
}

参数传入后

public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

true:创建一个 ReentrantLock 的公平锁,然后并创建一个 FairSync ,FairSync 其实是一个 Sync 的内部类,它的主要作用是同步对象以获取公平锁

false:非公平锁

---------------------------------------------------------------------------------------------------------------------------------

ReentrantLock 是一把可重入锁,也是一把互斥锁,它具有与 synchronized 相同的方法和监视器锁的语义,但是它比 synchronized 有更多可扩展的功能。

ReentrantLock 的可重入性是指它可以由上次成功锁定但还未解锁的线程拥有。当只有一个线程尝试加锁时,该线程调用 lock() 方法会立刻返回成功并直接获取锁。如果当前线程已经拥有这把锁,这个方法会立刻返回

---------------------------------------------------------------------------------------------------------------------------------

锁是否可重入

可重入锁与不可重入锁

可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java 中 ReentrantLock 和synchronized 都是可重入锁,可重入锁的一个优点是在一定程度上可以避免死锁

例:

private synchronized void doSomething(){
  System.out.println("doSomething...");
  doSomethingElse();
}

private synchronized void doSomethingElse(){
  System.out.println("doSomethingElse...");
}

doSomething() 方法中调用了 doSomethingElse() 方法,因为 synchronized 是可重入锁,所以同一个线程在调用 doSomething() 方法时,也能够进入 doSomethingElse() 方法中

如果 synchronized 是不可重入锁的话,那么在调用 doSomethingElse() 方法的时候,必须把 doSomething() 的锁丢掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁

也就是说,不可重入锁会造成死锁

多个线程能共享同一把锁

独占锁与共享锁

独占锁又叫做排他锁,是指锁在同一时刻只能被一个线程拥有,其他线程想要访问资源,就会被阻塞

JDK 中 synchronized和 JUC 中 Lock 的实现类就是互斥锁

共享锁指的是锁能够被多个线程所拥有,如果某个线程对资源加上共享锁后,则其他线程只能对资源再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据

ReadLock和WriteLock,一个读锁一个写锁,合在一起是读写锁,ReadLock和WriteLock是靠内部类Sync实现的锁,Sync继承于AQS子类

读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独占锁

读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值