自学内容网 自学内容网

JUC(java.util.concurrent)的常见类

JUC(java.util.concurrent)的常见类

Callable(这是一个interface接口)

这个也是创建线程的一种方式

Runnable能表示一个任务(run方法)

返回:void

Callable也能表示一个任务(call方法)

返回:一个具体的值,类型可以通过泛型参数来指定(Object)

如果进行多线程操作,只是关心多线程执行的过程,使用Runnable即可.(像,线程池,定时器 )

如果是关心多线程的计算结果,使用Callable更合适这个是使用Runnable来实现从1加到1000,最后返回值的代码.下面我们用Callable的方式写一个一模一样功能的代码

public class Demo {
    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 < 1000; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        //注意,对于Callable而言,是不能直接放到Thread中的,但是Runnable可以,
        // 原因就是Runnable不在意返回值,但是Callable在意返回值.
        //这里要用到FutureTask.
        FutureTask<Integer> futureTask=new FutureTask<>(callable);
        Thread t1=new Thread(futureTask);
        t1.start();
        //这里需要用到FutureTask的get方法来获取Callable最后的结果.
        Integer result=futureTask.get();
        System.out.println(result);
    }
}

注意看注释!!!

FutureTask.get();这个是用来获取call方法的返回结果的,get类似于join一样,如果call方法没执行完,就会阻塞等待.

ReentrantLock(可重入锁)

这个锁,没有synchronized那么常用,但是也是一个可以选择的加锁的组件,这个锁在使用上更接近于C++.这个里面有lock()方法以及unlock()方法,但是我们这样用的时候就容易出现,unlock调用不到的情况,比如,中间return/抛出异常.

ReentrantLock具有一些特点是synchronized不具备的功能

优势

1.提供了一个tryLock方法进行加锁

对于lock这样的一个操作,如果枷锁不成功,就会阻塞等待(死等).对于tryLock而言,如果加锁失败,直接返回false/也可以设定等待时间.

2.ReentrantLock有两种模式,可以工作在公平锁状态下,也可以工作在非公平锁的状态下,构造方法中通过参数设定的 公平/非公平模式

3.ReentrantLock也有等待通知机制,搭配Condition着花样的类来完成,这里的等待通知要比wait notify功能更强.

劣势

1.synchronized锁对象是任意对象,RenntrantLock锁对象就是自己本身,如果你多个线程针对不同的ReentrantLock调用Lock方法,此时是不会产生锁竞争的.

信号量 Semaphore

这个在操作系统中也经常出现,Semaphore是并发编程中的一个重要的概念/组件.

准确来说,Semaphore是一个计数器(变量),描述了"可用资源(别称:临界资源)的个数",临界资源其实就是多个线程/进程等并发执行的实体可以公共使用到的资源(多个线程修改同一个变量,这个变量就可以认为是临界资源)

例子:假设我们现在要去一个停车场去停车,停车场的入口,上面有一个显示屏,显示了一行字:还有XX个空余车位(这里的XX就是信号量).如果我开车进入停车场,就相当于释放了一个车位(申请了一个可用资源),此时计数器就要-1,称为P操作accquire,如果我开车出了停车场,就相当于,释放了一个车位(释放了一个可用资源),此时计数器就要+1,称为V操作release.当计数器为0的时候,继续进行P操作,就会阻塞等待,一直等到其他线程执行了V操作,释放了一个空闲资源为止.锁,蹦智商是一个特殊的信号量(里面的数值,非0即1,二元信号量)信号量要比锁更广义,不仅仅可以描述一个资源,还可以描述N个资源.

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //这里可以通过构造方法来指定计数器的初始值
        Semaphore semaphore=new Semaphore(3);
        semaphore.acquire();
        System.out.println("执行了一个P操作");
        semaphore.acquire();
        System.out.println("执行了一个P操作");
        semaphore.acquire();
        System.out.println("执行了一个P操作");
        semaphore.acquire();
        System.out.println("执行了一个P操作");
    }
}

由于我们的初始值只有三个,但是我们这里写了四个P操作,所以在执行第四个P操作的时候会进入阻塞等待.

CountDownLatch

这个是针对特定场景的一个组件,场景:下载某个东西,有的时候下载文件并不是你的网速慢,更多的时候是因为人家服务器的限制,有一些"多线程下载器",就可以把一个大的文件,拆分成多个小的部分,是哟几个多个线程分别下载,每个线程负责下载一部分,每个线程分别是一个网络连接,这样就可以大幅提高下载速度.假设,我们这里分成了10个线程,10个部分来下载,啥时候算是下载完了??10个部分都下载完了,整体才算完成,那我们怎么来判定10个部分都下载完了呢?

CountDownLatch:当需要把一个任务拆分成多个任务,如何衡量现在是把多个任务都搞定了呢?

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        //CountDownLatch的构造方法中所写的数字就是我们这里分成了几个任务
        CountDownLatch countDownLatch=new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            int id=i;//这个就可以修正下面因为变量捕获所带来的一些不必要的麻烦
            Thread t=new Thread(()->{
                //这里直接写:  +i+  会报错,为什么?
                //1.涉及到lambda表达式的变量捕获,前面说过了
                System.out.println("任务"+id+"开始执行");
                //这里用sleep来代替下载所需要的时间
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务"+id+"结束");
                //每个任务执行到这里都调用一下方法,就好比接力赛,每个运动员到这里都撞线了.
                countDownLatch.countDown();
            });
            t.start();
        }
        //主线程如何知道任务结束了呢?
        //总不能用join吧?假设我们有1万个线程,难道你要写1万个join吗?
        //而且有的时候任务结束并不代表线程结束,万一任务需要结束,但是由任务所创建出来的线程不需要结束,
        // 那么join不是也就不行了吗?
        //在这里,主线程中可以使用 CountDownLatch 来负责等待任务结束.
        //如果上方countDown这里的次数<设定的初始值
        //await就会阻塞
        countDownLatch.await();
        System.out.println("多个线程的所有任务都执行结束了");
    }
}

运行结果如下

集合类

在Java集合类中,哪些是线程安全的?也就是多个线程同时操作这个集合类,是否会产生问题.

Vector,Stack,HashTable是线程安全的,其中Vector与HashTable是不建议使用的,其他的集合类是线程不安全的.Vector和HashTable属于是Java上古时期搞出来的集合类.那个时候人们对多线程编程的认识还不够深刻,所以在这里Vector与HashTable都过分的加了锁,有的时候,加了锁,不一定就线程安全,不加锁也不一定就线程不安全=>务必要具体问题具体分析.

所以,Vector/HashTable这样的集合类,虽然加了synchronized也不能保证一定是线程安全的.同时,在单线程的情况下,又可能因为synchronized影响到执行效率,所以,后续设计的集合类,就不再是这种加锁方式了.

多环境使用ArrayList

1.自己使用同步机制(ReentrantLock或者synchronized)(说人话就是自己手动加锁)
2.Collections.synchronizedList(new ArrayList)(很少会真用这个)

ArrayList本身没有使用synchronized,但你又不想自己加锁,就可以使用这个,相当于让ArrayList像Vector一样工作.

CopyOnWriteArrayList(写时复制)

多个线程同时修改同一个变量,会出现线程安全问题,那么如果多个线程修改不同变量,是不是就安全了呢?

如果多线程去读取,本身就不会有任何线程安全问题,一旦有线程修改,就会把自身复制一份,尤其是修改如果比较耗时的话,其他线程还是从旧的数据上读取,一旦修改完成,就使用新的ArrayList替换旧的ArrayList(本质上就是一个引用的重新赋值,速度极快,并且又是原子的)这个过程中,没有引入任何加锁操作,使用了创建副本=>修改副本=>使用副本替换的方式

ConcurrentHashMap

线程安全的hash表

HashTable是在方法上直接加上synchronized,就相当于针对this加锁.

任意的针对ht对象的操作,都会涉及到针对this的加锁.此时,如果有很多线程都想操作ht,就一定会触发激烈的锁竞争,这些线程最后只能一个一个排着队,依次进行(这种情况,并发程度很低)

如果两个修改操作是针对两个不同的链表进行修改,是否会存在线程安全问题呢?? 显然是不会的,所以原来的HashTable给每一个插入以及其他操作都加锁,这种情况是没有必要的,因为加锁这个操作本身的开销就是比较大的,而且也会这样也会造成不必要的阻塞等待.还有一种情况,针对同一个链表修改时,如果两个线程修改的是同一个链表中的同一个位置,那么会造成线程安全问题,针对同一个链表的不同位置修改时,就不会造成线程安全问题.

具体的做法就是给每个链表都去安排一把锁

这样的话,我们的锁冲突的概率就打打降低了,那么怎么给每一个链表都上锁呢?由于Java的synchronized随便拿个对象就可以加锁,所以我们就拿每个链表的头结点来进行加锁

ConcurrentHashMap改进:

1.[核心]减小了锁的粒度,每个链表都有一把锁,大部分情况下都不会涉及到锁冲突.

2.广泛使用了CAS操作,减小了锁冲突的概率

3.写操作进行了加锁(链表级),读操作就不加锁了

4.针对扩容操作进行了优化,渐进式扩容

HashTable一旦触发扩容,就会立即的一口气完成所有元素的搬运,这个过程相当耗时.就会出现大部分请求都很顺畅,突然某个请求就卡了比较久这样的情况,要解决这个问题也非常简单:化整为零,当需要进行扩容的时候,会创建出另一个更大的数组,然后把旧的数组上的数据逐渐的往新的数组上搬运.

1.新增元素,往新数组上插入

2.删除元素,把旧数组的元素给删掉即可

3.查找元素,新数组旧数组都得查找

4.修改元素,统一把这个元素给搞到新数组上

与此同时,每个操作都会触发一定程度搬运,每次搬运一点,就可以保证整体的时间不是很长,积少成多之后,逐渐完成搬运了,也就可以把之前的旧数组彻底销毁了.

那么话说回来,HashMap和ConcurrentHashMap有什么区别吗?

线程安全和线程不安全之间的区别.

关于ConcurrentHashMap的分段技术?

Java 8 之前,ConcurrentHashMap是使用分段锁,从Java 8 开始,就是每个链表自己一把锁了.

就像这样,这样虽然能提高效率,但是不如每个链表一把锁,代码实现起来也更复杂


原文地址:https://blog.csdn.net/X_HJJ/article/details/140176592

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!