【多线程】 九

本文主要介绍了java.util.concurrent包的几个常见类,包括可以创建线程的Callable接口、ReentrantLock可重入锁、Semaphore信号量以及CountDownLatch拆分任务的应用场景,还讨论了集合类的线程安全问题。

一.java.util.concurrent常见类

concurrent并发(多线程)

1.Callable interface 

也是一种创建线程的方式

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

返回void

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

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

如果进行多线程操作,

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

如果是关心多线程的计算结果,使用Callable合适(比如让一个线程计算1+2+3+…+1000)

注意:使用Callable不能直接作为Thread的构造方法参数,需要先使用FutureTask把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<=1000;i++){
                    sum+=i;
                }
                return sum;
            }
        };

        //callable不能直接作为Thread的构造方法参数
        FutureTask<Integer> futuretask=new FutureTask<>(callable);
        Thread t=new Thread(futuretask);
        t.start();

        //使用futuretask获取call方法的返回结果
        //get类似于join一样,如果call没执行完,会阻塞等待
        Integer result=futuretask.get();
        System.out.println(result);
    }

2.ReentranLock可重入锁

reentrant再进去的,可重入的

没有synchronized那么常用,但是也是一个可选的加锁的组件

加锁加锁分为两个方法

lock()

unlock()

它具有一些synchronized不具备的功能

1.提供一个tryLock方法加锁

对于lock,如果加锁不成功,就会阻塞等待(死等)

对于tryLock,如果加锁失败,直接返回false/也可以设定等待时间

2.有两种模式,可以工作在公平锁状态下,也可以工作在非公平锁状态下

构造方法中通过参数设定的公平/非公平模式

3.也有等待通知机制,搭配Condition这样的类来完成

但是reantrantLock劣势也明显,就是unlock容易遗忘,可以使用finally执行unlock。

区别:

synchronized锁对象是任意对象

reentrantLock锁对象就是自己本身

如果多个线程针对不同的reantrantLock调用lock方法,也就会对不同的对象加锁,此时不会产生锁竞争。

  • 实际开发中,进行多线程开发,用到锁还是首选synchronized,因为它有很多优化提供给程序员使用。

3.Semaphore信号量

是并发编程中的一个重要的概念/组件。

它是一个计数器,描述了可用资源的个数

描述的是,当前这个线程,是否有”有临界资源可以用“

(临界资源:多个线程、进程等并发执行的实体可以公共使用的资源)

申请一个可用资源,计数器-1,称为P操作( accquire

释放一个可用资源,计数器+1,称为V操作 (release

  • 当计数器数值位0时,若继续p操作,就会阻塞等待,一直等待到其他线程执行了V操作,释放一个空空闲资源为止;

这个阻塞等待的过程其实和锁很相似

实际上,锁本质上就是一个特殊的信号量(里面的数值,非0即1,二元信号量)

 public static void main(String[] args) throws InterruptedException {
        //构造方法中,就可以用来指定计数器的初始值
        Semaphore semaphore=new Semaphore(4);

        //申请一个可用资源,计数器-1
        semaphore.acquire();
        System.out.println("执行p操作");

        semaphore.acquire();
        System.out.println("执行p操作");

        semaphore.acquire();
        System.out.println("执行p操作");

        semaphore.acquire();
        System.out.println("执行p操作");

        //计数器位0,阻塞等待
        semaphore.acquire();
        System.out.println("执行p操作");

    }

4.CountDownLatch

针对特定场景中的一个组件。

有一些多线程下载器,把一个大的文件,拆分为多个小的部分,使用多的线程分别下载,每个线程负责下载一部分,每个线程分别是一个网络连接,这样就可以大幅度提高下载速度。

假设,分为10个线程来下载

什么时候算下载完了?

10个线程都下载完了,整体才算完成,

public static void main(String[] args) throws InterruptedException {
        //构造方法中,指定创建几个任务
        CountDownLatch countDownLatch=new CountDownLatch(10);
        for(int i=0;i<10;i++){
            //为了实现变量捕获,新创建一个变量id(实际上的final)
            int id=i;
            Thread t=new Thread(()->{
                System.out.println("线程"+id+"开始工作");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程"+id+"结束工作");

                //把10个线程看成10个田径运动员,countdown就是运动员撞线
                countDownLatch.countDown();
            });
            t.start();
        }
        //主线程如何知道所有线程都完成了
        /**
         * 主线程中使用coutdownlatch负责等待任务结果
         * await等待所有任务解锁
         * 当countdown次数小于初始设置的次数,await就会阻塞
         *
         */
        countDownLatch.await();
        System.out.println("多个线程的所有任务都执行完了");
    }

CountDownLatch当需要把一个任务拆分称多个任务

二.集合类的线程安全问题

哪些是线程安全的?(多个线程同时操作这个集合类,是否会产生问题)

  • Vector,HashTable,Stack是线程安全的,其他的集合类不是线程安全的。

注意:但是Vector和HashTable属于是Java上古时期的集合类。

加了锁也一定是线程安全,不加锁也不一定线程不安全,

还需要具体问题具体分析。


HashMap 和 HashTable 和 concurrentHashMap的区别:

HashMap线程不安全

HashTable线程安全,关键方法都提供了synchronized

ConcurrentHashMap 线程安全的hash表

HashTable 和concurrrentMashMap

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

(意味着任何针对this对象的操作,都会涉及到针对this的加锁,此时,如果很多线程都想操作该对象,就会触发激烈的锁竞争,并发程度很低。

哈希桶/链表

如果两个修改操作,是针对两个不同的链表进行修改,是否会存在线程安全问题?

不会。

尽管如此,虽然没有线程安全为题,但是又不能完全不加锁。

比如同时有两个线程插入到两个链表之间,此时就会产生一些问题。

具体的做法:给每个链表都安排一把锁。

实质上,哈希表上的链表本身就有很多,两个线程同时操作同一个链表的概率本身就低,整体锁的开销就大大降低了。

由于synchronized任何都能用来了加锁,可以简单的使用每个链表的头结点作为锁对象。

ConCurrentHashMap改进:

1.核心:减小了锁的粒度,每个链表有一把琐,大部分情况下都不会涉及到锁竞争。

2.广泛使用了CAS操作(不会产生锁冲突)

3.写操作进行了加锁,读操作不加锁。

(如果是一个线程读,一个线程写,最多在修改的一瞬间,读到的是旧版本、新版本的数据,通过一些紧密的操作,保证不会读到“半个数据”)

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

HashTable一旦触发扩容,就会立即一口气的完成所有元素的搬运,这个过程相当耗时,大部分都比较顺畅,就怕突然某个请求卡壳比较久。

而这里采用化整为零,当需要扩容的时候,会创建出一个更大的数组,然后把旧的数组逐渐往新的数组上搬运,会出现一段时间,旧数组和新数组同时存在。

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

删除元素,把数组元素删掉即可

查找元素,新旧数组都查找

修改元素,统一把元素改到新数组上

以上操作都会触发一定程度搬运,多次少量地搬运就把之间地旧的数组销毁了


以上是HashTable和ConcurrentHashMap之间的区别,经典的面试题。

分段锁

Java8之前,concurrentHashMap使用分段锁,能提高效率,但是不如每个链表一把锁,并且代码实现起来比较复杂。


copyOnWriteArrayList

写时复制

多个线程同时修改同一个变量,必定会产生线程安全问题。

如果多个线程修改不同的变量,是不是安全呢?

如果是多线程读取,本身就不会有任何线程安全问题。

一旦有线程修改,就会把自己复制一份。

如果修改耗时的话,其他线程还是会从旧的数据上读取。

一旦修改完成,使用新的ArrayList替换旧的ArrayList替换旧的ArrayList

这个过程中,没有引入任何的加锁操作,使用的是

创建副本 --> 修改副本 --> 使用副本替换。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值