全栈工程师成神之路--Java并发

@wanqiuz 2018-06-09 07:51:24发表于 wanqiuz/blog-articles 全栈工程师成神之路

⭐️ 线程安全

线程安全是一个多线程环境下的概念,也就是保证多线程环境下,可共享的、可修改的状态的正确性。

如何保证线程安全

  • 将状态封装起来,甚至说让状态不可享
  • 让状态不可修改,共享不可变类
  • 使用锁机制同步

线程安全3个基本特性

  • 可见性: 某个线程修改了某个状态,该状态的最新值能够被其他线程知晓
  • 原子性:一系列操作不可中断,要么全部执行成功要么全部执行失败
  • 有序性:保证线程串行语义,也是happens-before规则

⭐️ 并发编程的挑战

  • 上下文切换导致操作系统用户态和内核态切换,代价高
  • 死锁
  • 资源的限制(软件、硬件)

⭐️ 上下文切换

减少上下文切换的方法

  • 无锁并发编程(如按照Hash算法取模分段,不同线程处理不同段的数据)
  • CAS算法
  • 使用最少线程
  • 协程

⭐️ 死锁

定义

两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。

死锁的必要条件(缺一不可)

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不可抢占条件:进程已获得的资源,在末使用完之前,不能强行剥夺,只能在进程使用完时由自己释放。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

破坏死锁

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 用定时锁
  • 将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

⭐️ Java如何实现原子操作

  • 锁(偏向锁、轻量级锁、重量级锁)
  • 循环CAS

⭐️ CAS实现原子操作的三大问题

  • ABA问题(使用版本号解决)
  • 循环时间长开销大
  • 只能保证一个共享变量的原子操作(JDK提供AtomicReference类保证引用对象之间的原子性,可以把多个变量放在一个对象里进行CAS操作)

⭐️ 并发编程模型的两个关键问题:线程间如何通信、如何同步

  • 共享内存:隐式通信、显式同步(Java)
  • 消息传递:显式同步,隐式通信

⭐️ Java内存模型(JMM)

  • 主内存与工作内存
    所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。
  • 内存间交互操作
    关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了八种操作来完成。
  • 重排序
    从Java源代码到最终实际执行的指令序列,会依次经过下面三种重排序:
    编译器优化的重排序、指令级并行的重排序、内存系统的重排序。
    为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种。
    happens-before原则。
  • 同步机制
    volatile、锁和final
  • 原子性、可见性与有序性

⭐️ final域的重排序规则

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

⭐️ happens-before规则

1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
2)监视器原则:对一个锁的解锁,happens-before于随后对这个锁的加锁
3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
5)start()规则:如果线程A执行操作ThreadB.start(),那么线程A的ThreadB.start() happens-before于线程B中的任意操作
6)join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于A线程的ThreadB.join()操作并成功返回

⭐️ 双重检查锁非线程安全

memory = allocate(); // 1: 分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
2、3可能发生重排序,有两种方法实现线程安全的延迟初始化
1)不允许2和3重排序:使用volatile关键字(优势是除了能实现静态字段的延时初始化,还能实现实例字段的延时初始化)
2)允许2和3重排序,但不允许其他线程看到这个重排序:基于类初始化的解决方案

public class InstanceFactory {
  private static class InstanceHolder {
    public static Instance instance = new Instance();
  }
  public static Instance getInstance() {
    return InstanceHolder.instance();
  }
}

⭐️ 进程和线程区别

进程是操作系统资源分配的最小单元
线程是操作系统调度的最小单元

⭐️ Daemon线程

通过调用threadA.setDaemon(true)将线程设置为Daemon线程(守护线程),该设置必须在线程启动之前设置。Daemon线程在非Daemon线程退出后自动退出,不保证finally块一定执行。

⭐️ suspend()、resume()、stop()废弃原因

suspend()方法调用后,不会释放已经占有的资源。
stop()方法不保证资源的正常释放。

⭐️ Synchronized和CAS比较

简介

  • Synchronized是一种语言的特性,可以用来修饰方法或者代码块
  • CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
    CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

思想上

Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

Synchronized缺点

Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管Java6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度(在到重量级锁之前,也是用的CAS机制),但是在最终转变为重量级锁之后,性能仍然较低。

CAS的缺点

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。低并发下CAS性能更好,高并发Synchronized性能好
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
3.ABA问题
这是CAS机制最大的问题所在。可以使用版本控制

使用场景

java.util.concurrent.atomic包下以Atomic为前缀的原子类,采用的是自旋锁的思想,底层用volatile关键字和CAS机制实现的。
Java6为Synchronized在到重量级锁之前,也是用的CAS机制。

⭐️ Synchronized和ReentrantLock比较

相同点

  • 都是悲观锁
  • 都是可重入的

区别

  • Synchronized是java语言的关键字。在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。
  • ReentrantLock是通过类提供的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
  • 低并发下两者性能差不多,高并发下ReentrantLock明显更优,公平的ReentrantLock在高低并发下和Synchronized相差不大。

什么时候才应该使用 ReentrantLock

在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票,或者在高并发下。

⭐️ 中断

定义

在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断。

中断的相关方法

public void interrupt() 
将调用者线程的中断状态设为truepublic boolean isInterrupted() 
判断调用者线程的中断状态。

public static boolean interrupted 
只能通过Thread.interrupted()调用。 
它会做两步操作:

返回当前线程的中断状态;
将当前线程的中断状态设为false

除了java提供的方法,当然也可以自行设置一个变量用于中断。

暂停、继续、停止线程(@deprecated

以下三个方法都是通过线程对象去调用。
suspend()
暂停调用者线程,只释放CPU执行权,不释放锁。
由于在不释放资源的情况下进入睡眠状态,容易产生死锁。因此已过时!
resume()
恢复调用者线程,让他处于就绪状态。
stop()
调用stop后,并不会保证资源被正确地释放(但会释放锁),它会使程序处于不正确的状态下。

处理中断

  • Java类库中提供的一些可能会发生阻塞的方法都会抛InterruptedException异常,如:BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep。
    当你在某一条线程中调用这些方法时,这个方法可能会被阻塞很长时间,你可以在别的线程中调用当前线程对象的interrupt方法触发这些函数抛出InterruptedException异常。
  • 当一个函数抛出InterruptedException异常时,表示这个方法阻塞的时间太久了,别人不想等它执行结束了。
  • 当你的捕获到一个InterruptedException异常后,亦可以处理它,或者向上抛出。
    抛出时要注意???:当你捕获到InterruptedException异常后,当前线程的中断状态已经被修改为false(表示线程未被中断);此时你若能够处理中断,则不用理会该值;但如果你继续向上抛InterruptedException异常,你需要再次调用interrupt方法,将当前线程的中断状态设为true。
  • 注意:绝对不能“吞掉中断”!即捕获了InterruptedException而不作任何处理。这样违背了中断机制的规则,别人想让你线程中断,然而你自己不处理,也不将中断请求告诉调用者,调用者一直以为没有中断请求。

⭐️ 线程间通信方式

  • volatile/synchronized关键字等
  • 等待/通知机制,wait/notify/notifyAll方法
  • 管道流PipedOutputStream、PipedInputStream、PipedWriter、PipedReader。分别对应字节流和字符流
  • 线程方法join(可被中断)

⭐️ 等待/通知机制

生产者/消费者模式,这种模式隔离了“做什么”和“怎么做”,在功能层面实现了解耦,体系结构上具备了良好的伸缩性。在Java中有两种方法实现。
1)循环检查变量并sleep一定时间,难以实现及时性,难以降低开销
2)任意对象的wait/notify方法,但必须获取锁

⭐️ ThreadLocal的使用

ThreadLocal提供了线程内的局部变量,并非用于解决并发问题
提供get、set、remove方法(某些情况下,需手动释放,防止内存泄露)
protected initialValue建议用匿名内部类重写
9671b789e1da4f760483456c03e4f4b6_hd
1)图中弱引用,存在内存泄露风险(在ThreadLocalRef释放时),get或set会将遇到的key为null的Entry会删除,当然,若是没遇到就不能删除了
2)最好手动删除,或将ThreadLocalRef设置为private static,使其尽可能存活的久

⭐️ 进程间通信方式?

1)管道(Pipe),2)命名管道(named pipe),3)信号(Signal),4)消息(Message)队列,5)共享内存,6)内存映射(mapped memory),7)信号量(semaphore),8)套接口(Socket)

⭐️ 线程池

⭐️ Java中的伪共享(false sharing)以及应对方案

⭐️ 类加载顺序

  1. 父类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
    2. 子类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
    3. 父类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
    4. 父类构造方法
    5. 子类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
    6. 子类构造方法

⭐️ Java中的并发工具类

CountDownLatch、CyclicBarrier、Semaphore提供了一种并发流程控制的手段,Exchanger工具类则提供了在线程间交换数据的手段。

⭐️ 等待多线程完成的CountDownLatch

  • CountDownLatch允许一个或多个线程等待其他线程完成操作。
  • 1 CountDownLatch c = new CountDownLatch(n);
    2 c.countDown();
    3 c.await(); // 可被中断
  • 一个线程调用countDown方法happens-before另一个线程调用await方法

⭐️ 同步屏障CyclicBarrier

  • CyclicBarrier,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续进行
  • 1 CyclicBarrier c = new CyclicBarrier(n);
    2 await方法说明达到了屏障,可被中断
    3 CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction

⭐️ CountDownLatch和CyclicBarrier区别

  • CountDownLatch调用countDown后线程不停顿
  • CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置
  • CyclicBarrier还提供了其他的方法,getNumberWaiting方法可以获得阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断

⭐️ 控制并发线程数的Semaphore

  • Semaphore(信号量)是用来控制同时访问特定资源的线程数量。
  • 1 Semaphore s = new Semaphore(n);
    2 acquire()方法获取许可证,可被中断,release()释放许可证,tryRelease()方法尝试获取许可证。

⭐️ 线程间交换数据的Exchanger

  • Exchanger用于进行线程间的数据交换,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
  • 1 Exchanger e = new Exchanger();
    2 T t1 = new t();
    T t2 = exchange(t1); // 线程1,阻塞线程,等待交换,可被中断
    T t2 = new t();
    T t1 = exchange(t2); // 线程2,阻塞线程,等待交换,可被中断

⭐️ 线程池优势

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

⭐️ 线程池的实现原理

ThreadPoolExcutor:
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
3)若果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法
ThreadPoolExcutor之所以这么设计,是因为创建新线程需要获取全局锁,会成为严重的可伸缩瓶颈

⭐️ 线程池执行任务分为两个情况

1)在excute()方法中创建一个线程时,会让这个线程执行当前任务
2)这个线程执行完任务后,会反复从BlockingQueue获取任务来执行

⭐️ 线程池的创建

new ThreadPoolExecutor(corePoolSize, maximum, keepAliveTime, milliseconds, runnableTaskQueue, handler);
runnableTaskQueue有以下阻塞队列可以选择:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue(有优先级的无线阻塞队列)
RejectedExecutionHandler(饱和策略)4种:1.直接抛出异常(默认)、2.调用者所在线程运行任务、3.丢弃队列里最近的一个任务,执行当前任务、4.不处理,丢弃掉

⭐️ 向线程池提交任务

1)execute()方法用于提交不需要返回值的任务
2)submit()方法用于提交需要返回值的任务,会返回一个future类型的对象,可以通过future的get方法来获取返回值,get()方法会阻塞当前线程直到任务完成

⭐️ 关闭线程池方法shutDown和shutDownNow区别

区别:
shutDown:设置线程池状态为SHOTDOWN,正在执行的继续执行,没有被执行的则中断
shutDownNow:设置线程池状态为STOP,正在执行的被中断,没有被执行的则返回
相同点:
1)停止接受新任务
2)执行完方法,isShutDown()返回true
3)当所有任务都关闭后,isTerminated()返回为true

⭐️ 合理地配置线程池

1)线程CPU时间占比越高,如CPU密集型任务,为减少上下文切换,需要越少线程,建议配置Ncpu + 1个线程的线程池
2)线程等待时间占比越高,如IO密集型任务,为不让CPU闲下来,需要越多线程,建议配置2 * Ncpu个线程的线程池
3)混合型任务,可以拆分成CPU密集型和IO密集型任务

⭐️ 高并发下如何配置线程池

1)高并发,执行任务短,建议配置Ncpu + 1个线程的线程池
2)低并发,执行任务长,若任务集中在IO操作,建议配置2 * Ncpu个线程的线程池,若任务几种在CPU操作,建议配置Ncpu + 1个线程的线程池
3)高并发,执行任务长,,关键不在线程池,关键在于设计架构,缓存,增加服务器,任务拆分等

⭐️ 线程池的监控

继承线程池,重写beforeExecute,afterExecute,terminated方法