JUC常见类以及线程安全的集合类

线~~~

目录

1.JUC常见类

1.1Callable接口

1.2ReentrantLock

1.3信号量 Semaphore

1.4CountDownLatch

2.线程安全的集合类

2.1多线程环境使用 ArrayList

2.2多线程环境使用队列

2.3多线程环境使用哈希表

2.3.1Hashtable

2.3.2ConcurrentHashMap


1.JUC常见类

JUC的全称:java.util.concurrent(concurrent指的是多线程相关操作)

1.1Callable接口

①Callable是什么:

Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果.

②为什么Callable接口更适合写这种关于计算的代码?

我们就是为了解决Runnable不方便返回结果这个问题

③我们使用Callable接口来解决这个问题的代码:

a.创建一个匿名内部类 , 实现 Callable 接口, 泛型参数表示返回值的类型。
b.重写 Callable call 方法 , 实现1+2+3+...+1000的执行过程。
c.把 callable 实例使用 FutureTask 包装一下。
b.创建线程 , 线程的构造方法传入 FutureTask , 此时新线程就会执行 FutureTask 内部的 Callable 的。
e.call 方法 , 完成计算,   计算结果就放到了 FutureTask 的 对象task中。
f.在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕 并获取到 FutureTask 中的结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo2{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer>callable=new Callable() {
            @Override
            public Object call() throws Exception {
               int sum=0;
               for(int i=0;i<=1000;i++){
                   sum+=i;
               }
               return sum;
            }
        };
        //为了让线程执行Callable中的任务,光使用构造方法是不够的,还需要使用一个辅助类
        FutureTask<Integer>task=new FutureTask<>(callable);
        //创建线程,来完成这里的工作
        Thread t=new Thread(task);
        t.start();
        int result = task.get();
        System.out.println(result);
    }
}
④进一步理解Callable接口:
Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务。Callable 通常需要搭配 FutureTask 来使用。 FutureTask 用来保存 Callable 的返回结果.。因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。FutureTask 就可以负责这个等待结果出来的工作。
举个生活中的例子来进一步说明:
当我们去一个餐馆进行吃饭,点单的时候会给我们一个小票,当我们刚拿到小票时显然商家正准备进行加工,而当加工完成将要反馈给我们的时候,他会进行叫号,以便确保是谁的单。而这种反馈叫号的操作很明显就是用上面我们提到的FutureTask等待接收结果的这个行为。

1.2ReentrantLock

①ReentrantLock是什么?

ReentrantLock也是一种可重入锁,和 synchronized很像,两者都是用来实现互斥效果, 保证线程安全的。

②基本用法:(它是把加锁解锁两个操作进行分开的操作)

lock():加锁,如果获取不到就一直死等到获取到为止的操作

trylock(超时时间):加锁,如果一段时间仍然获取不到锁,就放弃加锁

unlock():解锁

ReentrantLock和Synchronized的区别:

a.ReentrantLock是在JVM外部实现的一个标准库的类(基于Java来实现的),而synchronized是在JVM内部实现的一个关键字(基于C++来实现的)

b.ReentrantLock是需要我们进行手动加锁解锁的,使用起来确实更加灵活,但是很多时候手动释放也会被我们忽视。synchronized不需要手动释放锁,出了相应的代码块后,锁即自动释放。

c.ReentrantLock在锁竞争失败的时候除了阻塞等待以外,可以尝试trylock()来获取到锁,如果失败了就直接返回,给我们留下了更多的余地。而synchronized如果竞争的时候失败就会阻塞等待。

d.ReentrantLock既可以是公平锁,也可以是非公平锁,我们只需要在它的参数位置进行指定(默认是非公平锁,true即是公平锁),而synchronized只是一个非公平锁。

e.ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程。 synchronized 是通过 Object wait / notify 实现等待-唤醒.。每次唤醒的是一 个随机等待的线程,相对而言功能是有限的。

但是在我们的日常工作开发中,synchronized 就够用啦

1.3信号量 Semaphore

①什么是信号量:

信号量, 用来表示 "可用资源的个数"。实质上是一个更广义的锁。(锁也被称为二元信号量)
②举一个通俗的例子来帮助你理解信号量:
自驾去某个地方,我们经常会遇到停车的问题, 可以把信号量想象成是停车场的展示牌: 当前有车位 20 个,表示有 20 个可用资源。 当有车开进去的时候, 就相当于减少(申请资源)了一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作) 当有车开出来的时候, 就相当于增加(释放资源)一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)。 如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源。
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用。
import java.util.concurrent.Semaphore;
public class demo2{
    public static void main(String[] args) throws InterruptedException {
        //表示提供了10个资源
        Semaphore s=new Semaphore(3);
        //资源申请
        s.acquire();
        System.out.println("申请资源啦");
        s.acquire();
        System.out.println("申请资源啦");
        s.acquire();
        System.out.println("申请资源啦");
        s.acquire();
        System.out.println("申请资源啦");
        //释放资源
      //  s.release(1);
    }
}

如图所示,这个时候只有3个资源位,要是我申请了3个后没有释放继续申请,那么程序就会出现阻塞的情况,结果如下图:(也就说只会打印3次)

1.4CountDownLatch

①什么是CountDownLatch?

这用文字不怎么好理解,所以给大家举一个例子:

大家应该都玩过王者荣耀吧,我们都知道最终的胜利是退掉敌方水晶,所以当我们退掉一座塔是不够的,我们需要把最终的水晶退掉,才能够结束这一场对局。

②相关方法的说明:

countDown 给每个线程里面去调用,就表示到达终点了。(就相当于上面提到游戏中每推掉一座塔)

await是给等待线程去调用.当所有的任务都到达终点了, await 就从阻塞中返回,就表示任务完成。(就相当于推掉水晶)

③代码演示:(注意,要等所有调用完了,即水晶推完,await才会返回。也就才会打印gameover...那句话)

a.没有调用完

 b.调用完:

import java.util.concurrent.CountDownLatch;
public class demo2{
    public static void main(String[] args) throws InterruptedException {
        //在游戏里,包括水晶,我们需要推掉4个塔
        CountDownLatch c=new CountDownLatch(4);
       for(int i=0;i<4;i++){
           Thread t=new Thread(()->{
               try {
                   Thread.sleep(3000);
                   c.countDown();
                   System.out.println(Thread.currentThread().getName()+"推掉了1座塔");
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           });
           t.start();
          // t.join();不加这个的时候,线程的调度是将是随机的
       }
       c.await();
        System.out.println("gameover!恭喜你获得胜利");
    }
}

 出现顺序 不一致是因为线程的调度是随机的

2.线程安全的集合类

2.1多线程环境使用 ArrayList

①自己使用同步机制 (synchronized 或者 ReentrantLock)

在上面已经对两者进行了讲解及区别,在这里就不再重复了

②Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized

③使用 CopyOnWriteArrayList
a.CopyOnWrite是什么以及它的原理是啥?

CopyOnWrite容器即写时复制的容器。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。(即在修改的时候,会创建一个副本出来)

b.CopyOnWrite的好处是什么?

修改的同时对于读操作,是没有任何影响的,读的时候优先读旧的版本,不会说出现读到一个"修改了一半"的中间状态。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

 c.它的优点缺点:

优点:在读多写少的场景下, 性能很高, 不需要加锁竞争.
缺点:占用内存较多,新写的数据不能被第一时间读取到

适用于读多写少,数据小的情况。

2.2多线程环境使用队列

这里的内容已经在前面穿插讲解过了,就不再过多介绍了

2.3多线程环境使用哈希表

2.3.1Hashtable(不推荐的)

只是简单的把关键方法加上了 synchronized 关键字.

public synchronized V put(K key,V value){
public synchronized V get(Object key){
这相当于直接针对 Hashtable 对象本身加锁 .
如果多线程访问同一个 Hashtable 就会直接造成锁冲突 .
size 属性也是通过 synchronized 来控制同步 , 也是比较慢的 .
一旦触发扩容 , 就由该线程完成整个扩容过程 . 这个过程会涉及到大量的元素拷贝 , 效率会非常低。
而效率低如何理解呢?我们这里给大家举一个例子:
比如,在学校我们规定,要是一个学生需要请假需要找校长签字的话,那么要是同时有很多学生需要请假,那么他们都需要找校长,这样的话无论对于学生还是老师来说,这都是很低效的操作。
如果元素多了,链表就会加长,就很影响哈希表的效率,如果在这个时候需要扩容,呢么还需要创建一个更大的数组,把之前的旧的元素搬运过去,很显然,这是非常耗时的。

2.3.2ConcurrentHashMap(推荐的)

相比于 Hashtable 做出了一系列的改进和优化 . Java1.8 为例  
①什么是 ConcurrentHashMap?
我们还是先举个例子来进行说明:
我们接着上述学生请假的例子,当校长处理了太多的请假,他就觉得这样太过麻烦,他就把这个权利 给到了各班班主任,这样的话,当学生需要请假的时候,这需要找他的班主任,这样不同班的同校同学的效率就极大的提高了。(这里的原理就是这样,对每个数组元素进行加锁,它负责管理的是连着它的链表。)
如下图一般:

 这样只针对操作元素的时候,是针对这个元素的链表头节点来进行加锁的。如果两个线程涉及的是两个不同链表的元素,那么这个时候是不会存在安全问题的,也就是是不必加锁的,而在hash表里,链表的数目是很多的,而它们的长度也是相对短的,那么这样的话发生锁冲突的概率就大大降低了

ConcurrentHashMap的特点:

a. 减少了锁冲突,就让锁加到每个链表的头结点上(锁桶)
b.只是针对写操作加锁了.读操作没加锁.而只是使用
c. 更广泛的使用CAS,进一步提高效率(比如维护size操作)
d.针对扩容,进行了巧妙的化整为零
对于HashTable 来说只要你这次put触发了扩容就一口气搬运完,会导致这次 put非常卡顿。对于ConcurrentHashMap,每次操作只搬运一点点。通过多次操作完成整个搬运的过程,同时维护一个新的 HashMap和一个旧的。查找的时候既需要查旧的也要查新的.插入的时候只插入新的,直到搬运完毕再销毁旧的。

线程方面的问题就到这里结束啦~下期我们将会向大家介绍文件相关内容~

                                                       谢谢大佬们来访

猜你喜欢

转载自blog.csdn.net/weixin_58850105/article/details/124228023