首先说明文章来源:《Java并发编程之美》学习笔记 - 简书 (jianshu.com),博主写的很好,适合从宏观整理一下自己学的零碎知识点。
线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为要真正占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。
多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器和栈区域。
附图(来自javaGuide):可以直观了解->JavaGuide/memory-area.md at main · Snailclimb/JavaGuide · GitHub

Java 中有三种线程创建方式,分别为:
使用继承方式的好处是方便传参,可以在子类里添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递,而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量。不好的地方是 Java 不支持多继承,而如果继承了 Thread 类,那么子类不能再继承其他类,而 Runnable 则没有这个限制。前两种方式都没办法拿到任务执行的返回结果,但是 FutureTask 方式可以。
Java 中的 Object 类是所有类的父类,鉴于继承机制,Java 把所有类都需要的方法放到了 Object 类里面,其中就包含通知与等待系列的函数wait() / wait(long timeout)当一个线程调用一个共享变量的 wait() 方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
当线程调用共享对象的 wait() 方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。
notify()
一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。这个被唤醒的线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行
notifyAll()
notifyAll() 方法会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程
Thread 类中的 join 方法可以用来等待多个线程全部加载完毕再汇总处理。
线程 A 调用线程 B 的 join 方法后会被阻塞,当其他线程调用了线程 A 的 interrupt() 方法中断了线程 A 时,线程 A 会抛出 InterruptedException 异常而返回。
Thread 类有一个静态的 sleep() 方法,当一个执行中的线程调用了 Thread 的 sleep() 方法后,调用线程会暂时让出指定的执行权,也就是在这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 的资源后就可以运行了。
当一个线程调用了 Thread 类的静态方法 yield() 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
sleep() 和 yield() 方法的区别在于,当线程调用 sleep() 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用 yield() 方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
线程上下文切换时机有:当前线程的 CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。
什么是死锁
死锁是指两个或两个以上的线程在执行过程,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直等待而无法继续运行下去产生死锁的条件。
死锁的产生必须具备以下四个条件:
如何避免线程死锁
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是目前只有 请求并持有 和 环路等待 条件是可以被破坏的
资源的有序分配会避免死锁,因为资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。
Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。在 JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程,而垃圾回收线程则是守护线程
守护线程和用户线程区别之一是当最后一个非守护线程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。言外之意,只要有一个用户线程还没结束,正常情况下 JVM 就不会退出
创建守护线程的的方式是,设置线程的 daemon 参数为 true 即可
ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。
ThreadLocal 是一个 HashMap 结构,其中 key 就是当前 ThreadLocal 的实例引用,value 是通过 set 方法传递的值。ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。
多核 CPU 时代的到来打破了单核 CPU 对多线程效能的限制。多个 CPU 意味着每个线程可以使用自己的 CPU 运行,这减少了线程上下文切换的开销,但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都会高并发编程有着迫切的需求。
线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题
当一个线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。
假如线程 A 和线程 B 使用不同的 CPU 执行,此时由于 Cache 的存在,将会导致内存不可见问题。
synchronized 块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当做一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为 内部锁,也叫做 监视器锁 。
内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。
另外,由于 Java 中的线程是与操作系统中的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而 synchronized 的使用就会导致上下文切换。
进入 synchronized 块的内存语义是把在 synchronized 块内使用到的变量从线程的工作内存中清除,这样在 synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出 synchronized 块的内存语义是把在 synchronized 块内对共享变量的修改刷新到主内存。
除了可以解决共享变量内存可见性问题外,synchronized 经常被用来实现原子性操作。另外请注意,synchronized 关键字会引起线程上下文切换并带来线程调度开销。
对于解决内存可见性的问题,Java 还提供了一种弱形式的同步,也就是使用 volatile 关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。
线程安全性:即内存可见性和原子性。
CAS 即 Compare and Swap,是 JDK 提供的非阻塞原子性操作,它通过硬件保证了比较 -- 更新操作的原子性。JDK 里面的 Unsafe 类提供了一系列的 compareAndSwap 方法。
- // 比如说下面这个
- boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update);
其中 compareAndSwap 的意思是比较并交换。
CAS 有四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值。其操作含义是,如果对象 obj 中内存偏移量为 valueOffset 的变量值为 expect ,则使用新的值 update 替换旧的值 expect。这是处理器提供的一个原子性指令。
ABA 问题
CAS 操作有个经典的 ABA 问题。
ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从 A 到 B,然后再从 B 到 A 。如果变量的值只能朝着一个方向转换,比如 A 到 B,B 到 C,不构成环形,就不会存在问题。JDK 中的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。
Java 内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。
重排序在多线程下会导致非预期的程序执行结果,而使用 volatile 修饰变量就可以避免重排序和内存可见性问题。
写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。
为了解决主内存与 CPU 之间运行速度差的问题,会在 CPU 与主内存之间添加一级或多级高速缓冲器(Cache)。这个 Cache 一般是被集成到 CPU 内部的,所以也叫 CPU Cache 。
在 Cache 内部是按行存储的,其中每一行称为一个 Cache 行。Cache 行是 Cache 与主内存进行数据交换的单位。
由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个 Cache 行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每一个变量放到一个缓存行,性能会有所下降,这就是伪共享。
根据线程获取锁的抢占机制,锁可以分为 公平锁 和 非公平锁
公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。
非公平锁则在运行时闯入,也就是先来不一定先得。
ReentrantLock 提供了公平和非公平锁的实现
公平锁:ReentrantLock pairLock = new ReentrantLock(true)
非公平锁:ReentrantLock pairLock = new ReentrantLock(false) ,默认是非公平锁
在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销
根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为 独占锁 和 共享锁 。
独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock 就是以独占方式实现的。共享锁则可以同时由多个线程持有,例如 ReadWriteLock 读写锁,它允许一个资源可以被多个线程同时进行读操作。
可重入锁的原理是在锁内部维护了一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器,当计数器值为 0 时说明该锁没有被任何线程占用,当一个线程获取了该锁,计数器值会变为 1,这时其他线程再来获取锁时会发现锁的所有者不是自己而被阻塞挂起。但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,计数器值就 + 1,当释放锁后,计数器值 - 1。当计数器值为 0 时,锁里面的线程标示被重置为 null ,这时候被阻塞的线程会被唤醒来竞争获取该锁。
由于 Java 中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁失败后,会被切换到用户态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。
自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10,可以使用 -XX:PreBlockSpinsh 参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。
由此看来自旋锁是使用 CPU 时间换取线程阻塞与调度的开销,但是很有可能这些 CPU 时间白白浪费了。
以上几章建议所有学习之后再阅读:原文:《Java并发编程之美》学习笔记 - 简书 (jianshu.com)