多线程(JUC(Callable接口,ReentrantLock类,原子类,线程池,Semaphore,CountDownLatch),线程安全集合类)

JUC

JUC:Java.util.Concurrent,是Java中提供的供并发操作使用的一个包.

Callable接口

  1. 概念:Callable是一个interface,也是一种创建线程的方式,和Runnable类似,Callable也是描述一个任务,但不同的是Callable是一个有返回值的任务.
  2. 适用场景:Callable适合去进行一个希望通过计算得到一个结果的任务.
  3. 代码实现:通过Callable实现数字的求和运算
 static class Result{
        public int sum = 0;
    }
    //用Runable实现的数字求和运算,因为Runnbale是一种没有返回值的任务,所以需要单独设计一个类来描述返回值.比较繁琐.
    public static void main1(String[] args) throws InterruptedException {
        Result result = new Result();
        Thread t = new Thread(()->{
            int sum = 0;
           for(int i = 0; i <= 1000; i++){
               sum += i;
           }
           result.sum = sum;
        });
        t.start();
        t.join();
        System.out.println(result.sum);
    }
    ===========================================================================
    //用Callable实现的数字求和运算.对于Callable而言,其返回值由FutureTask来接收.
    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;
            }
        };
        FutureTask<Integer> f = new FutureTask<>(callable);
        Thread t = new Thread(f);
        t.start();
        System.out.println(f.get());
    }

ReentrantLock

  1. 概念:可重入互斥锁,用以保证线程安全.
  2. 用法:
    1. lock():加锁,如果获取不到锁就陷入死等
    2. trylock(超时时间):加锁,如果获取不到锁等待一段时间如果没有获取到锁就放弃加锁.
    3. unlock():解锁.
  3. 使用演示
	ReentrantLock r = new ReentrantLock();
        r.lock();
        try{
            //加锁的线程中需要完成的任务
        }finally {
            r.unlock();
        }
  1. ReentrantLock与synchronized的区别
    1. synchronized是一个关键字,是JVM内部实现的(基于C++),而ReentrantLock是Java中的一个类,是基于Java实现的.
    2. synchronized是自动释放锁,而ReentrantLock必须通过unlock()手动释放锁.
    3. synchronized在获取不到锁之后会陷入死等状态,RenntrantLock可以通过tryLcok()方法等待一段时间之后就放弃.
    4. synchronized是非公平锁,ReentrantLock默认是非公平锁,但可以通过将构造函数中的参数设置为true来变成公平锁.
    5. synchronized的阻塞机制是依据wait-notify完成的,当有多个线程处于阻塞状态时,notify唤醒某个线程是随机的;ReentrantLock的阻塞机制是基于Condition类完成的,在唤醒线程时可以指定唤醒某个线程.
  2. synchronized和ReentrantLock的使用场景
    1. 当锁竞争比较小时,用synchronized,效率更高,自动释放锁的机制更方便.
    2. 当锁竞争比较大时,用ReentrantLock,搭配tryLock方法能够更灵活,节省不必要的忙等消耗.
    3. 当需要用到公平锁时,用ReentrantLock.

原子类

  1. 原子类内部是基于CAS(CAS可参考多线程(锁策略,CAS,synchronized锁原理和锁优化))实现,性能相较于i++高很多,因为基于CAS实现的,所以不需要考虑线程安全的问题.
  2. 原子类的常用类:
    1. AtomicInteger
    2. AtomicBoolean
    3. AtomicLong
  3. AtomicInteger的常用方法:
    1. addAndIncrement(num),"等价于"i+=num
    2. decrementAndGet(),“等价于”–i
    3. getAndDecrement(),"等价于"i–
    4. incrementAndGet(),“等价于”++i
    5. getAndIncrement()."等价于"i++
  4. 使用演示
 public static void main(String[] args) throws InterruptedException {
        AtomicInteger a = new AtomicInteger();
        int n = 10;
        for(int i = 0; i < n; i++){
            Thread t = new Thread(()->{
                a.incrementAndGet();
            });
            t.start();
        }
        Thread.sleep(1000);
        System.out.println(a.get());
    }

线程池

多线程案例(单例模式(饿汉-懒汉),阻塞队列,定时器,线程池)这篇文章中我们已经介绍过了线程池.在这里我们再详细介绍一下线程池的ThreadPoolExecutor
之前介绍的Executors实际上是ThreadPoolExecutor的封装.在ThreadPoolExecutor的构造方法中存在多个参数,通过设置这些参数来使线程池达到不同的效果
在这里插入图片描述

ThreadPoolExecutor中的各个参数
如果将ThreadPoolExecutor理解为一个公司.

  • coorPoolSize:正式员工的数量
  • maximumPoolSize:正式员工和临时员工的数量
  • keepAliveTime(临时工允许的空闲时间)
  • unit:keepAliveTime的单位
  • workQueue:传递任务的阻塞队列
  • threadFactory:创建线程的工厂,参与具体的创建线程的工作
  • RejectedExecutionHandler:拒绝策略.当任务量超出公司的负荷时的处理手段
    1. AboryPolicy():超过负荷直接抛出异常
    2. CallerRunsPolicy():调用者负责处理
    3. DiscardOldestPolicy():丢弃队列中最老的任务
    4. DiscardPolicy():丢弃新来的任务

线程池的工作流程:在这里插入图片描述

信号量Semaphore

信号量:表示"可用资源"的个数.本质上是一个计数器.锁是一种特殊的信号量,被称为二元信号量(申请锁,释放锁).锁的可用资源就1个.而信号量的可用资源有多个.信号量有2个主要操作分别是P操作和V操作.P操作就是申请一个可用资源,V操作就是释放一个可用资源.Semaphore中的PV操作中的加减计数器的操作是原子性的,保证了多线程情况下是安全的.

CountDownLatch

CountDownLatch:同时等待多个任务结束.
用CountDownLatch模拟运动员跑步的过程

 CountDownLatch latch = new CountDownLatch(10);
      Runnable runnable = new Runnable() {
          @Override
          public void run() {
              try {
                  //等待发令枪起跑.
                  Thread.sleep(100000);
                  //开始跑步
                  //跑步结束计数器-1
                  latch.countDown();

              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      };
        for(int i = 0; i < 10; i++){
            //每个线程代表一个运动员
            Thread t = new Thread(runnable);
            t.start();
        }
        //等待所有"运动员"跑完之后结束比赛
        latch.await();
        

线程安全集合类

顺序表

Java中的ArrayList类是线程不安全的,解决线程不安全的方法有:

  1. 手动添加synchronized或者ReentrantLock
  2. 使用CopyOnWriteArrayList(new ArrayList).CopyOnWriteArrayList:写时拷贝.当向容器中添加元素时不会往原有的容器中添加而是将原有容器进行复制,然后在复制后的容器中进行元素的更新操作.然后再让旧容器指向复制后的容器
    1. CopyOnWriteArrayList的优点:适用于读多写少的情景,相当于读写锁的功能.
    2. CopyOnWriteArrayList的缺点:不能第一时间获取到修改后的元素,占用内存较多(复制)

队列

在多线程中使用线程安全的队列的类:

  1. ArrayBlockingQueue:基于数组实现的阻塞队列
  2. LinkedListBlockingQueue:基于链表实现的阻塞队列
  3. PriorityBlockingQueue:基于堆实现的优先级阻塞队列

哈希表

Java中的HashMap本身是线程不安全的.

HashTable

HashTable在解决线程安全方面是十分粗糙的,只是将对应的数据更新的方法整体加上了synchronized关键字.这相当于对HashMap对象加上了锁,在触发扩容的时候,是由单一线程完成的扩容,效率比较低.

ConcurrentHashMap

相对于HashTable,ConcurrentHashMap在线程安全方面做出了一系列的改进和优化

  1. 对于读操作是不加锁的,但为了保证读到的是刚修改的数据,使用了volatile关键字.对写操作利用synchronized加锁(java7之前是利用的lock),但不是对对象本身加锁,而是进行"锁桶"(为每个链表的头结点进行加锁)
  2. 充分利用了CAS特性.如在更新size是利用CAS大大提高了size更新的效率
  3. 优化了扩容方式:化整为零.
    当某个线程发现需要扩容时,并不是完整地进行扩容,而是只创建一个数组,然后将原来数组中的部分元素复制到新的数组中.新老数组是同时存在的,当后续线程进行操作时,分别将老数组中的部分元素转移到新的数组中.当执行数据插入,添加操作时,往新数组中插入,进行查询或删除操作时新老数组同时查询或删除.当老数组中的所有元素都被转移到新数组中后删掉旧的数组.

死锁

  1. 概念:死锁指的是多个线程同时处于阻塞状态且等待锁资源的释放,但因为线程处于一种无限循环状态,导致程序不可能终止.
  2. 死锁就类似于"哲学家就餐问题":有N个哲学家围着一张圆桌进行就餐,每个哲学家的手两侧分别放着一只筷子,也就是说如果哲学家A在哲学家B的左侧,那么哲学家B的左筷子就是哲学家A的右筷子.这些哲学家们需要做的事情有2个:吃饭,思考人生.由于哲学家非常固执,所以必须在完成一件事情之后才能进行下一件事情.当某个哲学家饿了的时候,他先拿起左手边的筷子,然后再拿起右手边的筷子.吃完之后把筷子放下然后去思考人生.这本是一件非常和谐的事情,但存在一种情况:在某个时刻N个哲学家同时饿了,也就意味着这N哥哲学家同时拿起了自己左手边的筷子,这时所有哲学家都发现自己右手边的筷子"消失"了,所以他猜测是旁边的哲学家在吃饭,所以他要"等"别人吃完饭放下筷子以后他再吃.这就陷入了一种死循环的状态.
  3. 死锁产生的4个必要条件.
    1. 互斥使用:当一个线程占有锁资源时,其他线程使用.
    2. 不可抢占:当一个线程占有锁资源时,其他线程不能抢占锁资源
    3. 请求和保持:当某个线程在申请其他资源的同时保持对原有资源的占有.
    4. 循环等待:即存在一个循环回路:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,导致形成了一个等待环路.
  4. 解决死锁的办法:破坏循环等待,给资源进行编号.
    拿我们上述举的"哲学家就餐问题"来说,我们对每根筷子按照顺时针方向进行递增编号.即1~N.并规定每个哲学家在进餐时需要先拿起相邻的编号小的筷子,然后再拿起相邻编号大的筷子.这时当所有哲学家同时要进餐时,当N-1个哲学家都拿起了自己左手边编号较小的筷子时,第N个哲学家左右两侧的筷子编号分别为N和1,根据规则,该哲学家需要先拿编号小的筷子1,但因为筷子1已经被第一个拿起筷子的哲学家给占有了,所以第N个哲学家处于等待的过程.这时编号为N的筷子就可以被第N-1个哲学家占有,当第N-1个哲学家吃完之后将他的左手边的筷子放下,这时第N-2个哲学家接着进餐,依次往返,知道第N个哲学家进餐完成.因此等待环路就被破坏.程序就不会进入无终止状态,也就不会产生死锁.

总结

本篇文章介绍了多线程的JUC包中的各个线程安全的类.同时也介绍了线程安全的集合类和死锁的原理和解决方案.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

囚蕤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值