
翟陆续 薛宾田
Java并发编程无处不在,涉及的知识点多,要掌握并用好它并非易事。作者加多拥有在大型互联网公司阿里巴巴的丰富工作经验,遇到并解决了业务场景中很多实际的并发问题。本书是他对自己实践经验的总结与升华。为帮助读者解决学习中的各类痛点,作者将全书明确地分为基础篇、高级篇和实践篇,脉络清晰;全书以代码说话,辅以图表,让初学者能一步一步地深入堂奥,掌握并发编程的精髓。
本书主要讲述Java并发相关知识,内容包括以下几个部分: 1. 线程基础 2. 关键字 3. ThreadLocal类 4. CAS与AQS 5. 原子操作类 6. Java锁机制 7. Java并发容器 8. Java同步器 9. 线程池ThreadPoolExecutor 线程基础 1. 一个JVM的进程中,main函数所在的线程就是被称为主线程。 2. 线程调用Object.wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回: 其他线程调用了该共享对象的notify()或者notifyAll()方法 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。 3. 如果调用wait()的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。该崩溃是一个常见崩溃。 4. 当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果该线程还持有其他共享变量的锁,则这些锁是不会被释放的。在使用的时候,这里可能导致死锁的发生。 5. 调用notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。 6. notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。如果调用notifyAll()方法后一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的。 7. Thread.join()方法,用于等待某个线程结束运行,调用者线程会被阻塞。 8. Thread.sleep()方法,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。调用线程在sleep时间到了,或者被中断时,会继续参与调度。 9. Thread.yield()方法是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。 10. sleep与yield区别:当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。 11. Thread.interrupt() 仅仅是设置标志,线程实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。 12. 当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。在编程时,要避免频繁的进行线程上下文切换。 13. Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。main线程运行结束后,JVM会自动启动一个叫作DestroyJavaVM的线程,该线程会等待所有用户线程结束后终止JVM进程。 关键字 1. synchronized是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。 2. synchronized的内存语义是:把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。 3. volatile变量:线程在写入该变量时,不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。 4. volatile虽然提供了可见性保证,但并不保证操作的原子性。volatile还有一个语义是:避免指令重排。 ThreadLocal类 1. ThreadLocal是线程本地变量,访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。 2. ThrealLocal原理:Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。每个线程的本地变量都是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。 3. 本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。 4. ThreadLocal不支持继承性,如果希望子类继承,也需要利用InheritableThreadLocal。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。 5. ThreadLocalRandom类是JDK 7在JUC包下新增的随机数生成器,它弥补了Random类在多线程下的缺陷。 CAS 1. CAS即Compare and Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性。 2. CAS的局限性: CAS会存在ABA问题 CAS操作一般都在一个无限循环里面调用,直到操作成功才返回。如果并发量大,变量被频繁修改,则会导致效率低下。 CAS只能对一个变量进行原子操作。 3. CAS可被看作是乐观锁的一种。它提供了非阻塞的原子性操作。 4. CAS利用sun.misc.Unsafe类实现,Unsafe核心方法都是native方法,这些native方法最终都是调用Atomic::cmpxchg,该函数利用汇编实现。单核处理器利用cmpxchgl指令实现,多核处理器利用lock前缀的cmpxchgl实现。 5. 只有Bootstrap类加载器加载的Class才能调用Unsafe类。 AQS 1. AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。 2. 在AQS中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。在不同的锁中,该变量含义不一样。 3. ReentrantLock的实现来说,state表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数;对于CountDownlatch来说,state用来表示计数。 4. AQS有个内部类ConditionObject,用来结合锁实现线程同步。ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程,如类图所示,这个条件队列的头、尾元素分别为firstWaiter和lastWaiter。 5. 在独占方式下获取和释放资源使用的方法为: void acquire(int arg) void acquireInterruptibly(int arg) boolean release(int arg)。 6. 在共享方式下获取和释放资源的方法为: void acquireShared(int arg) void acquireSharedInterruptibly(int arg) boolean releaseShared(int arg)。 7. AQS类并没有提供可用的tryAcquire和tryRelease方法 。tryAcquire和tryRelease需要由具体的子类来实现。 8. synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量。在每个条件变量内部都维护了一个条件队列,用来存放调用条件变量的await()方法时被阻塞的线程。 9. AQS只提供了ConditionObject的实现,并没有提供newCondition函数,该函数用来new一个ConditionObject对象。需要由AQS的子类来提供newCondition。 原子操作类 1. JUC并发包都是基于CAS实现。其中包含有AtomicInteger、AtomicLong和AtomicBoolean等原子性操作类,底层也都是基于CAS实现。 2. AtomicLong的性能瓶颈是由于过多线程同时去竞争一个变量的更新而产生的。LongAdder用来克服在高并发下使用AtomicLong的缺点。 3. LongAdder原理:LongAdder在内部维护多个Cell变量,每个Cell里面有一个初始值为0的long型变量,这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。另外,多个线程在争夺同一个Cell原子变量时如果失败了,它并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的。 4. LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base。由于Cells占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。 5. LongAdder类是LongAccumulator的一个特例,LongAccumulator比LongAdder的功能更强大。 Java锁机制 1. 悲观锁:指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。 2. 乐观锁:乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。 3. 公平锁与非公平锁:根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。而非公平锁则在运行时闯入,也就是先来不一定先得。 4. 独占锁与共享锁:独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。共享锁则可以同时由多个线程持有,例如ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。 5. 可重入锁:当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时,如果不被阻塞,那么我们说该锁是可重入的。 6. 自旋锁:当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销。 7. synchronized内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。 8. ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。 9. ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。读写锁的内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。 10. StampedLock是并发包里面JDK8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个long型的变量,我们称之为戳记(stamp),这个戳记代表了锁的状态。其中try系列获取锁的函数,当获取锁失败后会返回为0的stamp值。当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp值。 11. StampedLock的三种模式: 写锁writeLock:是一个排它锁或者独占锁,某时只有一个线程可以获取该锁,当一个线程获取该锁后,其他请求读锁和写锁的线程必须等待,这类似于ReentrantReadWriteLock的写锁 悲观读锁readLock:是一个共享锁,在没有线程获取独占写锁的情况下,多个线程可以同时获取该锁。 乐观读锁tryOptimisticRead:它是相对于悲观锁来说的,在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算测试。 12. StampedLock的读写锁都是不可重入锁,所以在获取锁后释放锁前不应该再调用会获取锁的操作,以避免造成调用线程被阻塞。当多个线程同时尝试获取读锁和写锁时,谁先获取锁没有一定的规则,完全都是尽力而为,是随机的。 Java并发容器 1. ConcurrentLinkedQueue是线程安全的无界非阻塞队列,其底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。 2. LinkedBlockingQueue使用独占锁实现的阻塞队列。默认队列容量为0x7fffffff,用户也可以自己指定容量,所以从一定程度上可以说LinkedBlockingQueue是有界阻塞队列。 3. LinkedBlockingQueue有两个ReentrantLock的实例,分别用来控制元素入队和出队的原子性,其中takeLock用来控制同时只有一个线程可以从队列头获取元素,其他线程必须等待,putLock控制同时只能有一个线程可以获取锁,在队列尾部添加元素,其他线程必须等待。另外,notEmpty和notFull是条件变量,它们内部都有一个条件队列用来存放进队和出队时被阻塞的线程,其实这是生产者—消费者模型。 4. LinkedBlockingQueue的内部是通过单向链表实现的,使用头、尾节点来进行入队和出队操作,也就是入队操作都是对尾节点进行操作,出队操作都是对头节点进行操作。 5. ArrayBlockingQueue使用有界数组方式实现的阻塞队列。 6. 相比LinkedBlockingQueue, ArrayBlockingQueue的size操作的结果是精确的,因为计算前加了全局锁。 7. PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素。其内部是使用平衡二叉树堆实现的,所以直接遍历队列元素不保证有序。默认使用对象的compareTo方法提供比较规则,如果你需要自定义比较规则则可以自定义comparators。 8. DelayQueue并发队列是一个无界阻塞延迟队列,队列中的每个元素都有个过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最快要过期的元素。 9. DelayQueue内部使用PriorityQueue存放数据,使用ReentrantLock实现线程同步。 线程同步器 1. CountDownLatch:CountDownLatch是使用AQS实现的。通过下面的构造函数,你会发现,实际上是把计数器的值赋给了AQS的状态变量state,也就是这里使用AQS的状态值来表示计数器值。 2. CountDownLatch与join区别: 调用一个子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而CountDownLatch则使用计数器来允许子线程运行完毕或者在运行中递减计数,也就是CountDownLatch可以在子线程运行的任何时候让await方法返回而不一定必须等到线程结束。 使用线程池来管理线程时一般都是直接添加Runable到线程池,这时候就没有办法再调用线程的join方法了,就是说countDownLatch相比join方法让我们对线程同步有更灵活的控制。 3. 当线程调用CountDownLatch对象的await方法后,当前线程会被阻塞,直到下面的情况之一发生才会返回: 当所有线程都调用了CountDownLatch对象的countDown方法后,也就是计数器的值为0时; 其他线程调用了当前线程的interrupt()方法中断了当前线程,当前线程就会抛出InterruptedException异常,然后返回。 4. CyclicBarrier:回环屏障,它可以让一组线程全部达到一个状态后再全部同时执行。这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await方法后,线程们就会冲破屏障,继续向下运行。 5. CyclicBarrier基于独占锁实现,本质底层还是基于AQS的。内部有两个变量parties和count。parties用来记录线程个数,这里表示多少线程调用await后,所有线程才会冲破屏障继续往下运行。count变量记录当前需要等待的线程数量。 6. CycleBarrier与CountDownLatch的不同在于,前者是可以复用的,并且前者特别适合分段任务有序执行的场景。 7. Semaphore信号量:它内部的计数器是递增的,并且在一开始初始化Semaphore时可以指定一个初始值,但是并不需要知道需要同步的线程个数,而是在需要同步的地方调用acquire方法时指定需要同步的线程个数。 8. Semaphore默认采用非公平策略。公平性策略,还是靠hasQueuedPredecessors这个函数来保证的。 好了,就到这里~
这本书应该是我耗费时间最长的一本书,按照阿里大佬的事例及看源码一步步跟过来的,从什么是线程,线程状态,API,死锁,线程安全,内存可见性,原子性,关键字volatile,synchronized,AQS,CAS,伪共享,lock,并发包中三大同步工具(CountDownLatch,回环屏障CylicBarrier,信号量Samephone)与join的对比及优化,线程池的创建及注意事项,ConcurrentHashMap在实际项目中的注意事项,SimpleDateFormat的线程不安全问题及解决办法,使用线程池之后需要释放资源,ThreadLocal的若引用带来的OOM问题用完之后需要remove....等等,感恩
【第一幕:田埂遇书】 前日去后生仔屋檐底下避雨,见他案头摆着本《Java并发编程之美》,封皮上印着"翟陆续、薛宾田"两个名字,倒像是两根麻绳捆着半袋陈谷子。我眯眼瞄着目录里"线程""锁""死锁"的字样,忽地想起那年赶集撞见两头犟驴——一个往东拉磨盘,一个往西拽石碾,最后把驴缰绳都拧成麻花辫。 "老哥你看,这书讲的是让千百头驴子乖乖拉磨的法子哩!"后生仔不知何时冒出来,手里还攥着半块啃剩的芋头糕。我啐了口旱烟沫:"莫哄俺,驴子听话还要你拿鞭子抽?" 【第二幕:灶台说理】 后生仔把书往柴火灶边一撂,火星子溅到泛黄的纸页上:"您老见过村口老槐树没?树杈上挂十只破箩筐,十只麻雀争着啄米粒——这就叫并发!"他蘸着米汤在桌上画圈圈:"Java并发就像咱插秧,要讲究个'深浅均匀不伤根'。" 我抄起竹篾编的簸箕比划:"莫不是教人拿竹竿赶麻雀?"后生仔直摇头:"您这法子顶多算同步锁(synchronized),饿死三只麻雀,撑肥七只田鼠!"灶膛里噼啪炸响的火星,倒像是代码里乱蹦的NullPointerException。 【第三幕:月光夜话】 三更天打更的梆子响了三响,后生仔还在院里踱步:"您知道线程池为啥像咱村水车吗?"他指着月光下泛银光的溪流:"流水不争先,争的是滔滔不绝。Executor框架就像六叔公那架百年水车,转得再欢实,也得守着'三不原则'——不空转、不卡壳、不翻车。" 我咂着旱烟袋突然乐了:"莫不是要给线程唱《天净沙》?"顺口溜道: "枯藤线程老树, 锁头昏鸦争渡。 线程池里赶路, 伪共享处, 原子操作如故。" 后生仔拍大腿笑出泪花:"老哥这曲儿比代码注释还传神!您看这伪共享(False Sharing),就像晒谷场上的箩筐挨太近,筛米的婆娘们胳膊肘总撞一块儿。" 【第四幕:鸡鸣收网】 五更鸡叫头遍时,后生仔往我怀里塞了本包了油纸的书:"您老就当看《西厢记》——表面上张生崔莺莺谈情说爱,底下藏着十八般武艺。"晨雾里他指着书脊上"2018"的烙印:"这可是十年陈的'并发秘笈',比咱腌了三冬的酸笋还经得起熬。" 我摸着书页里密密麻麻的代码,忽觉像极了当年在祠堂抄的《千家诗》。正要开口,村口传来拖拉机突突声——那声音倒像是线程饥饿(Starvation)时发出的哀鸣。 【尾声:田垄新悟】 如今我插秧总带着这本书,遇着秧苗倒伏就想起"线程安全",看见蚂蟥吸血就记起"内存泄漏"。前日教三娃子认字,竟把"volatile"念成"忽儿漏",倒比教科书还传神。想来这并发编程,原与种地同理——讲究个春种秋收时序,忌讳贪多嚼不烂。 后生仔说得在理:"你老登把synchronized叫'木门闩',把CAS操作比作'抢绣球',这书啊,就是给咱庄稼汉写的《农政全书》!"这话糙理不糙,就像咱粤西的艾糍,初尝扎嘴,细品回甘。 🤣 线程似那穿帘燕,锁头如铁护花钿。 CAS轻点,伪共享惊散, 望长街车水马龙喧。 且把代码作诗篇, 笑看那并发舞翩跹。
前面的部分是针对并发的基础概念的简单描述,均是点到即止,没有深入。而且存在一些问题,就是作者没有考虑读者的角度。例如在线程死锁的时候已经提到监视器锁,单其实读者在这个时候并未有所了解,看起来感觉非常突兀。基础知识的部分,均是概念然后加上代码的演示。随后全书均是对juc的源码的解读,解读过程沉闷,相信很少读者能看完并从中有所收获。无疑,作者对JUC相关的源码都很熟悉,技术功底不错。但全书没有看到整个体系,更谈不上“之美”。仅推荐需要面试的同学抱佛脚用。
对于我这类没有太多机会接触并发编程的开发者而言,能从这本书积累经验。看多了,知道怎么做比较好,再进一步去实验或者实践。感谢。
近11个小时读完了本书。从并发编程基础(线程创建、运行、通知和等待等基本操作,线程中断,死锁、上线文切换,内存可见性、cas操作、synchronized关键字等) 到Java 并发包中的原子操作类、锁、并发队列、线程池等高级主题的讲解,再到并发编程的实践(SimpleDateFormat 、深度复制等) 。每天读到不同的主题都会有新的收获、但还有些概念没有搞清楚比如偏移量、cas的原理等问题。 是一本不错的书、非常值得一读。希望过段时间回头重读一下。
整体来说,本书还是很推荐看看的,会写代码的同时还需要知道代码底层的运行逻辑。成体系的阅读能够帮忙我们更好理解碎片化的知识点,JDK以及优秀的开源框架源码都是学习的很好方式。 Talk is cheap, show me the code. 平常写代码过程中(ps 生产确实遇到了!)可能遇到的并发问题在最后一章都提及到了,通过解析源码找到问题原因并给出相关的解决措施。有的时候问AI或者Google找答案,回头才发现这些问题早在灯火阑珊处了。 Go, just go
初步看了几章,讲的通俗易懂,容易吸收,值得推荐!
很nice的一本书,跟着作者一块阅读了juc包中的源代码,对并发场景有了更深入的了解。因为一边看源代码一边读所以花费了许多的时间,不过收获颇丰。 感谢duag lea大佬,好多代码都是他撸的✧٩(ˊωˋ*)و✧; 感谢作者大大,凡是有代码的地方都写得很详细(◦˙▽˙◦)
这是一本相当不错的书,应该是大多程序员喜闻乐见的。代码实例更容易实践。 通读一遍,仍需针对性的细读部分章节。推荐大家读