【个人学习】 Java 并发编程1

【个人学习】 Java 并发编程1 前言多线程可能早就听过很多次了但是具体多线程是什么干什么的需要详细了解一下。我们刚开始写的普通的程序是单线程例如第一行移动小球 A第二行播放背景音乐第三行移动小球 B在单线程环境下如果“移动小球 A”是一个需要运行 10 秒的死循环那么背景音乐永远不会响小球 B 也会纹丝不动。这叫阻塞。他是怎么做到这三件事情同时运行的呢这就是多线程的作用。1.多线程--进程与线程我们要问计算机是如何同时运行多个程序的想象你的电脑是一个巨大的工业园区。每一个运行中的软件如 IDE、浏览器都是园区里的一家工厂。在操作系统层面这被称为进程Process。每个工厂都有自己独立的电力系统、地皮和仓库内存空间互不干扰。那么工厂内部是如何工作的呢工厂里的一名名工人就是线程Thread。所有工人共享这个工厂的原材料和空间但每个工人同时只能专注做一件事情。为什么要有多线程如果工厂只有一个工人当他需要等待快递员送货网络 I/O时整个工厂就停工了。但如果有多个工人一个人在等货其他人可以继续切菜或打扫。这就是多线程的核心意义充分利用 CPU 资源不让昂贵的处理器处于闲置状态。2.线程安全问题多线程带来了效率也带来了混乱。矛盾点在哪由于线程工人共享进程工厂的资源当两个工人同时冲向同一个账本去写字时灾难就发生了。观察一个著名的现象原子性缺失。假设有一个变量 count 0。线程 A 和线程 B 同时执行 count。在底层count 并不是一步完成的它分为三步读取 count 的值将值加 1将结果写回。如果 A 读到了 0还没来得及加 1B 也读到了 0。结果是 A 写回了 1B 也写回了 1。原本应该等于 2 的结果现在变成了 1。这就是线程不安全。为了解决它我们需要引入“秩序”。3.锁如何保证账本不会被写乱最直观的办法是加锁。1. synchronizedJava 的内置“自动锁”synchronized 是 Java 最基础的方案。它就像是在操作间门口加了一把感应锁当一个工人进去后门自动锁上其他人在外面排队。只有里面的工人出来了下一个人才能进去。它简单、安全但不够灵活。package com; public class Text { private int count 0; // 共享的账本 public void add() { count count 1; } public static void main(String[] args) throws InterruptedException { Text shop new Text(); // 两个工人每人负责往账本上加 10000 次 Thread workerA new Thread(() - { for (int i 0; i 10000; i) shop.add(); }); Thread workerB new Thread(() - { for (int i 0; i 10000; i) shop.add(); }); workerA.start(); workerB.start(); // 等两个工人都干完活 workerA.join(); workerB.join(); // 理论上应该是 20000但实际运行结果通常会小于 20000 System.out.println(理论值应该是20000); System.out.println(实际运行结果 shop.count); } }加上synchronized后public synchronized void add() { count count 1; }2. ReentrantLock更高级的“手动锁”有时候我们希望锁能更智能。比如“如果锁被占用了我等 5 秒钟等不到我就去干别的。”tryLock“我想让读账本的人可以同时进去但写账本的人必须独占。”ReadWriteLock这就是 java.util.concurrent.locks 包提供的能力它让开发者对锁有了精细化的控制。private final ReentrantLock lock new ReentrantLock();public void smartAdd() {// 尝试拿锁拿不到就算了不等了if (lock.tryLock()) {try {count;System.out.println(Thread.currentThread().getName() 抢到锁并加了1);} finally {lock.unlock();}} else {// 没抢到锁我不傻等先去干别的System.out.println(Thread.currentThread().getName() 没抢到锁先去喝杯咖啡);}}4.线程池 (Thread Pool)当我们的工厂规模变大新的问题出现了雇佣和解雇工人的成本太高了。在 Java 中创建一个线程需要向操作系统申请资源这是很重的操作。如果我们为每个小任务都创建一个新线程任务结束后再销毁CPU 的大量时间都会浪费在“招人”和“开除人”上。线程池的逻辑我们预先招好 5 个核心员工核心线程数 corePoolSize让他们常驻工厂。任务来了员工去处理。任务太多了员工忙不过来就让任务在休息室排队工作队列 workQueue。如果休息室也挤满了我们临时再招几个临时工最大线程数 maximumPoolSize。如果连临时工也招满了任务还在涌入我们就不得不拒绝接收新任务拒绝策略 RejectedExecutionHandler。public class SimplePool { public static void main(String[] args) { // 1. 创建一个固定只有 2 个线程的池子 ExecutorService pool Executors.newFixedThreadPool(2); // 2. 连续丢进去 5 个任务 for (int i 1; i 5; i) { int taskId i; pool.execute(() - { System.out.println(Thread.currentThread().getName() 正在处理任务 taskId); try { Thread.sleep(1000); } catch (InterruptedException e) {} }); } // 3. 关闭线程池 pool.shutdown(); } }通过线程池Java 实现了对资源的极致复用。5.AQS 与 CAS锁和线程池是“产品”手机而 CAS 和 AQS 是“零件和组装图纸”芯片和电路最底层CAS原子操作这是最微小的动作像是一个开关。它保证了“改数字”这个动作不会出错。中间层AQS排队框架有了开关CAS还不够如果 100 个线程都在抢开关抢不到的人怎么办得有人管他们排队、睡觉、唤醒。这个“管家”就是AQS。产品层ReentrantLock / CountDownLatch / 线程池1. CAS (Compare And Swap)这是现代并发编程的基石。它不加锁而是采用“对比并交换”的策略。工人 A 拿着计算好的结果“1”准备写回账本时会先看一眼账本现在是不是还是他当初看到的“0”。如果是 0说明没人动过写入成功。如果不是 0说明被别人抢先了A 放弃写入重新读取、重新计算。这在 Java 中体现为 AtomicInteger 等原子类性能极高。2. AQS (AbstractQueuedSynchronizer)无论是 ReentrantLock 还是 Semaphore它们背后都有一个共同的“大脑”——AQS。AQS 维护了一个状态位state和一个等待队列。state 0房门开着。state 1有人在里面。队列没抢到房门钥匙的人都在队列里睡觉挂起等待被唤醒。6.JMM (Java 内存模型)为了运行速度CPU 不会每次都直接读写内存而是会把数据缓存在自己的 L1、L2 缓存里。这就导致了一个诡异的问题线程 A 修改了变量 X由于 X 还在 A 的 CPU 缓存里线程 B 在另一个 CPU 核心上看到的 X 还是旧值。Java 的应对volatile 关键字当你给一个变量加上 volatile你就给了它“可见性”。它要求所有线程每次读变量必须从主内存读每次改变量必须立即刷回主内存。它就像在工厂里装了一个巨大的实时显示屏任何数据的变动所有工人都立刻可见。