使用线程
三种方法
- 继承Thread类
需要重写run方法,然后调用start方法启动线程。 - 实现Runnable接口
需要重写run方法,然后调用start方法启动线程。 - 实现Callable接口
需要重写call方法,然后调用start方法启动线程。有返回值,通过 FutureTask 进⾏封装。
实现接口还是继承类?
实现接口更好一些,java不支持多继承,但可以实现多个接口。
基础线程机制
Executor
Executor用来管理多个异步操作(多个任务互不干扰,不需要同步操作)。
主要有三种Executor:
CachedThreadPool:⼀个任务创建⼀个线程;
FixedThreadPool:所有任务只能使⽤固定⼤⼩的线程;
SingleThreadExecutor:相当于⼤⼩为 1 的 FixedThreadPool。
Daemon
Daemon守护线程是⼀个服务线程,⽤于为其他线程提供服务。
所有非守护线程都执⾏完毕后,无论有没有守护线程,程序都会退出。
线程启动前可以通过setDaemon() 方法来设置该线程是否为守护线程。
sleep()
Thread.sleep() 使当前线程暂停执⾏指定的时间,暂停期间,其他线程可以继续运⾏,不会受到阻塞。
sleep()可能会抛出InterruptedException异常,异常不会传回main(),所以必须在Thread类中捕获并处理。
yield()
代表线程已经走完了重要的部分,可以让其他线程有机会执行。
中断
线程完成会自动关闭,但是如果线程异常也会提前关闭。
InterruptedException 异常
线程在 sleep() 或 wait() 时被中断,会抛出 InterruptedException 异常。
interrupted()
如果一个线程处于无限循环中,并且没有执行 sleep() 或 wait(),那么可以通过 interrupted() 来判断线程是否被中断。
如果线程被中断,interrupted() 会返回 true。
如果线程没有被中断,interrupted() 会返回 false。
Executor的中断操作
调用shutdown()方法会等待所有任务执行完毕后关闭Executor。
调用shutdownNow()方法会中断所有任务,相当于调用每个任务的interrupt()方法,然后关闭Executor。
如果只想中断某一个线程,可以通过submit() 方法提交一个Callable任务,然后调用Future的cancel()方法来中断任务。
互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第⼀个是 JVM 实现的
synchronized,另⼀个是 JDK 实现的 ReentrantLock。
synchronized
synchronized 是 Java 中的关键字,它可以修饰方法和代码块。
修饰方法时,锁的是当前对象。
修饰代码块时,锁的是括号中的对象。
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
比较synchronized和ReentrantLock
- 锁的实现
synchronized 是 JVM 实现的,⽽ ReentrantLock 是 JDK 实现的。 - 性能
新版本 Java 对 synchronized 进⾏了很多优化,例如⾃旋锁,synchronized 与ReentrantLock 的性能⼤致相同。 - 等待可中断
当持有锁的线程⻓期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,⽽ synchronized 不⾏。 - 公平锁
公平锁是指多个线程在等待同⼀个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是⾮公平的,ReentrantLock 默认情况下也是⾮公平的,但是也可以是公平的。 - 锁绑定多个条件
⼀个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
因为synchronized 是 JVM 实现的,可以保证⾃⼰的线程安全,⽽ ReentrantLock 需要程序员手动释放锁,
线程之间的协作
join()
在一个线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到被调用的线程执行完毕。
wait() notify() notifyAll()
wait() 使当前线程等待,直到其他线程调⽤ notify() 或 notifyAll() 方法。
notify() 随机唤醒⼀个等待线程,notifyAll() 唤醒所有等待线程。
wait()必须在 synchronized 块中调⽤,否则会抛出IllegalMonitorStateException异常。
wait()和sleep()的区别
- wait() 是 Object 的⽅法,sleep() 是 Thread 的⽅法。
- wait() 会释放锁,sleep() 不会释放锁。
- wait() 可以被 notify() 或 notifyAll() 唤醒,sleep() 只能被中断。
- wait() 必须在 synchronized 块中调⽤,sleep() 可以在任何位置调⽤。
await() signal() signalAll()
java.util.concurrent 提供的 Condition 类,可以再Condition 上调⽤ await() 使线程等待.
相比wait(),await() 可以指定时间,超过时间会⾃动唤醒。
线程状态
一个线程通常只有一种状态,并且这里特指jvm线程状态,而不是操作系统线程状态.
线程状态有6种:
新建(New)
创建后尚未启用.
可运行(Runnable)
正在Java虚拟机中执行,但是它可能正在等待操作系统分配处理器资源.
阻塞(Blocked)
线程被阻塞,等待其他线程完成操作.
等待(Waiting)
线程等待其他线程执行特定操作.
计时等待(Timed Waiting)
线程等待指定的时间.
终止(Terminated)
线程已经完成执行.
状态转换

J.U.C - AQS
java.util.concurrent (J.U.C)⼤⼤提⾼了并发性能,AQS 被认为是 J.U.C 的核⼼。
AQS是AbstractQueuedSynchronizer的缩写,是Java并发包中用来实现锁的基础框架.
CountDownLatch
CountDownLatch用来控制一个或多个线程等待其他线程完成操作.
CyclicBarrier
用来控制多个线程互相等待,直到到达某个公共屏障点(common barrier point)。
和CountDownLatch不同的是,CyclicBarrier的计数器可以被重置后使用,所以它被称为循环屏障。
Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
J.U.C - 其它组件
FutureTask
实现了Future接口和Runnable接口,可以作为Runnable被线程执行,也可以用来获取异步执行的结果.
适用于需要异步执行任务,并且需要获取结果的场景.
BlockingQueue
阻塞队列,可以用来实现生产者-消费者模式.
java.util.concurrent.BlockingQueue 接⼝有以下阻塞队列的实现:
FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定⻓度)
优先级队列 :PriorityBlockingQueue
ForkJoin
和MapReduce类似,可以将⼤量的数据拆分成⼩量的数据,然后分⽴计算,最后将结果合并。
Java 内存模型
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到⼀致的内存访问效果。
主内存和工作内存
主内存是所有线程共享的内存区域,工作内存是每个线程独有的内存区域。
所有的变量都存储在主内存中,每个线程还有⾃⼰的⼯作内存,⼯作内存存储在⾼速缓存或者寄存器中,保存了该线程使⽤的变量的主内存副本拷⻉。
线程只能直接读写⾃⼰的⼯作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。

内存间的交互操作

java内存模型规定了8种操作来完成主内存和工作内存之间的交互操作:
read:把⼀个变量的值从主内存传输到⼯作内存中
load:在 read 之后执⾏,把 read 得到的值放⼊⼯作内存的变量副本中
use:把⼯作内存中⼀个变量的值传递给执⾏引擎
assign:把⼀个从执⾏引擎接收到的值赋给⼯作内存的变量
store:把⼯作内存的⼀个变量的值传送到主内存中
write:在 store 之后执⾏,把 store 得到的值放⼊主内存的变量中
lock:作⽤于主内存的变量
unlock:作⽤于主内存的变量
内存模型的三大特性
原子性
java内存模型保证了read、load、use、assign、store、write这6个操作是具有原子性的。
但是不保证这6个操作的组合是具有原子性的。
AtomicInteger 是⼀个提供原子操作的 Integer 类。
除了使用原子类外,还可以通过 synchronized 关键字来保证操作的原子性。
可见性
可⻅性指当⼀个线程修改了共享变量的值,其它线程能够⽴即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可⻅性的。
主要有三种可见性的实现方式:
- volatile
- synchronized,对⼀个变量执⾏ unlock 操作之前,必须把变量值同步回主内存。
- final,被 final 关键字修饰的字段在构造⽅法中⼀旦初始化完成,并且没有发⽣ this 逃逸(其它线程通过 this 引⽤访问到初始化了⼀半的对象),那么其它线程就能看⻅ final 字段的值。
有序性
有序性指的是在本线程内观察,所有操作都是有序的;如果在⼀个线程观察另⼀个线程,所有操作都是无序的。
Java 内存模型是通过禁止指令重排序来保证有序性的。
主要有两种有序性的实现方式:
- volatile
- synchronized,对⼀个变量执⾏ unlock 操作之前,必须把变量值同步回主内存。
先⾏发⽣原则
先⾏发⽣原则是指如果在程序中两个操作的先后顺序与代码中的顺序相同,那么这两个操作就会先⾏发⽣。
1. 单⼀线程原则
在单⼀线程中,在程序前⾯的操作先⾏发⽣于后⾯的操作。
2. 管程锁定规则
⼀个 unlock 操作先⾏发⽣于后⾯对同⼀个锁的 lock 操作。
3. volatile 变量规则
对⼀个 volatile 变量的写操作先⾏发⽣于后⾯对这个变量的读操作。
4. 线程启动规则
Thread 对象的 start() ⽅法调⽤先⾏发⽣于此线程的每⼀个动作。
5. 线程加⼊规则
Thread 对象的结束先⾏发⽣于 join() ⽅法返回。
6. 线程中断规则
对线程interrupt()⽅法的调⽤先⾏发⽣于被中断线程的代码检测到中断事件的发⽣,可以通过 interrupted() ⽅法检测到是否有中断发⽣。
7. 对象终结规则
⼀个对象的初始化完成(构造⽅法执⾏结束)先⾏发⽣于它的 finalize() ⽅法的开始。
8. 传递性
如果操作 A 先⾏发⽣于操作 B,操作 B 先⾏发⽣于操作 C,那么操作 A 先⾏发⽣于操作 C。
线程安全
多个线程不管以何种⽅式访问某个类,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的⾏为,那么就称这个类是线程安全的。
线程安全有以下几种实现方法:
不可变
不可变(Immutable)的对象⼀定是线程安全的,不需要再采取任何的线程安全保障措施。只
要⼀个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不⼀致的状
态。多线程环境下,应当尽量使对象成为不可变,来满⾜线程安全。
不可变的类型:
final 关键字修饰的基本数据类型
String
枚举类型
Number 部分⼦类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等⼤
数据类型。但同为 Number 的原⼦类 AtomicInteger 和 AtomicLong 则是可变的。
对于集合类型,我们可以使⽤ Collections.unmodifiableXXX() 方法来获取⼀个不可变的集合。
互斥同步
synchronized 和 ReentrantLock。
⾮阻塞同步
阻塞同步是一种悲观的并发策略,即认为只要不去做正确的同步措施,那就肯定会出现问题。
而非阻塞是一种基于冲突检测的乐观并发策略,即不加锁,但是如果存在冲突,就重试当前操作直到成功。
CAS
硬件⽀持的原⼦性操作最典型的是:⽐较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执⾏操作时,只有当 V的值等于 A,才将 V 的值更新为 B。
乐观锁需要操作、冲突检测这两个步骤具备原⼦性,这⾥就不能再使⽤互斥同步来保证了,只能靠硬件来完成。
AtomicInteger
J.U.C 包⾥⾯的整数原⼦类 AtomicInteger 的⽅法调⽤了 Unsafe 类的 CAS 操作。
ABA
如果⼀个变量初次读取的时候是 A 值,它的值被改成了 B,后来⼜被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了⼀个带有标记的原⼦引⽤类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。
⼤部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改⽤传统的互斥同步可能会⽐原⼦类更⾼效。
无同步方案
要保证线程安全,并不是⼀定就要进⾏同步。如果⼀个⽅法本来就不涉及共享数据,那它⾃然就⽆须任何同步措施去保证正确性。
栈封闭
多个线程访问同⼀个⽅法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机
栈中,属于线程私有的。
线程本地存储(Thread Local Storage)
如果⼀个变量在线程的⼀个⽅法中被TLS变量存储,并被其他⽅法读取和修改,那么即使两个线程执⾏的是同⼀个代码,它们也会访问到不同的数据。
锁优化
主要是针对synchronized关键字的优化。
⾃旋锁
⾃旋锁虽然能避免进⼊阻塞状态从⽽减少开销,但是它需要进⾏忙循环操作占⽤ CPU 时间,
它只适⽤于共享数据的锁定状态很短的场景。
锁消除
锁消除是指虚拟机即时编译器在运⾏时,对代码进⾏扫描,去除不可能存在共享数据竞争的锁,
通过锁消除,可以节省毫无意义的请求锁时间。
锁粗化
锁粗化是指虚拟机即时编译器在运⾏时,对代码进⾏扫描,将多个相邻的加锁操作合并为⼀个加锁操作,
通过锁粗化,可以节省加锁和释放锁的时间。
轻量级锁
JDK 1.6 引⼊了偏向锁和轻量级锁,从⽽让锁拥有了四个状态:⽆锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
轻量级锁相比传统的重量级锁,它使用CAS操作来避免线程阻塞和唤醒的开销,同时也避免了操作系统层面的线程调度。
如果轻量级锁自旋或检测到有线程冲突,会升级为重量级锁。
偏向锁
偏向锁是指在没有线程竞争的情况下,锁对象会偏向于使⽤它的线程,这样就不需要进⾏额外的加锁和解锁操作。
多线程开发良好的实践
- 给线程起个有意义的名字,这样可以⽅便找 Bug。
- 缩⼩同步范围,从⽽减少锁争⽤。例如对于 synchronized,应该尽量使⽤同步块⽽不是同步⽅法。
- 多⽤同步⼯具少⽤ wait()和 notify() 。⾸先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,⽽⽤ wait()和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
- 使⽤ BlockingQueue 实现⽣产者消费者问题。
- 多⽤并发集合少⽤同步集合,例如应该使⽤ ConcurrentHashMap ⽽不是 Hashtable。
- 使⽤本地变量和不可变类来保证线程安全。
- 使⽤线程池⽽不是直接创建线程,这是因为创建线程代价很⾼,线程池可以有效地利⽤有限的线程来启动任务。