并发前言JUC工具类Callable 接口示例小结ReentrantLock 可重入锁使用优势劣势信号量 Semaphore示例CountDownLatch示例线程安全的集合类多环境使用ArrayList多线程环境使用队列多线程环境使用哈希表HashtableConcurrentHashMap全文总结 系列文章导航前言上一篇我们讲解了各类锁策略、CAS 无锁编程、synchronized 底层锁升级优化。本篇聚焦并发工程实战内容完整梳理 JUC 包核心工具类与各类线程安全集合覆盖 Callable、ReentrantLock、Semaphore、CountDownLatch 以及 ConcurrentHashMap 等高频面试考点配套可运行案例补齐 Java 并发编程业务落地相关知识点。JUC工具类JUCjava.util.concurrent这里的内容主要是一些多线程相关的组件。Callable 接口Callable 接口带有泛型参数的接口有返回值一种创建线程的方式。和 Runnable 对比1Runnable.run()无返回值2Callable.call()支持返回执行结果可抛出异常。注意Callable 不能直接传入 Thread 构造方法必须依靠FutureTask包装。示例实现使用Callable来创建线程计算123…1000。importjava.util.concurrent.Callable;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.FutureTask;publicclassMain{publicstaticvoidmain(String[]args)throwsExecutionException,InterruptedException{//定义任务CallableIntegercallablenewCallableInteger(){OverridepublicIntegercall()throwsException{intsum0;for(inti1;i1000;i){sumi;}returnsum;}};//把任务放到线程中执行//callable不能直接传给Thread的构造方法要借用中间的类包装一下//futureTask相当于号码牌拿着号码牌取结果FutureTaskIntegerfutureTasknewFutureTask(callable);ThreadtnewThread(futureTask);t.start();//此处的get就能获取到callable里面返回的结果//线程是并发执行执行到主线程的get时t线程可能还没执行完此时get就会阻塞System.out.println(futureTask.get());}}小结线程的创建方式继承Thread类重写run方法可以创建单独的类也可以使用匿名内部类。实现Runnable接口重写run方法可以创建单独的类也可以使用匿名内部类。实现Callable接口重写call方法可以创建单独的类也可以使用匿名内部类。使用lambda表达式。ThreadFactory线程工厂。线程池。ReentrantLock 可重入锁使用ReentrantLocklocknewReentrantLock();lock.lock();//加锁try{//working}finally{lock.unlock();//一定不能忘记unlock的调用}相对来说synchronized是Java的内置锁由JVM自动管理锁的获取与释放无需手动调用解锁方法因此不会出现因忘记解锁或异常导致锁无法释放的问题。优势ReentrantLock 在加锁时有两种方式lock与synchronized类似若遇到锁冲突就会一直阻塞等待trylock尝试加锁若没加上就放弃给了更多的可操作空间。ReentrantLock 提供了公平锁的实现默认情况下是非公平锁。ReentrantLock 提供了更强大的等待通知机制。搭配Condition类实现等待通知可显示唤醒几个线程。劣势ReentrantLock 使用起来复杂容易忘记解锁。加锁时首选synchronized背后还有一系列的优化手段。信号量 Semaphore信号量就是一个计数器描述了“可用资源”的个数。信号量用法每次申请一个可用资源计数器-1 ⇒ P操作原子操作acquire。每次释放一个可用资源计数器1 ⇒ V操作原子操作release。假设信号量初始值为10已经进行了十次P操作之后值为零此时继续进行P操作会阻塞等待。锁本质上就是一种特殊的信号量即可用资源为1的信号量值只可能为1和0也就是二元信号量。操作系统提供了信号量实现提供了APIJVM封装了这样的API就可以在Java代码中使用。示例publicclassMain{publicstaticvoidmain(String[]args)throwsInterruptedException{SemaphoresemaphorenewSemaphore(4);semaphore.acquire();System.out.println(P 操作);semaphore.acquire();System.out.println(P 操作);semaphore.acquire();System.out.println(P 操作);semaphore.acquire();System.out.println(P 操作);semaphore.acquire();System.out.println(P 操作);//semaphore.release();}}开发中如果遇到了需要申请资源的场景就可以使用信号量来实现了。CountDownLatchCountDownLatch适用于多个线程来完成一系列任务的时候用来衡量任务的进度是否完成。CountDownLatch 的方法await调用时会阻塞就会等待其他线程完成任务所有的线程都完成了任务之后此时await才会返回就会继续往下走。countDown告诉countDownLatch当前这一个子任务完成了。如果 countDown 调用总次数小于初始化数值await 会永久阻塞。示例publicclassMain{publicstaticvoidmain(String[]args)throwsInterruptedException{//十个选手参赛await会在10次调用完countDown之后才能继续执行CountDownLatchcountDownLatchnewCountDownLatch(10);//如果循环次数少于10await会一直处于阻塞状态for(inti0;i10;i){intidi;ThreadtnewThread(()-{System.out.println(thread id);try{Thread.sleep(500);}catch(InterruptedExceptione){thrownewRuntimeException(e);}//通知当前任务已经执行完毕countDownLatch.countDown();});t.start();}countDownLatch.await();System.out.println(所有任务都完成了);}}线程安全的集合类数据结构中大部分的集合类都是线程不安全的早期VectorStackHashTable线程安全自带synchronized。那么如何保证线程安全呢1.加锁。2.标准库也提供了一些搭配的组件保证线程安全。多环境使用ArrayList使用同步机制synchronized或ReentrantLock。Collections.synchronizedListnew ArrayList会返回一个新的对象这个新的对象相当于给ArrayList套了一层壳这层壳就是在方法上直接使用synchronized。使用CopyOnWriteArrayList写时拷贝1比如两个线程使用同一个ArrayList可能会读可能会写两个线程读就直接读取若某个线程需要进行修改就把ArrayList复制出来一个副本该线程的修改操作就是在修改这个副本与此同时另一个线程仍然可以读取数据是在原来的数据上读取一旦这边修改完毕就会使用修改好的副本替代掉原来的数据往往就是一个引用赋值。2当前操作的ArrayList不能太大拷贝成本太高。3更适合于一个线程修改多个线程读取的情况。4适合于服务器的配置更新可以通过配置文件来描述配置的详细内容配置的内容会被读到内存中再由其他的线程读取这里的内容但是修改这个配置内容往往只有一个线程来修改。⇒ 使用某个命令让服务器重新加载配置就可以使用写时拷贝的方式了。多线程环境使用队列ArrayBlockingQueue基于数组实现的阻塞队列。LinkedBlockingQueue基于链表实现的阻塞队列。PriorityBlockingQueue基于堆实现的带优先级的阻塞队列。TransferQueue最多只包含一个元素的阻塞队列。多线程环境使用哈希表HashMap本身不是线程安全的HashtableHashtable 保证线程安全主要就是给关键方法加上synchronized直接加到方法上相当于给this加锁。只要两个线程在操作同一个Hashtable就会出现锁冲突。1.在不考虑触发扩容的前提下操作不同的链表是线程安全的比如A和C。2.相比之下若两个线程操作的是同一个链表才容易出现问题比如A和B。3.若两个线程操作的不是同一个链表就不用加锁否则需要加锁。ConcurrentHashMapConcurrentHashMap 最核心的改进把一个全局的大锁改进成了每个链表独立的一把小锁(把每个链表的头结点作为锁对象synchronized可以使用任意对象作为锁对象)大幅度降低了锁冲突概率。一个哈希表有很多这样的链表两个线程恰好同时访问同一个链表的情况较少。ConcurrentHashMap 充分利用了CAS特性把一些不必要加锁的环节省略加锁了。比如使用变量记录hash表中的元素个数就可以使用原子操作(CAS)修改元素个数ConcurrentHashMap针对读操作没有加锁。ConcurrentHashMap 在底层编码过程在中处理了一些细节修改时会避免使用、–这种非原子的操作而是使用进行修改本身就是原子的因此读的时候要么读到写之前的旧值要么读到写之后的新值不会出现读到一个修改到一半的值。写和写之间还是要加锁的。ConcurrentHashMap针对扩容操作做出了单独的优化。本身HashMap和Hashtable在扩容时都需要把所有的元素都拷贝一遍若元素很多拷贝就比较耗时。化整为零一旦需要扩容确实需要搬运但不是在一次操作中搬运完成而是分成多次来搬运每次只搬运一部分数据避免单次操作过于卡顿。在Java8之前ConcurrentHashMap 是基于分段锁(多个链表共用一把锁)的方式实现的Java8之后就是直接在链表头结点加锁的形式。全文总结CallableFutureTask 实现带返回值线程ReentrantLock、Semaphore、CountDownLatch 开发场景与底层特性各类线程安全 List、阻塞队列、哈希表对比重点掌握 ConcurrentHashMap JDK8 优化细节。至此 Java 并发编程系列全部知识点讲解完毕整套专栏从基础线程、同步机制、锁、CAS、JUC 工具到并发容器完整闭环全部代码均可直接运行。欢迎点赞收藏整套专栏评论区交流面试问题 系列文章导航本篇是「Java并发编程系列」的连载内容点击链接查看完整系列 上一篇Java 并发编程六并发进阶高频CAS、锁升级 下一篇Java IOFile 文件操作 字节流 / 字符流完整笔记 递归删除文件实战 点击直达「Java并发编程」专栏合集