Java~了解并发编程JUC中的重要组件(atomic、locks、ConcurrentHashMap)

文章目录

      • 原子类java.util.concurrent.atomic
      • java.util.concurrent.locks
      • Callable/Future/FutureTask
      • Executors (ThreadPoolExcutor)
      • Semaphore信号量
      • ReentrantLock可重入互斥锁
      • CountDownLatch
      • ConcurrentHashMap(重点)
        • 对比HashMap、HashTable、ConcurrentHashMap

原子类java.util.concurrent.atomic

  • 原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

AtomicBoolean/AtomicInteger/AtomicIntegerArray/AtomicLong/AtomicReference/AtomicStampedReference

java.util.concurrent.atomic

java.util.concurrent.locks

  • 包含了很多其他的锁,

ReentrantLock的加锁和解锁操作是分开的 使用更加灵活
常见方法
tryLock 付过获取锁失败是不会挂起等待的而是直接放弃等待
locklnterruptibly 设置等待锁的过程中是否会被异常中断

  • 如果用户想自己实现锁, 就可以实现这个借口, 进一步开发 所以这个包的主要作用就是让用户实现一些自己定制的锁
    java.util.concurrent.locks

Callable/Future/FutureTask

  • 使用Callable和FutureTask也可以创建线程 (创建的是带返回值的线程 借助这个线程可以得到结果)
    创建线程的几种方式

Executors (ThreadPoolExcutor)

  • 线程池 主要用途就是避免频繁的创建和销毁线程
    为什么要引入线程池? 如何自己简单实现一个线程池?

Semaphore信号量

  • 信号量本质就是一个计数器, 表示可用资源的个数 如果要申请资源 计数器就-1 § 释放资源计数器就+1 (V) (P和V操作是原子的)
  • 类似一个停车场一样 来一辆车显示停车数目加一 走一辆 显示停车数目就减一
  • 有一个叫二元信号量只有0和1俩个取值 这个信号量也可以当成是一个简单的锁

ReentrantLock可重入互斥锁

  • 最大区别就是对于Synchronized来说,synchronize是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
  • 便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
  • 锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

CountDownLatch

  • 这个创建的线程就好比是一场赛跑一样 所有的选手都跑完了才会宣布比赛结束 等所有线程都结束才会认为这个执行结束
import java.util.concurrent.CountDownLatch;

/**
 * Created with IntelliJ IDEA.
 * Description: If you don't work hard, you will a loser.
 * User: Listen-Y.
 * Date: 2020-08-05
 * Time: 17:45
 */
public class Demo1 {

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            thread.start();
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //10个都执行完才会输出下面语句
        System.out.println("结束");
    }

}

ConcurrentHashMap(重点)

  • 简单的集合大多是线程不安全的 常见的线程安全的集合有Vector Stak HashTable(及建议使用效率很低) 再就是ConcurrentHashMap
  • 并发编程实践中,ConcurrentHashMap是一个经常被使用的数据结构,它的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响
  • 读取数据是不需要加锁,但不保证一定读到最新的数据。
    写入时,不涉及扩容,则对链表的第一个结点进行加锁,即只有下标一样的线程才争抢同一把锁。如果发生了扩容,则情况就非常复杂了;为了效率,它支持多个线程同时进行扩容,而且没有加锁。

对比HashMap、HashTable、ConcurrentHashMap

  • 一个最本质的区别就是HashMap是线程不安全的 其他俩个是线程安全的
  • HashTable使用是最简单粗暴的synchronize加锁来实现线程安全 这简直就是一把大锁控制所有 这就意味着只要针对这个实例进行操作 都必需先申请锁 冲突的概率比较大 所以比较底下
  • ConcurrentHashMap的设计就很巧妙 他的内部每一个桶都有一把锁 也就是每次操作的时候只有是同一个哈希值的线程才会去竞争对应的锁 这就大大降低了冲突的概率 也就提高了效率
  • 还有在ConcurrentHashMap中他做到了能尽量使用CAS操作修使用CAS操作完成(比如一些++操作)
  • 并且在ConcurrentHashMap中有更优化的扩容方案

HashTable的扩容就是一鼓作气 某个元素插入后出发了扩容 就得由该线程把整个hash表都扩容后才能算是插入完毕 (耗时很久 很麻烦)
ConcurrentHashMap就是使用大事化小的思路 当需要扩容的时候 不会由某一个线程单独的一次性完成扩容操作 而是由后续的一系列操作都要进行扩容 (每次只扩容一点点) 这样就可以避免某个线程出现耗时很大的时候

你可能感兴趣的:(Java,多线程,并发编程,java,面试)