Java进阶(6)——抢购问题中的数据不安全(非原子性问题)& Java中的synchronize和ReentrantLock锁使用 & 死锁及其产生的条件

引出


1.大量请求拥挤抢购中的数据不安全问题;
2.事务ACID:原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability);
3.线程安全特征:原子性(Atomicity)可见性(Visibility)有序性(Ordering);
4.java中的锁初步,synchronize锁和ReentrantLock锁使用初步;
5.滥用锁的问题,以及产生死锁的条件;

场景:大量请求拥挤抢购

在这里插入图片描述

在这里插入图片描述

package com.tianju.redis.service.impl;

import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class RushGoodsServiceImpl implements IRushGoodsService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String GOODS = "goods";

    @Override
    public String rush() {
        String sNum = stringRedisTemplate.opsForValue().get(GOODS);
        int nums = Integer.parseInt(sNum);
        if (nums>0){
            stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(--nums) );
            return stringRedisTemplate.opsForValue().get(GOODS);
        }else {
            return "error";
        }
    }
}
    @PutMapping("/rushJmeter")
    public void rushJmeter(){
        String goodsNum = rushGoodsService.rush();
        System.out.println("goodsNum: "+goodsNum);
    }

在这里插入图片描述

事务的基本特征ACID

事务是指一组操作被视为一个不可分割的工作单元,要么全部执行成功,要么全部不执行。事务具有以下四个基本特征,通常被称为ACID特性:

在这里插入图片描述

  • 原子性(Atomicity):事务是一个原子操作,要么全部执行成功,要么全部不执行。如果事务中的任何一个操作失败,整个事务将被回滚到初始状态,不会对数据库产生任何影响。

  • 一致性(Consistency):事务在执行前和执行后,数据库的状态必须保持一致。这意味着事务中的操作必须满足数据库的完整性约束,包括唯一性约束、外键约束等。

  • 隔离性(Isolation):事务的执行是相互隔离的,一个事务的操作不会被其他事务所干扰。隔离性确保了并发执行的事务之间不会产生不一致的结果。

  • 持久性(Durability):一旦事务提交成功,其所做的修改将永久保存在数据库中,即使系统发生故障或重启,修改的结果也不会丢失。

这些特性确保了事务的可靠性和一致性。数据库管理系统通过使用日志和锁等机制来实现事务的特性。在设计和实现数据库应用程序时,需要考虑事务的边界和正确使用事务来保证数据的完整性和一致性

线程安全的基本特征

线程安全是指在多线程环境下,对共享资源的访问和操作不会导致数据不一致或产生不可预期的结果。线程安全的基本特征包括:

  • 原子性(Atomicity):对共享资源的操作要么全部执行成功,要么全部不执行,不存在中间状态。即使在多线程环境下,也能保证操作的完整性。简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现

  • 可见性(Visibility):一个线程对共享资源的修改对其他线程是可见的。当一个线程修改了共享资源的值后,其他线程能够立即看到最新的值。可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。

  • 有序性(Ordering):线程的执行顺序与程序的代码顺序一致。即使在多线程环境下,也能保证操作按照预期的顺序执行。是保证线程内串行语义,避免指令重排等。

在这里插入图片描述
解决办法

  • 使用互斥锁(Mutex)或信号量(Semaphore)等同步机制,确保在同一时间只有一个线程能够访问共享资源。

  • 使用原子操作(Atomic Operation)来保证对共享资源的操作是原子的,不会被其他线程中断。

  • 使用volatile关键字来保证共享变量的可见性,确保一个线程对共享变量的修改对其他线程是可见的。

  • 使用线程安全的数据结构或类,这些数据结构或类已经在设计上考虑了多线程环境下的安全性。

加锁(java)

synchronized锁

可重入锁: sychronized ReentrantLock

在这里插入图片描述

synchronized (this.getClass()){}

在这里插入图片描述

ReentrantLock锁

private final ReentrantLock lock = new ReentrantLock(); // 可重入锁

在这里插入图片描述

什么是可重入锁?

当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁;

在这里插入图片描述

在这里插入图片描述

如何保证可重入

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁

如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程。再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放

滥用锁的代价?(死锁)

死锁的四个必要条件

循环 A —> B —>C —>A

产生死锁的必要条件是以下四个条件同时满足:

  • 互斥条件(Mutual Exclusion):至少有一个资源被一个进程独占使用,即在一段时间内只能由一个进程访问。

  • 请求与保持条件(Hold and Wait):一个进程在持有至少一个资源的同时,又请求获取其他进程持有的资源。

  • 不可剥夺条件(No Preemption):资源只能由持有者显式地释放,其他进程无法强制剥夺。

  • 循环等待条件(Circular Wait):存在一个进程资源的循环链,每个进程都在等待下一个进程所持有的资源。

当这四个条件同时满足时,就可能发生死锁。如果任何一个条件不满足,就不会发生死锁。

死锁是多线程或多进程并发执行时的一种常见问题,它会导致系统无法继续执行下去,需要通过死锁检测、死锁预防、死锁避免或死锁解除等方法来处理。

死锁的案例

在这里插入图片描述

可能导致死锁

在这里插入图片描述

锁对象ObjLock

package com.tianju.redis.lock;

public class ObjLock {
    private String name;

    public ObjLock(String name){
        this.name = name;
    }

    @Override
    public String toString() {
        return "ObjLock:"+this.name;
    }
}

加锁释放锁方法DeadLockDemo

package com.tianju.redis.lock;

public class DeadLockDemo {

    private ObjLock a;
    public ObjLock b;

    public DeadLockDemo(ObjLock a,ObjLock b){
        this.a = a;
        this.b = b;
    }

    public void dead(){
        System.out.println("********"+a+"对象"+b+"对象都加锁**************");
        System.out.println(a+"--"+b+": "+"准备给"+a+"对象加锁>>");
        synchronized (a){
            System.out.println(a+"--"+b+": "+a+"对象加锁成功...");
            System.out.println(a+"--"+b+": "+"准备给"+b+"对象加锁>>>");
            synchronized (b){
                System.out.println(a+"--"+b+": "+b+"对象加锁成功");
            }
            System.out.println(a+"--"+b+": "+"释放"+b+"对象的锁");
        }
        System.out.println(a+"--"+b+": "+"释放"+b+"对象的锁");
        System.out.println("****************");
    }
}

测试方法

package com.tianju.redis.lock;

public class TestDeadLock {

    /**
     * 一个一个顺序运行
     */
    public static void run(){
        ObjLock a = new ObjLock("A");
        ObjLock b = new ObjLock("B");
        ObjLock c = new ObjLock("C");

        DeadLockDemo lockDemo1 = new DeadLockDemo(a, b);
        lockDemo1.dead(); // 锁住a和b

        DeadLockDemo lockDemo2 = new DeadLockDemo(b, c);
        lockDemo2.dead(); // 锁住a和b

        DeadLockDemo lockDemo3 = new DeadLockDemo(c, a);
        lockDemo3.dead(); // 锁住a和b
    }

    /**
     * 进行线程抢,死锁
     */
    public static void rushRun(){
        ObjLock a = new ObjLock("A");
        ObjLock b = new ObjLock("B");
        ObjLock c = new ObjLock("C");

        new Thread(()->{
            DeadLockDemo lockDemo1 = new DeadLockDemo(a, b);
            lockDemo1.dead(); // 锁住a和b
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(()->{
            DeadLockDemo lockDemo2 = new DeadLockDemo(b, c);
            lockDemo2.dead(); // 锁住a和b
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(()->{
            DeadLockDemo lockDemo3 = new DeadLockDemo(c, a);
            lockDemo3.dead(); // 锁住a和b
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }


    public static void main(String[] args) {
//        run(); // 顺序执行加锁,解锁
        rushRun(); // 线程进行抢
    }
}


总结

1.大量请求拥挤抢购中的数据不安全问题;
2.事务ACID:原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability);
3.线程安全特征:原子性(Atomicity)可见性(Visibility)有序性(Ordering);
4.java中的锁初步,synchronize锁和ReentrantLock锁使用初步;
5.滥用锁的问题,以及产生死锁的条件;