Java多线程(八):常见的锁策略

    });

    t1.start();



    // --

    Thread t2 = new Thread(() -> {

        for (int i = 0; i < MAX_COUNT; i++) {

            number--;

        }

    });

    t2.start();



    t1.join();

    t2.join();

    System.out.println("最终结果:" + number);

}

}




 运行结果:



![](https://img-blog.csdnimg.cn/9fc43be5b95e4921977afee978c47803.png)



> 结果并不是预期的0,是线程不安全的。



 **线程安全代码(AtomicInteger):**



public class CASDemo2 {

private static final int MAX_COUNT = 100000;

private static final AtomicInteger atomicInteger = new AtomicInteger(0);



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

    // ++

    Thread t1 = new Thread(() -> {

        for (int i = 0; i < MAX_COUNT; i++) {

            atomicInteger.getAndIncrement();

        }

    });

    t1.start();



    // --

    Thread t2 = new Thread(() -> {

        for (int i = 0; i < MAX_COUNT; i++) {

            atomicInteger.getAndDecrement();

        }

    });

    t2.start();



    t1.join();

    t2.join();

    System.out.println("最终结果:" + atomicInteger.get());

}

}




 运行结果:



![](https://img-blog.csdnimg.cn/a664c610af244b7bbf58c100b2445475.png)



>  结果为0,证明使用了**AtomicInteger**是线程安全的。



###  1.1.4 CAS缺点(ABA问题)



        ABA 转账问题,X 给 Y 转账,系统卡顿点击了两次转账按钮,X 原来是 300,正常是转完账(100元) 还剩下200,第⼀次转账成功之后变成了 200,此时 Z 给 X 转了 100 元,余额又变回了 300,第⼆次CAS 判断(300,300,200)成功,于是又扣了X 100 元,X直接亏了100元。



![](https://img-blog.csdnimg.cn/img_convert/61555b0393b51a650f5101f324b07a32.gif)



 **ABA代码演示:**



public class ABADemo1 {

private static final AtomicInteger money = new AtomicInteger(300);



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

    // 第一次转账点击按钮(-100)

    Thread t1 = new Thread(() -> {

        // 先得到余额

        int oldMoney = money.get();

        // 执行耗时 2s

        try {

            Thread.sleep(2000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        money.compareAndSet(oldMoney, oldMoney - 100);

    });

    t1.start();



    // 第二次点击按钮(-100)(不小心点击的,因为第一次点击完没反应,所以又点了一次)

    Thread t2 = new Thread(() -> {

        int oldMoney = money.get();

        money.compareAndSet(oldMoney, oldMoney - 100);

    });

    t2.start();



    // 给账户 +100

    Thread t3 = new Thread(() -> {

        int oldMoney = money.get();

        // 执行耗时 1s

        try {

            Thread.sleep(1000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        money.compareAndSet(oldMoney, oldMoney + 100);

    });

    t3.start();



    t1.join();

    t2.join();

    t3.join();

    System.out.println("最终余额:" + money.get());

}

}




 运行结果:



![](https://img-blog.csdnimg.cn/0c2d628e0ef54f5e90d38132e2bbf6ca.png)



>  可以看到,这波属实亏大了!



 **解决方案(AtomicStampedReference)**:引入了一个版本号,每次操作后让版本号+1,执行的时候判断版本号和值。



public class ABADemo2 {

private static final AtomicStampedReference<Integer> money = new AtomicStampedReference<>(300, 0);



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

    // 第一次转账点击按钮(-100)

    Thread t1 = new Thread(() -> {

        // 先得到余额

        int oldMoney = money.getReference();

        // 得到版本号

        int version = money.getStamp();

        // 执行耗时 2s

        try {

            Thread.sleep(2000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        money.compareAndSet(oldMoney, oldMoney - 100, version, version + 1);

    });

    t1.start();



    // 第二次点击按钮(-100)(不小心点击的,因为第一次点击完没反应,所以又点了一次)

    Thread t2 = new Thread(() -> {

        int oldMoney = money.getReference();

        int version = money.getStamp();

        money.compareAndSet(oldMoney, oldMoney - 100, version, version + 1);

    });

    t2.start();



    // 给账户 +100

    Thread t3 = new Thread(() -> {

        int oldMoney = money.getReference();

        int version = money.getStamp();

        // 执行耗时 1s

        try {

            Thread.sleep(1000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        money.compareAndSet(oldMoney, oldMoney + 100, version, version + 1);

    });

    t3.start();



    t1.join();

    t2.join();

    t3.join();

    System.out.println("最终余额:" + money.getReference());

}

}




运行结果:



![](https://img-blog.csdnimg.cn/e3c7d610b5b34707bed933d77f23f2b4.png) 



> 成功解决ABA问题。



1.2 悲观锁

-------



### 1.2.1 悲观锁定义



        总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。



### 1.2.2 悲观锁应用



        之前介绍过的 synchronized、Lock 都是悲观锁。



详情请看:[Java多线程(三):线程安全问题与解决方法\_澄白易的博客-CSDN博客]( )



2. 公平锁&非公平锁

===========



>         假设三个线程 A, B, C。 A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.当线程 A 释放锁的时候, 会发生啥呢? 



*   **公平锁**: 遵守 "先来后到"。B 比 C 先来的,当 A 释放锁的之后, B 就能先于 C 获取到锁;

*   **非公平锁**: 不遵守 "先来后到"。 B 和 C 都有可能获取到锁。



注意:



1.  操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是**非公平锁**. 如果要想实现**公平锁**, 就需要依赖额外的数据结构, 来记录线程们的先后顺序;

2.  **公平锁**和**非公平锁**没有好坏之分, 关键还是看适用场景。



3\. 读写锁

=======



3.1 读写锁的定义

----------



        **读写锁(Readers-Writer Lock)****顾名思义就是⼀把锁分为两部分:读锁和写锁**,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得(写锁),并且写操作和读操作也是互斥的,总结来说,**读写锁的特点是:读读不互斥、读写互斥、写写互斥。**



        Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁:



*   ReentrantReadWriteLock.ReadLock 类表示⼀个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁;

*   ReentrantReadWriteLock.WriteLock

[video(video-8PGqMbZO-1716354551447)(type-bilibili)(url-https://player.bilibili.com/player.html?aid=1804892953)(image-https://img-blog.csdnimg.cn/img_convert/d03a705143646f4f3f6ea879d59b2788.png)(title-必看视频!获取2024年最新Java开发全套学习资料 备注Java)]

 类表示⼀个写锁. 这个对象也提供了 lock / unlock方法进行加锁解锁.



**3.2 读写锁示例**

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



public class ReadWriteLockDemo {

public static void main(String[] args) {

    // 创建读写锁

    final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 创建读锁

    final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

    // 创建写锁

    final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    // 线程池

    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,

            new LinkedBlockingDeque<>(100));

    // 启动新线程执行读任务

    executor.submit(() -> {

        // 加锁操作

        readLock.lock();

        try {

            System.out.println("执行读锁1:" + LocalDateTime.now());

            TimeUnit.SECONDS.sleep(3);

        } catch (InterruptedException e) {

            e.printStackTrace();

        } finally {

            readLock.unlock();

        }

    });



    // 启动新线程执行读任务2

    executor.submit(() -> {

        readLock.lock();

        try {

            System.out.println("执行读锁2:" + LocalDateTime.now());

            TimeUnit.SECONDS.sleep(3);

        } catch (InterruptedException e) {

            e.printStackTrace();

        } finally {

            readLock.unlock();

        }

    });



    // 启动新线程执行写操作

    executor.submit(() -> {

        writeLock.lock();

        try {

            System.out.println("执行写锁1:" + LocalDateTime.now());

            TimeUnit.SECONDS.sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        } finally {

            writeLock.unlock();

        }

    });



    // 启动新线程执行写操作2

    executor.submit(() -> {

        writeLock.lock();

        try {

            System.out.println("执行写锁2:" + LocalDateTime.now());

            TimeUnit.SECONDS.sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        } finally {

            writeLock.unlock();

        }

    });

    executor.shutdown();

}

}




运行结果:





# 总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

![](https://img-blog.csdnimg.cn/img_convert/0842a7fd074a8ee36e9dd91ef6a1d651.webp?x-oss-process=image/format,png)
行写锁2:" + LocalDateTime.now());

                TimeUnit.SECONDS.sleep(1);

            } catch (InterruptedException e) {

                e.printStackTrace();

            } finally {

                writeLock.unlock();

            }

        });

        executor.shutdown();

    }

}

运行结果:

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

[外链图片转存中…(img-sf2fiF51-1716458140733)]

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值