【JavaEE初阶】CAS的ABA问题,JUC多线程编程有用的相关类

 

前言

🌟🌟本期讲解关于CAS的补充和JUC中有用的类,这里涉及到高频面试题哦~~~

🌈上期博客在这里:【JavaEE初阶】深入理解不同锁的意义,synchronized的加锁过程理解以及CAS的原子性实现(面试经典题);-CSDN博客

🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客

 

目录

📚️1.CAS的ABA问题

1.1ABA问题

1.2ABA问题的解决

 📚️2.JUC的相关类

2.1callable接口

1.介绍

2.代码实现

2.2ReentrantLock可重入锁

1.介绍

2.ReentrantLock与synchronized的区别

2.3 信号量semaphore

1.介绍

2.代码实现

 2.4CountDownLatch类

1.介绍

2.代码实现

3.CountDownLatch与join的区别

2.5多线程环境使用ArrayList 

1.synchronizedlist

2.CopyOnWriteArrayList

 2.6ConcurrentHashMap(面试经典)

1.介绍

2.ConcurrentHashMap的改进

📚️3.总结

 

📚️1.CAS的ABA问题

1.1ABA问题

 在上期小编讲解过CAS的自带的原子性和自旋功能后,本期又进行最后CAS的补充即CAS的ABA问题~~~

我们知道了解CAB中当寄存器和内存的值一样时,就进行++操作,但是如果不一样的时候,那么就不进行操作,就直接再次进行读取内存中的值,一般情况下是没有任何的问题的

如下图:

这里我们进行读取内存后,发生穿插,再次读内存,发现一样那么就进行-500的操作(取500),那么再次执行线程1的时候,这里发现寄存器的值和内存不一样了,那么就不会进行(-500)的操作,这是没问题的

但是有以下问题:

 

那么此时t3线程进行存500的操作,那么可以发现在执行线程1后面的代码时,那么寄存器和内存的值是一样的,那么就会再次(-500)的操作,那么此时我们想的是剩下1000,结果导致现在扣款两次,剩下500了,则这是存在问题的;

注意:由于CAS不能判定这个要进行修改的数据是否在之前已经被修改并回复过,那么就会导致出现问题;

1.2ABA问题的解决

这里解决ABA的问题有两种:

即约定数据的修改只能时单向的(即只能增加或者只能减少),不能是双向的;

当数据的修改只能是双向变化的话,那么就可以引入一个版本号,这个版本号随着数据的修改而增加(这里只能是增加,不能减少)

 📚️2.JUC的相关类

这里的JUC即(java.util.concurrent)这个包里的关于多线程编程的有用相关类;

2.1callable接口

1.介绍

这里的callable接口即是实现多线程编的另一种方法,他和Runnable的区别:

Runnable接口:它主要注重的是运行的过程,而不关注这个结果;

callable接口:它记注重运行的过程,还关注这个结果;

2.代码实现

那么接下来,小编接举个例子吧~~~

使用Runnable实现多线程编程,代码如下:

public static int result=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                int sum=0;
                for (int i = 0; i <=100 ; i++) {
                    sum+=i;
                }
                result=sum;
            }
        });
        t.start();
        t.join();
        System.out.println(result);
    }

那么此时可以发现,我们在实现最终结果的打印的时候,要重新定义一个静态的成员变量,当线程多了的时候,显然要多定义多个静态成员变量,但是这很麻烦;

实现callable接口,代码如下:

public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum=0;
                for (int i = 0; i <=100 ; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        //添加粘合剂
        FutureTask<Integer> futureTask=new FutureTask<>(callable);
        Thread t=new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());
    }

这里可以看到此时代码就不用设置静态成员变量了,此时代码就美观多了,但是这里要设置 FutureTask<Integer> futureTask=new FutureTask<>(callable);这里的意思是未来的任务,啥任务呢?即callable的实现重写的任务;

注意:这里的futherTask是作为这里的粘合剂来放入到线程的执行中,执行callable重写的任务,这里的输出get是带有阻塞的功能的,当线程执行完后才能打印,没有执行完就进行阻塞,不打印~~·

2.2ReentrantLock可重入锁

1.介绍

这里的ReentrantLock是比较久远锁,在synchronized没有很强大的功能的时候,大多用这个ReentrantLock锁来进行加锁操作;

注意:传统的锁的加锁和解锁是分开的即lock加锁,unlock即开锁,但是在某些操作中return后,或者try catch后无法解锁,所以在实现ReentrantLock锁一定要使用finally 进行解锁;

2.ReentrantLock与synchronized的区别

1.ReentrantLock提供了trylock的操作

在ReentrantLock进行加锁失败后,不会进入阻塞,直接返回false;

而synchronized提供的lock,加锁后失败后,直接进入阻塞当中;

影响:使用trylock就提供了更多的操作空间,可以做其他的事情

2.ReentrantLock是一个公平锁

在之前我们讲到过,在公平锁可以避免“线程饿死”的情况,这里通过队列来对记录加锁线程的先后

而synchronized是一个非公平锁,就有可能造成“线程饿死”的情况

3.ReentrantLock的搭配机制不同

 ReentrantLock搭配的是condition类,在多个线程共用一个锁对象时,可以进行指定线程的唤醒操作

synchronized搭配wait/notify,在个线程共用一个锁对象时,随机唤醒某个线程

在绝大部分开发中synchronized就已经够用了~~~ 

2.3 信号量semaphore

1.介绍

即申请释放操作,当申请一个可用资源那么数值就-1(即申请操作P)当释放一个资源,那么数值就+1(即释放操作V);

2.代码实现

这里的信号量可以看做一个特殊的锁,释放锁,那么就进行+1操作值为1,加锁那么就是申请即+1操作的值就是-1即为0;当为0后就不能进行加锁操作了;

具体代码实例如下:

public static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore=new Semaphore(1);
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                semaphore.release();
            }
        });

在之前我们设置两个线程进行count的++操作,需要使用锁来进行打包操作,但是此时我们使用semaphore也能够解决线安全的问题;(这里是两个线程,代码基本一致,小编就省略咯~~)

 2.4CountDownLatch类

1.介绍

在多线程完成一个较大的任务时,需要拆分为几个小的任务,当所有任务执行完后,在拼在一起,即多线程执行任务后拼接,在使用CountDownLatch时可以很方便的知道每个线程是否执行完

2.代码实现

但我们存在10个线程要进行等待的时候使用join只能等待一个线程,那么我们就可以使用CountDownLatch来进行每个线程的执行完的记录;

代码如下:

public static void main(String[] args) throws InterruptedException {
        CountDownLatch count=new CountDownLatch(10);
        for (int i = 0; i <10 ; i++) {
            int num=i;
            Thread t=new Thread(()->{
                System.out.println("线程"+Thread.currentThread().getName()+"开始下载"+num+"任务");
                Random r=new Random();
                int time=r.nextInt(5)*1000;
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("任务"+num+"执行完成");
                count.countDown();
            });
            t.start();
        }
        count.await();
        System.out.println("所有任务执行完毕");
    }

这里的构造函数为10就表示有10个线程执行任务,在随机进行休眠后,通过countdown方法记录完成的线程,最后通过await来进行等待countdown为10,在执行主线程;

3.CountDownLatch与join的区别

在上述代码中也可以随时用join来进行操作,但是输出如下:

在使用CountDownLatch执行任务的时候,输出代码如下:

注意:join只能一个线程一个线程的执行完然后再次执行下个线程,但是使用CountDownLatch可以执行多个线程;

2.5多线程环境使用ArrayList 

1.synchronizedlist

collection.synchronizedList(new ArrayList),对arraylist套了一个壳,得到的新的对象的关键方法都是带有锁的;

2.CopyOnWriteArrayList

即写时拷贝,在对顺序表进行修改的时候,对其进行拷贝一份新的,修改新的顺序表的值,然后修改引用的指向(这里是原子性的)

图示如下:

比如:服务加载配置文件的时候,就要将配置文件进行解析到内存的数据结构中;

局限性:

1.复制的顺序表不能太大

2.修改不能太频繁

 2.6ConcurrentHashMap(面试经典)

1.介绍

我们知道HashMap是不安全的,但是Hashtable是线程安全的,因为Hashtable在关键的方法上添加了synchronized;所以标准库引进了ConcurrentHashMap,那么ConcurrentHashMap先比较与Hashtable做出了什么改进

2.ConcurrentHashMap的改进

1.锁的粒度缩小

所谓的粒度即加锁内容的代码量,当代码长执行时间长那么就是粒度大(粗),反之则反

如下图就是两者的区别:

Hashtable:

当对于不同链表的操作的时候,都会发生所冲突,这是我们不希望看到的,这两个链表没有必然的联系, 当对于不同的链表进行修改操作的时候,不会发生线程安全问题;但是操作同一个链表上的时候,由于操作到同一个引用那么就会发生线程安全问题;

ConcurrentHashMap:

在操作不同链表上的值,是不会发生锁冲突的(由于是不同的锁对象),并且在操作同一个链表的值时,由于加锁的操作,就保证了线程安全;(锁桶)

 2.充分使用了CAS的原子操作

例如:对与哈希表的元素的个数的维护;

我们知道CAS中Java提供的原子类工具,是可以实现原子性的操作(即一个指令),不会发生线程安全问题,所以ConcurrentHashMap对其做了充分的利用

3.针对扩容操作的优化

 这里涉及到“负载因子”描述了每个桶上平均有多少个元素,在达到负载因子阈值的时候就进行树化或者扩容,这里就要注意扩容的机制了

HashMap

创建一个更大的数组,将旧的元素全部搬到新的表中,但是当hash中的元素非常多的时候可能就会导致扩容操作非常慢;

ConcurrentHashMap:

这里针对扩容操作就是“蚂蚁搬家”的方法,当需要扩容的时候,每次“查找、删除,插入”都只会搬运一部分元素,虽然花费的时间长了,但是每次操作的的消耗时间变短了,就更加流畅

注意:扩容操作是一个低频的操作,前提是设置好容量

补充:

插入操作:是在新的顺序表中插入

查询操作:是在新的,旧的顺序表中进行查询

删除操作:是在新的,旧的顺序表中进行删除

📚️3.总结

💬💬本期是多线程的完结篇,涉及到CAS的ABA问题,以及JUC的多线程编程的相关类,例如:callable接口,ReentrantLock可重入锁,信号量semaphore,CountDownLatch类,ConcurrentHashMap,关于他们的性质,以及与我们之前学过的内容join,wait,HashMap....做了比较;

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!

多线程代码完结!!!所有的代码都在这里:GGBondlctrl/Thread (gitee.com)

 


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

                             😊😊  期待你的关注~~~

评论 52
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值