synchronized缺陷及优化解决(自定义显示锁)

本文详细解析了synchronized关键字的缺陷,并通过自定义Lock接口,实现了具备可中断和可控制阻塞时长的锁机制。文章通过具体代码示例,展示了如何通过线程间的状态检查和异常处理,实现更灵活的线程同步。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先回顾一下synchronized缺陷:1、无法控制阻塞时长   2、阻塞不可中断,验证代码如下

public class SynchronizedDefect {

    public  void method(){
        synchronized(this){
            try {
                System.out.println(Thread.currentThread().getName() + "进来了");
                TimeUnit.SECONDS.sleep(10);
                System.out.println(Thread.currentThread().getName() + "结束了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException{

        SynchronizedDefect defect = new SynchronizedDefect();
        Thread t1 = new Thread(defect::method,"线程一");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread t2 = new Thread(defect::method,"线程二");
        t2.start();
        Thread t3 = new Thread(defect::method,"线程三");
        t3.start();
        System.out.println(t1.getState());//TIMED_WAITING
        System.out.println(t2.getState());//BLOCKED
        System.out.println(t3.getState());//BLOCKED
        t2.interrupt();// interupt相当于提前中断了t2的阻塞,会修改interupt标识,所以一进入该休眠方法立即进入异常
    }
//简单分析:启动三个线程分别调用同步方法,同步方法,首先线程一启动,进入睡眠,另外两个线程进入阻塞状态,之后可能线程二可能线程三抢到锁,假设线程二抢到则直接报错(因为你修改了interrupt标识),
//总结:假设有一个线程进入了同步方法,另一个线程在外等候,你试图利用t2.interropt修改打断标识,让其捕获以此打断阻塞的线程二,发现你并不能打断阻塞线程,只能干巴巴的等到他抢到锁。

}

接下来来实现具备可中断并且可控制阻塞时长的lock接口锁的实现

第一步定义接口Lock:

public interface Lock {
    void lock() throws InterruptedException;//可被中断的方法
    void lock(long timeout) throws  InterruptedException, TimeoutException;//可被中断,并且增加了超时的功能
    void unlock();//解锁
    List<Thread> getBlockThreads();//用来获取当前阻塞线程
}

第二步实现接口:

public class BoolLock implements Lock{

    private Thread currentThread;//当前拥有锁的线程
    private Boolean locked = false;
    private final List<Thread> lockList = new ArrayList<>();//存储阻塞线程

    @Override
    public void lock() throws InterruptedException {
        synchronized (this){
            //其它线程能进入此处,因为这不是关联对象调用此方法,而是另一个测试类的方法引用此方法
            while (locked){
                lockList.add(Thread.currentThread());//孤儿线程  阻塞队列线程
                this.wait();//该线程放弃关联引用对象的monitor的所有权,并释放monitor的所有权给其它线程争抢的机会
            }
            System.out.println("当前线程获得锁" + Thread.currentThread().getName());
            lockList.remove(Thread.currentThread());//如果抢到锁了,则从阻塞队列中删除自己(如果没有返回false)
            locked = true;
            this.currentThread = Thread.currentThread();//拿到锁的线程
        }
    }

    @Override
    public void lock(long timeout) throws InterruptedException, TimeoutException {
        synchronized (this){
            if(timeout <= 0){
                try {
                    throw new TimeException();
                } catch (TimeException e) {
                    e.printStackTrace();
                }
            }else {
                long remainMills = timeout;
                long endMills = System.currentTimeMillis() + remainMills;
                while (locked){
                    if (remainMills <= 0){
                        throw  new TimeoutException("当前线程被唤醒或者指定时间wait到了还没获得锁");
                    }
                    if(!lockList.contains(Thread.currentThread())){
                        lockList.add(Thread.currentThread());
                    }
                    this.wait(remainMills);
                    remainMills = endMills - System.currentTimeMillis();//剩余时间
                }
                System.out.println("当前获得锁的线程:"+Thread.currentThread().getName());
                lockList.remove(Thread.currentThread());
                this.locked = true;
                this.currentThread = Thread.currentThread();
            }
        }
    }

    @Override
    public void unlock() {
        synchronized (this){
            //获得锁的线程解锁
            if (currentThread == Thread.currentThread()){
                this.locked = false;
                System.out.println("当前线程解锁" + Thread.currentThread().getName());
                this.notify();//唤醒睡眠线程强锁   这里唤醒一个也可以
            }
        }
    }
    @Override
    public List<Thread> getBlockThreads() {
        return Collections.unmodifiableList(lockList);
    }
}

涉及到的自定义异常TimeException类:

public class TimeException extends  Exception{
    @Override
    public void printStackTrace() {
        System.out.println("您输入的时间小于0");
    }
}

第三步测试类调用:

public class Test {
    private final  Lock lock = new BoolLock();

    public void test(){
        try {
            lock.lock();//这里是Test类调用同步方法
            TimeUnit.SECONDS.sleep(5);//谁调用休眠的方法,谁将睡眠5s,这里指获得锁的线程,因为孤儿线程还是waitting状态,不会往下走
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(lock.getBlockThreads());//在解锁前,输出阻塞队列
            lock.unlock();
        }
    }

    public static void main(String[] args) throws  InterruptedException{
        Test test = new Test();
        //启动五个线程,分别去调用另一个类BolckLock的同步方法,观察线程输出打印信息,这一点与syncronized同步非常类似,接下来验证可中断的特性
        IntStream.range(0,5).mapToObj(value -> new Thread(test::test)).forEach(Thread::start);
        
    }

}

这里为止就已经可以发现已经具备了syncronized关键字类似的功能,线程同步并且一次获取锁和解锁,接下来修改一下main方法,继续观察,实现可中中断,继而避免了syncronized不能打断阻塞线程

 public static void main(String[] args) throws  InterruptedException{
        Test test = new Test();
        Thread t1 = new Thread(test::test,"T1线程");
        t1.start();
        TimeUnit.MILLISECONDS.sleep(100);//睡眠10毫秒确保T2后启动
        Thread t2 =  new Thread(test::test,"T2线程");
        t2.start();
        System.out.println("T1状态" + t1.getState() + "T2状态" + t2.getState() );
        t2.interrupt();//wait和sleep都能接受到中断异常
    }
-----------------------输出信息-----------------------
当前线程获得锁T1线程
T1状态TIMED_WAITING,T2状态RUNNABLE
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at Lock.BoolLock.lock(BoolLock.java:20)
	at Lock.Test.test(Test.java:13)
	at java.lang.Thread.run(Thread.java:748)
[Thread[T2线程,5,main]]//T2线程直接进入finally因为不是解锁线程,所以只输出了一句,T2生命周期结束
[Thread[T2线程,5,]]//T1是解锁线程既输出了阻塞队列,又输出了解锁方法的打印信息
当前线程解锁T1线程
-----------------------修改后输出信息-----------------------
前线程获得锁T1线程
T1状态TIMED_WAITING,T2状态WAITING
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at Lock.BoolLock.lock(BoolLock.java:21)
	at Lock.Test.test(Test.java:13)
	at java.lang.Thread.run(Thread.java:748)
[Thread[T2线程,5,main]]
当前线程解锁T1线程
当前线程获得锁T2线程  //因为catch获取了异常,即这里可以抓住异常,依然会执行内部的循环 
[]
当前线程解锁T2线程

但是此时还存在一个问题,就是阻断的线程还在阻塞的队列中,没有清除,需要将lock()覆写的代码稍加修改,在阻塞线程中加入一个异常捕获即可

 @Override
    public void lock() throws InterruptedException {
        synchronized (this){
            //其它线程能进入此处,因为这不是关联对象调用此方法,而是另一个测试类的方法引用此方法
            while (locked){
               try {
                    lockList.add(Thread.currentThread());//孤儿线程  阻塞队列线程
                    this.wait();//该线程放弃关联引用对象的monitor的所有权,并释放monitor的所有权给其它线程争抢的机会
                } catch (InterruptedException e) {
                    lockList.remove(Thread.currentThread());//如果当前阻塞线程被打断,则移除避免内存泄漏
                    e.printStackTrace();
                }
            }
            System.out.println("当前线程获得锁" + Thread.currentThread().getName());
            lockList.remove(Thread.currentThread());//如果抢到锁了,则从阻塞队列中删除自己(如果没有返回false)
            locked = true;
            this.currentThread = Thread.currentThread();//拿到锁的线程
        }
    }

上面已经讲解过了提前中断阻塞线程的方法,现在说一下可超时的情况:假设某一个线程抢到锁需要5s睡眠,而其它线程在外面等待,假设我设定其其它线程的等待时间,每个人给它1s,一旦超过1s,则集体罢工。

正常情况:等待时间 >= 5s,则我这里不会进入超时,因为你等待的时间一直被重置,但是重置期间另外一个线程已经走完了,你还在等待中,允许你有时间去拿锁。

等待时间<5s,则会进入超时异常,直接结束掉程序,目的是为了减少过长时间的滞留。修改了一下lock(long timeout)方法

BlockLock类可超时的方法,不必等拿到锁的线程过长时间
 @Override
    public void lock(long timeout) throws InterruptedException, TimeoutException {
        synchronized (this){
            if(timeout <= 0){
                try {
                    throw new TimeException();
                } catch (TimeException e) {
                    e.printStackTrace();
                }
            }else {
                long remainMills = timeout;
                long endMills = System.currentTimeMillis() + remainMills;
                while (locked){
                    if (remainMills <= 0){
                        lockList.remove(Thread.currentThread());
                        throw  new TimeoutException("当前线程被唤醒或者指定时间wait到了还没获得锁");//抛出异常:下面代码不执行
                    }
                    if(!lockList.contains(Thread.currentThread())){//时间复杂度o(n)
                        lockList.add(Thread.currentThread());
                    }
                    this.wait(remainMills);
                    remainMills = endMills - System.currentTimeMillis();//剩余时间
                }
                System.out.println("当前获得锁的线程:" +Thread.currentThread().getName());
                lockList.remove(Thread.currentThread());
                this.locked = true;
                this.currentThread = Thread.currentThread();
            }
        }
    }

//Test类的方法
 public void test(){
        try {
            lock.lock(1000);//这里是Test类调用同步方法
            TimeUnit.SECONDS.sleep(5);//谁调用休眠的方法,谁将睡眠5s,这里指获得锁的线程,因为孤儿线程还是waitting状态,不会往下走
        } catch (Exception e) {
                e.printStackTrace();
        } finally {
            System.out.println(lock.getBlockThreads());//在解锁前,输出阻塞队列
            lock.unlock();
        }
    }

    public static void main(String[] args) throws  InterruptedException,TimeoutException{
        Test test = new Test();
        //启动五个线程,分别去调用另一个类BolckLock的同步方法,观察线程输出打印信息,这一点与syncronized同步非常类似,接下来验证可中断的特性
        //IntStream.range(0,5).mapToObj(value -> new Thread(test::test)).forEach(Thread::start);

        Thread t1 = new Thread(test::test,"T1线程");
        t1.start();
        TimeUnit.MILLISECONDS.sleep(100);//睡眠10毫秒确保T2后启动
        Thread t2 =  new Thread(test::test,"T2线程");
        t2.start();
        System.out.println("T1状态" + t1.getState() + ",T2状态" + t2.getState());
        //t2.interrupt();//wait和sleep能接受中断异常
    }
--------------控制台信息-------
当前获得锁的线程:T1线程
T1状态TIMED_WAITING,T2状态RUNNABLE
java.util.concurrent.TimeoutException: 当前线程被唤醒或者指定时间wait到了还没获得锁
	at Lock.BoolLock.lock(BoolLock.java:49)
	at Lock.Test.test(Test.java:15)
	at java.lang.Thread.run(Thread.java:748)
[] //T2线finally打印的,因为不是锁线程就没打印解锁
[]//T1线程打印
当前线程解锁T1线程

 

<think>嗯,用户现在想了解乐观和悲观的概念、使用场景,以及在Spring中的实现方式和区别。让我先回顾一下之前的对话和提供的引用内容,确保回答准确。 首先,用户之前问过Spring循环依赖的问题,我回答了三级缓存、解决流程和相关方案。现在用户转向了机制,特别是乐观和悲观。需要结合提供的引用内容来构建答案。 根据引用[1],乐观和悲观的基本概念已经提到,尤其是CAS算法和synchronized优化。引用[2]到[5]详细说明了在Spring和JPA中的实现方式,包括使用版本号的乐观和数据库机制的悲观,还有事务隔离级别的相关点。 用户的问题包括概念、使用场景、Spring实现方式和区别。需要分部分回答,结构要清晰。先解释两种的概念,然后对比它们的区别,接着在Spring中的具体实现方法,最后适用场景。同时,需要引用提供的资料中的例子,比如JPA的@Version注解和LockModeType的使用。 需要注意的是,用户可能对如何在Spring项目中实际应用这些机制感兴趣,所以代码示例会很有帮助。例如,引用[3]中的User实体类使用@Version,以及查询时加的方法。同时,引用[2]提到Spring Data JPA的EntityManager和JpaTemplate的使用,这也是实现悲观的方式。 另外,用户可能对事务隔离级别和的关系有疑问,比如引用[5]提到的事务隔离级别设置对的影响,需要简要说明。不过用户的主要问题集中在本身,所以这部分可能作为补充信息。 需要确保回答覆盖以下几点: 1. 定义和核心思想 2. 实现方式(包括Spring/JPA的具体方法) 3. 优缺点对比 4. 使用场景 5. 可能的问题和注意事项 还要注意引用标记,比如在提到JPA版本号时添加[^3],在数据库机制时引用[^2][^4]。同时,避免重复引用,保持引用标注正确。 现在,组织内容。先讲乐观,再悲观,然后对比,最后使用场景。每个部分包含概念、实现、代码示例(如果有)、优缺点。确保语言简洁,技术点明确,符合用户的中文提问需求。 检查是否有遗漏的信息,比如synchronized在Java中的演变,虽然用户的问题集中在Spring,但可能也需要简要提及,但根据引用[1],可以提到synchronized优化,但重点还是在Spring的实现。 最后,生成相关问题,可能包括的选择建议、性能影响、在分布式系统中的处理等,但要根据引用内容中的点来设计,比如CAS的缺点、版本号冲突处理等。</think>--- ### 乐观与悲观核心解析 #### **一、基本概念对比** | 特性 | 乐观 | 悲观 | |-------------------|----------------------------------------------------------------------|----------------------------------------------------------------------| | **核心思想** | 假设并发冲突概率低,仅在提交时检测冲突[^3] | 假设并发冲突概率高,操作前直接加[^4] | | **实现方式** | 版本号机制(如JPA的@Version)、CAS算法 | 数据库(SELECT FOR UPDATE)、Java同步synchronized) | | **数据一致性** | 最终一致性 | 强一致性 | | **性能消耗** | 无设计,高并发下吞吐量高 | 竞争导致性能下降 | --- #### **二、Spring中的实现方式** ##### **1. 乐观实现** **JPA版本号机制**(推荐方案)[^3] ```java @Entity public class Product { @Id @GeneratedValue private Long id; private String name; private Integer stock; @Version // 核心注解,自动管理版本号 private Integer version; } ``` - **更新时自动校验**:若版本号不匹配,抛出`OptimisticLockException` - **适用场景**:电商库存、订单状态等低频冲突业务 ##### **2. 悲观实现** **方案一:数据库(Spring Data JPA)**[^2] ```java @Repository public class ProductRepository { @PersistenceContext private EntityManager em; public Product findWithLock(Long id) { return em.find(Product.class, id, LockModeType.PESSIMISTIC_WRITE); // 加写 } } ``` **方案二:Java同步** ```java @Service public class PaymentService { private final Object lock = new Object(); public void transfer() { synchronized(lock) { // 互斥访问 // 核心业务逻辑 } } } ``` --- #### **三、关键差异分析 1. **冲突处理方式** - 乐观:通过`重试机制`解决冲突(需业务层处理异常)[^3] - 悲观:通过`阻塞等待`避免冲突(由数据库/JVM保证) 2. **事务隔离级别关联** - 乐观通常配合`Read Committed`级别(平衡性能与一致性)[^5] - 悲观需要`Repeatable Read`或`Serializable`级别支持 3. **典型异常类型** | 类型 | 异常类 | 触发条件 | |--------|-------------------------|----------------------| | 乐观 | OptimisticLockException | 版本号或CAS值不匹配 | | 悲观 | LockTimeoutException | 获取超时 | --- #### **四、选型决策树** ```mermaid flowchart TD A{是否需要强一致性?} A --> |是| B[选择悲观] A --> |否| C{冲突频率如何?} C --> |高| D[悲观+事务优化] C --> |低| E[乐观+重试机制] ``` --- #### **五、生产环境建议** 1. **混合使用场景** - 对核心资金操作使用`悲观+@Transactional` - 对商品库存使用`乐观+重试策略`[^4] 2. **性能优化技巧** ```java @Retryable(value = OptimisticLockException.class, maxAttempts = 3, backoff = @Backoff(delay = 100)) public void updateProduct(Product product) { // 带版本号的更新操作 } ``` (使用Spring Retry实现自动重试) 3. **分布式系统扩展** - 乐观:结合Redis实现分布式版本号 - 悲观:使用Redisson分布式 --- ### 相关问题 1. **CAS算法在Java中如何实现?有什么缺陷?** 2. **Spring Retry如何配置自定义重试策略?** 3. **分布式场景下如何选择机制?** 4. **JPA的@Version字段能否使用其他数据类型?** [^1]: Java通过偏向、轻量级优化提升synchronized性能 : Spring Data JPA提供原生悲观支持 [^3]: JPA通过@Version注解实现乐观 [^4]: 高并发场景建议根据业务特性选择机制 : 事务隔离级别设置影响机制的实际效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值