java中的锁

一、Java中锁分类

在这里插入图片描述

二、乐观锁与悲观锁

1 乐观锁

1.1乐观锁的CAS实现

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLock {
    private AtomicInteger counter;

    public OptimisticLock() {
        this.counter = new AtomicInteger(0);
    }

    public void increment() {
        int oldValue = counter.get();
        int newValue = oldValue + 1;
        
        // CAS自循环机制,当旧值和当前值不等就重新获取当前值进行重新计算,直到值相等跳出循环
        while (!counter.compareAndSet(oldValue, newValue)) {
            oldValue = counter.get();
            newValue = oldValue + 1;
        }
    }

    public int getCounterValue() {
        return counter.get();
    }
}
 

在这个案例中,OptimisticLock类用一个AtomicInteger来表示一个计数器。在increment方法中,我们首先获取当前计数器的旧值,然后计算新的值。然后使用compareAndSet方法比较旧值和当前值,如果相等则更新为新值。如果不相等,则说明其他线程已经修改了计数器的值,需要重新获取旧值并重新计算新值,直到成功更新为止。

这里使用了AtomicInteger类的compareAndSet方法来实现CAS操作,保证了原子性和线程安全性。这个方法返回一个布尔值,表示是否成功更新。

这个例子展示了CAS的典型应用场景,即乐观锁的实现。通过CAS操作,我们可以在无锁的情况下实现并发控制,避免了传统锁的开销和线程阻塞。

1.2 乐观锁的版本号实现

public class OptimisticLockExample {
    private int value;
    private int version;

    public OptimisticLockExample() {
        this.value = 0;
        this.version = 0;
    }

    public void increment() {
        int currentVersion = this.version;
        int newValue = this.value + 1;

        // 模拟更新数据前的一些其它操作

        // 检查版本号是否发生变化
        if (this.version == currentVersion) {
            this.value = newValue;
            this.version++;
        } else {
            // 版本号发生变化,说明数据已经被其他线程修改,抛出异常或进行其他处理
            throw new OptimisticLockException("Concurrent modification detected.");
        }
    }

    public int getValue() {
        return this.value;
    }

    public int getVersion() {
        return this.version;
    }

    public static void main(String[] args) {
        OptimisticLockExample example = new OptimisticLockExample();

        // 创建并启动多个线程对value进行增加操作
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                example.increment();
            }).start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final value: " + example.getValue());
        System.out.println("Final version: " + example.getVersion());
    }
}

class OptimisticLockException extends RuntimeException {
    public OptimisticLockException(String message) {
        super(message);
    }
}
 

通过版本号机制可解决上面CAS实现可能导致的ABA问题

2 悲观锁

1.2 悲观锁的synchronized使用示例

public class PessimisticLockExample {
    private int value;

    public synchronized void increment() {
        // 模拟更新数据前的一些其它操作

        this.value++;
    }

    public int getValue() {
        return this.value;
    }

    public static void main(String[] args) {
        PessimisticLockExample example = new PessimisticLockExample();

        // 创建并启动多个线程对value进行增加操作
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                example.increment();
            }).start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final value: " + example.getValue());
    }
}
 

在上述代码中,PessimisticLockExample类表示一个使用悲观锁控制并发的例子。increment方法使用synchronized关键字修饰,这样在进入该方法前会对示例对象进行加锁,保证同一时间只有一个线程可以访问该方法。在进行自增操作之前,可以进行一些其它操作。最后,对value进行自增操作。

在main方法中,我们创建了多个线程对value进行增加操作。最后,输出最终的value值。

需要注意的是,使用悲观锁的代价较高,因为每次访问共享资源都需要获取锁。在实际应用中,需要根据具体需求和实际情况选择合适的锁策略。

2 乐观锁和悲观锁的区别

区分乐观锁悲观锁
定义态度乐观,认为别的线程不会同时修改数据,所以不会上锁,但是更新前还是会通过比较的方式判断是否有过更新态度悲观,认为别人一定会修改数据,所以上锁,别的线程想拿数据就会自旋或阻塞
实现可以使用 版本号机制和CAS算法 实现(比较和替换是一个原子操作,自旋操作,不断重试)Synchronized,ReentrantLock
场景写多读少,冲突多,这种场景如果是乐观锁的话会不断retry反而增加开销读多写少,冲突少,可以省去锁竞争的开销,增加系统吞吐量

三、Synchronized

定义: synchronized 是Java的一个关键字,一种悲观锁,自jdk1.6后进行了优化,不在像之前那么重量

特点:

  1. 悲观锁,可重入锁
  2. 每个对象锁只能分配给一个线程
  3. 可自动释放锁
  4. 根据锁竞争的激烈程度进行升级

示例:

// 互斥锁,可重入锁,
		synchronized (this){
		    System.out.println("只有一个线程能抢占");
		}

原理: 根据锁竞争的激烈程度存在一个锁升级的过程,由无锁——》偏向锁——》轻量锁——》重量锁

偏向锁

  • 检查到没有人获取到偏向锁的时候,将线程ID存到对象头的MarkWord中,相同线程再次获取锁只需要比较线程id即可

轻量级锁

  • 当有线程已经获取锁(偏向锁),其他线程也想获取的时候,偏向锁转为轻量级锁,将MarkWord存到获得锁的线程栈桢中,原MarkWord的位置存一个指向锁记录的指针,锁记录中也会存一个指向锁对象的指针,这样线程和锁对象都存在指向对方的指针,线程就获取了锁。其他线程进行CAS自旋,自旋一定次数获取到锁,那就是获取到轻量级锁,未获取到,就进入阻塞,升级为重量级锁

重量级锁

  • 多个线程竞争轻量锁,一个线程获取锁,其他线程自旋无果进入阻塞状态,等待获取锁的线程释放锁之后唤醒阻塞的线程,阻塞——》唤醒,内核态——》用户态的切换,所以是重量级锁

注:对象头中存在MarkWord 和 Class MetaData Address共12个字节

MarkWord: 哈希码,GC分代年龄,锁状态,线程持有的锁,线程ID
Class MetaData Address: 存储对象类型指针,指向类元数据
synchronized 锁哪里比较合适,可以锁对象(对象相同被锁),锁类(任何此类对象都会被锁)

四、ReentranLock

定义: 实现lock接口的可重入锁、悲观锁

特点:

  1. 读/读、读/写、写/写 操作都不能同时发生,如果读能够以共享锁的方式进行那么会进一步提升性能
  2. 有两种模式,公平模式与非公平模式,默认是公平模式
  3. 需要手动释放锁,可重入
  4. 可通过tryLock方法确定锁是否可用

方法:

方法释义
boolean tryLock()如果资源未被任何其他线程持有,则调用 tryLock() 返回 true,并且持有计数加 1。如果资源不是空闲的,则该方法返回 false,线程不会被阻塞,而是退出
boolean tryLock(long timeout, TimeUnit timeUnit)线程在退出前等待方法参数定义的一定时间段来获取资源的锁
void lock()调用 lock() 方法将保持计数加 1,如果共享资源最初是空闲的,则将锁分配给线程,如果锁不可用就阻塞直到锁释放
lockInterruptibly()如果资源空闲,则此方法获取锁,同时允许线程在获取资源时被其他线程中断。意思是如果当前线程正在等待锁,但是其他线程请求锁,那么当前线程将被中断并抛出异常
void unlock()调用unlock()方法将持有计数减1。当这个计数达到零时,资源被释放
getHoldCount()此方法返回资源上持有的锁的数量
isHeldByCurrentThread()如果当前线程持有资源的锁,则此方法返回 true

示例:

 // 排他锁,可重入锁,参数为TRUE,表示是公平锁模式
        ReentrantLock reentrantLock = new ReentrantLock(true);
        reentrantLock.lock();
        try {
            System.out.println("不允许 读读 ");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 必须释放锁
            reentrantLock.unlock();
        }

五、ReentrantReadWriteLock

ReentrantReadWriteLock 读写锁(乐观锁): Reentrant(可重入)Read(读)Write(写)Lock(锁)。

定义: 实现lock接口的读写锁,乐观锁

特点:

  1. 读写锁,可以分别获取读锁或写锁。也就是说将数据的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
  2. 读锁使用共享模式;写锁使用独占模式;读锁可以在没有写锁的时候被多个线程同时持有;获得写锁的线程可获得读锁
  3. 需要手动释放锁,可重入
  4. 可通过tryLock方法确定锁是否可用
  5. 有两种模式,公平模式与非公平模式,默认是公平模式
 // 读写锁,可重入锁,参数为TRUE,表示是公平锁模式
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
        try {
            reentrantReadWriteLock.writeLock().lock();
            System.out.println("相比ReentrantLock增加了读的性能");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            reentrantReadWriteLock.writeLock().unlock();
        }

注: synchronized 在执行完可以自动释放,而ReentranLock 必须手动释放,为保证不会死锁,要放到finally中,但是更加灵活

五、问题

1. Synchronized和Reentrantlock区别?

区别项SynchronizedReentrantlock
锁释放自动释放手动释放
灵活性当前类,方法使用可以跨类,跨方法使用
可重入性可重入可重入,需要手动释放加锁次数
响应中断不可响应中断(获取不到锁就一直等待)可中断通过tryLock检测
公平锁不支持支持
适用范围可加在对象,类,代码块上只能加在代码块上

参考博客:
http://t.csdn.cn/LO8Bs
https://blog.csdn.net/m0_50370837/article/details/124471888

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值