JUC(java.util.concurrent)包的一些类 和 一些线程安全的集合类

Callable接口

Callable接口是Java中的一个函数式接口,它允许在另一个线程中执行某个任务并返回结果。

Callable也是一种创建线程的方式。但是与Runnable不同的是,Callable的call方法可以有一个返回值,Runnable的run方法没有返回值。即:Runnable关心过程(比如定时器,线程池中都是使用Runnable创建新线程),Callable关心结果(比如要让线程计算一个公式)。

 Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1 + 2;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        int a = futureTask.get();
        System.out.println(a);

需要注意的是,Callable不能直接作为Thread的参数,通常需要搭配FutureTask使用。

因为Callable线程什么时候执行完并产生返回值我们并不确定,什么时候去接收就不确定。FutureTask的get方法解决了这个问题,它可以阻塞等待直到Callable产生返回值。

ReentrantLock类

ReentrantLock也是Java中的一种锁机制,与Synchronized类似,它们都是可重入互斥锁,但是ReentrantLock还提供了一些其它的功能:

1:ReentrantLock需要手动的去获取和释放锁:

  ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();//调用lock方法加锁
        //需要加锁的操作
        //...
        reentrantLock.unlock();//调用unlock方法解锁

这个时候就会产生一个问题:如果中间进行return 或者 抛出异常 ,就会执行不到unlock方法,这个锁也就无法被释放了。这也是ReentrantLock一个比较致命的劣势

针对这个问题,我们可以把解锁操作写在finally模块当中:

ReentrantLock reentrantLock = new ReentrantLock();
        try{
            reentrantLock.lock();//调用lock方法加锁
            //需要加锁的操作
            //...
           
        } finally {
            reentrantLock.unlock();//调用unlock方法解锁
            
        }

2.提供了超时锁机制:即允许线程在等待一定时间后自动放弃获取锁;

ReentrantLock提供了一个unlock方法,它有俩个重载版本,一是在尝试加锁后直接返回加锁结果,二是设置一个等待时间,在等待时间内尝试获取锁,成功就返回true,失败就返回false。这给我们提供了很大的操作性。

3.ReentrantLock支持公平锁和非公平锁俩种模式

ReentrantLock默认是不公平锁,但是可以设置为公平锁。

4.ReentrantLock有更强大的等待通知机制

ReentrantLock可以搭配Condition实现更加灵活和精细的操作,具有更加强大的功能。

另外,Synchronized和ReentrantLock相比:

Synchronized是jvm内部实现的,而ReentrantLock是库中的一个类。

Synchronized加锁对象可以是任何对象,而ReentrantLock只能是自己本身

信号量 Semaphore

Semaphore本质上是一个计数器,用来描述可用资源的个数,是并发编程的一个常用组件。

具体来说:Semaphore提供了俩个方法:acquire()方法 和  release()方法,我们创建Semaphore对象时给它可用资源的个数。

acquire()方法获取资源,使得可用资源的个数减一

release()方法释放线程获得的资源,使得可用资源的个数加1

当可用资源的个数为0时,如果继续获取,就会产生阻塞等待。

锁本质上就是一个二元信号量,加锁相当于获取资源,解锁就相当于释放资源,资源用完再获取就会产生阻塞。

CountDownLatch

CountDownLatch是Java中的一个同步工具类,用于控制线程的执行顺序。它可以让某些线程一直等待,直到其他线程完成一系列操作后再执行。CountDownLatch维护了一个计数器,表示需要等待的操作的数量,线程可以通过调用countDown()方法来减少计数器的值,也可以通过调用await()方法来等待计数器的值变为0。

使用CountDownLatch可以实现一些并发控制的场景,例如等待多个线程完成某个操作后再进行下一步操作、等待多个服务启动完成后再启动主服务等

举例:

CountDownLatch countDownLatch = new CountDownLatch(5);//维护一个计数器值为5
        for(int i = 0;i < 5;i ++){
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(500);
                    countDownLatch.countDown();//执行完一个任务,让计数器的值减为0

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            });
            thread.start();
        }

        countDownLatch.await();//等待计数器的值为0
        System.out.println("5个任务全部执行完");

除了这些类以外,JUC中还包括之前已经写过的原子类和线程池。

线程安全的使用集合类

Vector Stack HashTable集合类

Vector,Stack,HashTable是很久之前人们实现的线程安全的集合类,但是它们为了实现线程安全,用类似给整个方法进行加锁的方式,大大影响到了程序的并发性。而且这种加锁方式也不能保证完全线程安全方法之间配合使用时还是有可能引起线程安全问题),而且由于时间过早,它们有很多不太合理的机制后面我们又引入了很多更加灵活高效的集合类,所以这几种集合类不建议被使用。

多线程使用ArrayList

1.自己根据实际情况合适的使用锁机制

2.Collections.synchronizedList(new ArrayList);

相当于让ArrayList像Vector一样工作,使得它关键步骤都加上锁。不建议使用。

3.使用 CopyOnWriteArrayList

本质上也是一种读写分离的思想,读数据时不会产生线程安全问题,所以直接正常返回值就好。

当要修改数据时,会涉及到线程安全问题,数组先去复制一份,在这个复制的数组上进行修改,同时对整个修改过程加锁,避免线程安全问题。修改完成以后,将修改后的ArrayList替换旧的ArrayList(本质上就是一个引用的重新赋值,是一个原子操作)。在这个过程中,可以正常在原数组上进行读操作,互不影响。

CopyOnWriteArrayList的set方法源码:

缺点:占用内存太多(要复制)

            修改的值不能立刻被读到

优点:在读多写少的场景下性能优秀,锁竞争很少。

HashTable   HashMap  ConcurrentHashMap 比较

HashTable 和 ConcurrentHashMap 是线程安全的哈希表类

HashMap线程不安全

HashTable 和 ConcurrentHashMap的比较:

HashTable的缺点:

1:枷锁方式

HashTable实现线程安全的方式是给关键方法加锁相当于针对对象加锁,当这个对象多次重复调用加锁方法时就会产生激烈的锁竞争,这时,程序就像是串行执行,极大影响了并发性。

2:扩容机制

HashTable计算出新的容量并进行初始化后,一股脑将所有元素逐一重新哈希,这时候我们就不能正常进行操作了,相当于突然就卡顿了一会儿。

ConcurrentHashMap做出的改进:

1:枷锁方式

ConcurrentHashMap缩小了锁的粒度,修改数据时,ConcurrentHashMap对哈希桶的每个链表分别加锁。

当我们修改不同链表上的数据时,根本不会产生线程安全问题,也就不会发生锁冲突。当修改同一个链表上的数据时,由于我们针对每个链表加锁,这时就会产生锁冲突,避免了线程安全问题。

本身,多线程修改到同一个链表上的数据的概率是很小的,所以我们这样的加锁方式大大减少了锁冲突,提高了并发性。

2:扩容机制:

ConcurrentHshMap采用了渐进式扩容方法:

在创建出新的数组后,ConcurrentHashMap逐渐的将旧的表上的数据往新的哈希表上挪,这个过程可能需要一段时间,在这个时间内,我们仍然可以进行操作,这个期间, 插入只往新数组加.这个期间, 查找需要同时查新数组和老数组

并且,我们每次进行操作也会连带着搬运一定量的数组,这样积少成多,逐步完成扩容过程。

3:只给写操作加锁,读操作不加锁了

读操作和写操作冲突的问题开发者通过其他手段解决掉了

4:引入CAS机制,使size++(对哈希表元素个数计数的变量)这样的操作不会引发线程安全问题

最无关紧要的区别:HashMap允许键和值为空,HashTable和ConcurrentHashMap不允许键和值为空。

          

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值