实战Java高并发程序设计 笔记

第1章 走入并行世界

概念

同步和异步

  • 同步:同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为;
  • 异步:一旦方法,方法调用就会立即返回,调用者就可以继续后续的操作。如果异步调用需要返回结果,那么当这个异步调用真是完成时,则会通知调用者。

临界区

表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它。一旦临界区资源被占用,其他线程想要使用这个资源,就必须等待。

阻塞和非阻塞

  • 阻塞:一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起。
  • 非阻塞:没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断前向执行。

JMM

原子性

原子性是指一个操作是不可中断的。多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

可见性

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

有序性

程序实行时,可能会进行指令重排。

Happen-Before规则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写先发生于读
  • 锁规则:unlock必然发生在随后的lock前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每一个动作
  • 线程的所有操作先于线程的终结
  • 线程的interrupt()先于被中断线程的代码
  • 对象的构造函数执行、结束先于finalize()方法

第2章 Java并行程序基础

初始线程:线程的基本操作

新建线程

1
2
3
4
5
6
7
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("Hello");
}
};
t1.start();

注意:调用start()方法,不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。

1
2
Thread t1 = new Thread();
t1.start();

考虑Java是单继承的,使用Runnable接口来实现同样的操作。

1
2
3
4
5
6
7
8
9
10
public class MyThread implements Runnable{
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread());
t1.start();
}
@Override
public void run() {
System.out.println("Runnbale");
}
}

终止线程

stop()方法不推荐,可能引起数据不一致的问题。Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。

可以使用一个标记变量用于指示线程是否需要退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
volatile boolean flag = false;
public void stop() {
flag = true;
}
@Override
public void run() {
while (true) {
if (flag) {
break;
}
……
}
}

线程中断

线程中断并不会使线程立即推出,而是给线程发送一个通知,告诉目标线程希望退出。目标线程接到通知后如何入理,完全由目标线程自行决定。

Thread.interrupt()方法是一个实例方法,通知目标线程中断,也就是设置中断标志位。

Thread.isInterrupted()方法也是实例方法,判断当前线程是否由被中断(通过检查中断标志位)

Thread.interrupted也是用来判断当前线程的中断状态,同时会清除当前线程的中断标志位状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread() {
@Override
public void run() {
while(true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted");
break;
}
Thread.yield();
}
}
};
t1.start();
t1.sleep(2000);
t1.interrupt();
}

wait和notify

当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。

如果一个线程调用了object.wait(),那么它就会进入object对象的等待队列。当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程并将其唤醒,这个选择是不公平的,并不是先等待的线程会被优先选择,这个选择完全是随机的。Object对象还有一个类似的notifyAll()方法,它会唤醒在这个等待队列中所有的等待的线程,而不是随机选择一个。

无论是wait()或者notify()都需要首先获得目标对象的一个监视器。

join和yield

线程需要等待依赖线程执行完毕,join()方法会一直阻塞当前线程,直到目标线程执行完毕。

yield()方法会使当前线程让出CPU,还会进行CPU资源的争夺。

守护线程

如果用户线程全部结束,守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。

线程安全与synchronized

关键字synchronized的作用是实现线程间的同步,它的工作是对同步的代码加锁,是的每一次只能有一个线程进入同步块,从而保证线程间的安全性

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁
  • 直接作用与静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁

除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。

第3章 JDK并发包

多线程的同步协作:同步控制

synchronized的功能拓展:重入锁

重入锁可以完全替代synchronized关键字,使用java.util.concurrent.locks.ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException{
ReenterLock rl = new ReenterLock();
Thread t1 = new Thread(rl);
Thread t2 = new Thread(rl);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}

使用重入锁保护临界区资源i,确保多线程对i操作的安全性。开发人员必须手动指定何时加锁,何时释放锁,对逻辑控制的灵活性要远远好于synchronized。

“重入”:如果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。

除了使用上的灵活性外,重入锁还提供了一些高级功能。

  • 中断响应:对于synchronized来说如果一个线程在等待锁,那么要么它获得这把锁继续执行,要么它就保持等待。使用重入锁,线程可以被中断,也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。

    1
    lock.lockInterruptibly();
  • 锁申请等待超时

    1
    lock.tryLock(5, TimeUnit.SECONDS)

    tryLock()方法接受两个参数,一个表示等待时长,另外一个表示记时单位,表示线程在这个锁请求中最多等待时间,如果超过时间还没有得到锁就会返回false,如果成功获得锁则返回true。

    tryLock()方法也可以不带参数直接运行,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true,如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。这种模式不会引起线程等待,因此也不会产生死锁。

  • 公平锁

    公平锁按照时间的先后顺序,保证先到者先得,后到者后得。公平锁不会产生饥饿现象。

    构造函数,参数fair为true时表示锁的公平的。

    1
    public ReentrantLock(boolean fair)

    实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下。因此默认情况下,锁是非公平的。根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。

ReentrantLock的几个重要方法

  • lock():获取锁,如果所已经被占用,则等待
  • lockInterruptibly():获得锁,但优先相应中断
  • tryLock():尝试获得锁,如果成功返回true,失败返回false;该方法不等待,立即返回
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁
  • unlock():释放锁

Condition条件

wait()和notify()方法是和synchronized关键字合作使用的,而Condition是与重入锁相关联的。通过Lock接口的new Condition()方法可以生成一个与当前重入锁绑定的Condition实例。

  • await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行
  • signal()方法用于唤醒一个在等待中的线程
  • signalAll()方法会唤醒所有在等待中的线程

信号量

信号量可以指定多个线程同时访问某一个资源。

构造函数

1
2
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
  • acquire()方法尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断
  • tryAcquire()尝试获得一个许可,如果成功返回true,失败返回false,不会进行等待,立即返回
  • release()用户在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问

ReadWriteLock读写锁

读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写锁允许多个线程同时读。

  • 读-读不互斥
  • 读-写互斥
  • 写-写互斥

如果在系统中,读操作次数远远大于写操作,则读写锁可以发挥最大的功效

1
2
3
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();

倒计时器 CountDownLatch

CountDownLatch构造函数接受一个整数作为参数,即当前这个计时器的计数个数

1
public CountDownLatch(int count)
1
2
3
CountDownLatch latch = new CountDownLatch(10);
latch.countDown();
latch.await();
  • countDown()方法通知CountDownLatch一个线程已经完成了任务,倒计时器可以减1
  • await()方法要求主线程等待所有任务全部完成后,主线程才能继续执行

循环栅栏 CyclicBarrier

计数器可以反复使用。构造函数中parties表示计数总数,也就是参与的线程总数。barrierAction就是当计数器一次计数完成后,系统会执行的动作

1
public CyclicBarrier(int parties, Runnable barrierAction)

线程复用:线程池

线程的创建与关闭需要花费时间,如果为每一个小的任务都创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的情况,反而会得不偿失。

其次,线程本身也是要占用内存空间的,大量的线程会抢占宝贵的内存资源。

线程池

让创建的线程进行复用。使用线程池后,创建线程变成了从线程池获取空闲线程,关闭线程变成了向线程池归还线程。

JDK对线程池的支持

JDK提供了一套Executor框架,其本质就是一个线程池。ThreadPoolExecutor表示一个线程池。Executors类则扮演线程池工厂的角色,通过Executors可以取得一个拥有特定功能的线程池。

  • newFixedThreadPool()方法:返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,带有线程空闲时,便处理在任务队列中的任务。
  • newSingleThreadExecutor()方法:返回一个只有一个线程的线程池。若多于一个任务被提交到线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务
  • newCachedThreadPool()方法:返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • newScheduledThreadPool()方法:返回一个ScheduledExecutorService对象,可以指定线程数量。拓展了在给定时间执行某任务的功能。

核心线程池的内部实现

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
  • corePoolSize:指定了线程池中的线程数量
  • maximumPoolSize:指定了线程池中最大线程数量
  • keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间。即超过corePoolSize的空闲线程在多长时间内会被销毁
  • unit:keepAliveTime的单位
  • workQueue:任务队列,被提交但尚未被执行的任务
  • threadFactory:线程工厂,用于创建线程
  • handler:拒绝策略。当任务太多来不及处理如何拒绝任务

第4章 锁的优化及注意事项

有助于提高锁性能的几点建议

  • 减小锁持有时间
  • 减小锁粒度
  • 读写分离锁来替换独占锁
  • 锁分离
  • 锁粗化

Java虚拟机对锁优化所做的努力

锁偏向

锁偏向是一种针对加锁操作的优化手段。它的核心思想是如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须在做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。因此对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次多次极有可能是同一个线程请求相同的锁。

轻量级锁

如果偏向锁失败,虚拟机并不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。轻量级锁简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁就会膨胀为重量级锁。

自旋锁

锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机会使用自旋锁。系统假设在不久的将来线程可以得到这把锁,因此虚拟机会让当前线程做几个空循环,在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真实地将线程在操作系统层面挂起。

锁消除

Java虚拟机在JIT编译时,通过对运行上下文地扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。

ThreadLocal

ThreadLocal是一个线程的局部变量,也就是说只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。

  • set()方法:首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中。
  • get()方法:首先取得当前线程的ThreadLocalMap对象,然后通过将自己作为key取得内部的实际数据。

无锁

对于并发控制而言,锁是一种悲观的策略。它总是假设每一次临界区操作会发生冲突。如果有多个线程需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。

无所是一种乐观的策略,它会假设对资源的访问是没有冲突的。如果遇到冲突,无锁的策略使用CAS比较交换的技术来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

CAS 比较交换

包含三个参数(V, E, N):V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N;如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后CAS返回当前V的真实值。

CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作同一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行适当的处理。

第5章 并行模式与算法

单例模式

单例模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。

1
2
3
4
5
6
7
public class Singleton {
private Singleton() {}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
  • 把构造函数设置为private,保证系统中不会有人意外构建多余的实例
  • instance对象必须是private并且static的
  • 不足:实例在什么时候创建是不受控制的。对于静态成员instance,它会在类第一次初始化的时候被创建

一种支持延迟加载的策略

1
2
3
4
5
6
7
8
9
public class LazySingleton {
private LazySingleton() {}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance() {
if (instance == null)
instance = new LazySingleton();
return instance;
}
}

补充:双重检查模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DCLSingleton {
private volatile static DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null)
instance = new DCLSingleton();
}
}
return instance;
}
}

静态内部类

1
2
3
4
5
6
7
8
9
public class StaticSingleton {
private StaticSingleton() {}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}
  • getInstance()方法中没有锁,使得高并发环境下性能优越
  • 只有在getInstance()方法被第一次调用时,StaticSingleton的实例才会被创建

不变模式

一个对象一旦被创建,则它的内部状态将永远不会发生变化。所以没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。

生产者-消费者模式

生产者线程负责提交用户请求,消费者线程负责具体处理生产者提交的任务。生产者和消费者之间通过共享内存缓冲区进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Producer implements Runnable {
BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
queue.put(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue){
this.queue = queue;
}
@Override
public void run() {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}