这是关于线程的最后一更啦~~~
目录
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和一个旧的。查找的时候既需要查旧的也要查新的.插入的时候只插入新的,直到搬运完毕再销毁旧的。
线程方面的问题就到这里结束啦~下期我们将会向大家介绍文件相关内容~
谢谢大佬们来访