并发
线程
状态转换
- 新建(New)
创建后尚未启动。 - 可运行(Runnable)
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。 - 阻塞(Blocked)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。 - 无限期等待(Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。 - 限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用Thread.sleep()
方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用Object.wait()
方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用Thread.sleep()
和Object.wait()
等方法进入。 - 死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
线程使用
- 实现Runnable接口
public class Thread01 implements Runnable{
@Override
public void run() {
System.out.println("Implements Runnable,Override run");
}
public static void main(String[] args) {
System.out.println("Main Start");
new Thread(()->{
System.out.println("Lambda Start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Lambda End");
}).start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main End");
}
}
Main Start
Lambda Start
Lambda End
Main End
- 实现Callable接口
public class Thread02 implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread01 t0 = new Thread01();
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Boolean> future = executor.submit(t0);
Object result = future.get();
System.out.println(result);
executor.shutdown();
}
}
- 继承Thread类
public class Thread03 extends Thread{
@Override
public void run() {
System.out.println("extends Thread,Override RUN");
}
public static void main(String[] args) {
Thread03 thread = new Thread03();
thread.start();
thread.setPriority(1);
System.out.println(thread.getState().toString());
}
}
基础线程机制
Executor
Executor
管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
CachedThreadPool
:一个任务创建一个线程;FixedThreadPool
:所有任务只能使用固定大小的线程;SingleThreadExecutor
:相当于大小为 1 的FixedThreadPool
。
submit()
提交一个线程,它会返回一个 Future<?>
对象,通过调用该对象的 cancel(true)
方法就可以中断线程。
调用 Executor
的 shutdown()
方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow()
方法,则相当于调用每个线程的 interrupt()
方法。
Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
sleep()
Thread.sleep(millisec)
方法会休眠当前正在执行的线程,millisec
单位为毫秒。
sleep()
可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
通过调用一个线程的 interrupt()
来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出InterruptedException
,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
yield()
对静态方法 Thread.yield()
的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
public void run() {
Thread.yield();
}
interrupted()
调用interrupt()
方法会设置线程的中断标记,此时调用 interrupted()
方法会返回true
。因此可以在循环体中使用interrupted()
方法来判断线程是否处于中断状态,从而提前结束线程。
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run() {
while (!interrupted()) {
// ..
}
System.out.println("Thread end");
}
}
}
互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM
实现的 synchronized
,而另一个是JDK
实现的 ReentrantLock
。
synchronized
同步代码块
只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
public void func() {
synchronized (this) {
// ...
}
}
同步方法
public synchronized void func () {
// ...
}
同步类
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
同步静态方法
作用于整个类。
public synchronized static void fun() {
// ...
}
ReentrantLock
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock();
// 确保释放锁,从而避免发生死锁。
}
}
}
除非需要使用ReentrantLock
的高级功能,否则优先使用 synchronized
。这是因为 synchronized
是 JVM 实现的一
种锁机制,JVM 原生地支持它,而ReentrantLock
不是所有的 JDK 版本都支持。
synchronized | ReentrantLock |
---|---|
JVM 实现 | JDK 实现 |
优化:自旋锁 | synchronized 与 ReentrantLock 大致相同 |
synchronized 不行 | ReentrantLock 可中断 |
非公平 | 默认情况下也是非公平的,但是也可以是公平的 |
ReentrantLock 可以同时绑定多个 Condition 对象 |
线程协作
join()
在线程中调用另一个线程的 join()
方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
wait() notify() notifyAll()
调用 wait()
使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify()
或者 notifyAll()
来唤醒挂起的线程。
它们都属于 Object 的一部分,而不属于 Thread。
只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException
。
使用 wait()
挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify()
或者 notifyAll()
来唤醒挂起的线程,造成死锁。
wait() 和 sleep() 的区别
wait()
是 Object 的方法,而sleep()
是 Thread 的静态方法;wait()
会释放锁,sleep()
不会。
await() signal() signalAll()
java.util.concurrent
类库中提供了 Condition
类来实现线程之间的协调,可以在Condition
上调用await()
方法使线程等待,其它线程调用 signal()
或 signalAll()
方法唤醒等待的线程。
相比于 wait()
这种等待方式,await()
可以指定等待的条件,因此更加灵活。
使用 Lock
来获取一个 Condition
对象。
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
try{
condition.await();
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
JUC
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
CountDownLatch
用来控制一个线程等待多个线程。
维护了一个计数器cnt
,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方
法而在等待的线程就会被唤醒。
CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行.
线程执行 await()
方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await()
方法而在等待的线程才能继续执行。
CyclicBarrier
和 CountdownLatch
的一个区别是,CyclicBarrier
的计数器通过调用 reset()
方法可以循环使用,所以它才叫做循环屏障。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
Semaphore
Semaphore
类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
FutureTask
在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask 可用于异步获取执行结果或取消执行任务的场景。
FutureTask<Integer> futureTask = new FutureTask<Integer>(
new Callable<Integer>() {
@Override public Integer call() throws Exception {
int result = 0;
for (int i = 0; i < 100; i++) {
Thread.sleep(10);
result += i;
}
return result;
}
});
Thread computeThread = new Thread(futureTask);
computeThread.start();
System.out.println(futureTask.get());
}
}
BlockingQueue
java.util.concurrent.BlockingQueue
接口有以下阻塞队列的实现:
- FIFO 队列 :
LinkedBlockingQueue
、ArrayBlockingQueue
(固定长度) - 优先级队列 :
PriorityBlockingQueue
提供了阻塞的take()
和put()
方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
ForkJoin
主要用于并行计算中,和 MapReduce
原理类似,都是把大的计算任务拆分成多个小任务并行计算。
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
main()
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
JMM
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题.
内存间交互操作
内存模型三大特性
-
原子性
AtomicInteger 能保证多个线程修改的原子性。
-
可见性
可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
volatile
synchronized
,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
final
,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 -
有序性
有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因
为发生了指令重排序。
volatile
关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
线程安全
不可变
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态
- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
互斥同步
悲观
synchronized 和 ReentrantLock。
非阻塞同步
- CAS
先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止).
比较并交换(Compare-and-Swap,CAS
)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。 - AtomicInteger
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,
- ThreadLocal
每个 Thread 都有一个ThreadLocal.ThreadLocalMap
对象。
锁优化
这里的锁优化主要是指 JVM 对 synchronized
的优化。
自旋锁
自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。
轻量级锁
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)
、偏向锁状态(biasble)
、轻量级锁状态(lightweight locked)
和重量级锁状态(inflated)
。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
偏向锁
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS操作也不再需要。