前言
🌟🌟本期讲解关于CAS的补充和JUC中有用的类,这里涉及到高频面试题哦~~~
🌈上期博客在这里:【JavaEE初阶】深入理解不同锁的意义,synchronized的加锁过程理解以及CAS的原子性实现(面试经典题);-CSDN博客
🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客
目录
2.ReentrantLock与synchronized的区别
📚️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)
💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。
😊😊 期待你的关注~~~