Java并发

https://www.yuque.com/docs/share/31deed7b-935a-4cf7-bcf3-8107bee40404?#0ea5e531

1. 线程基础

1.1 实现线程的方法

thread,runnable , callable

1.2 启动和停止线程

start

  1. stop()方法,它会释放已经锁定的所有监视资源,如果当前任何一个受监视资源保护的对象处于一个不一致的状态(执行了一部分),其他线程线程将会获取到修改了的部分值,这个时候就可能导致程序执行结果的不确定性,并且这种问题很难被定位。
  2. suspend()方法,容易发生死锁。因为调用suspend()方法不会释放锁,这就会导致此线程挂起。鉴于以上两种方法的不安全性,Java语言已经不建议使用以上两种方法来终止线程了。
  3. 一般建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,既执行完run()方法,也就是说提供一种能够自动让run()方法结束的方式,在实际中,我们可以通过flag标志来控制循环是否执行,从而使线程离开run方法终止线程。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MyThread implements Runnable{
    private volatile Boolean flag;
    public void stop(){
        flag=false;
    }
    public void run(){
    while(flag);//do something
    }
    }
    上述通过stop()方法虽然可以终止线程,但同样也存在问题;当线程处于阻塞状态时(sleep()被调用或wait()方法被调用或当被I/O阻塞时),上面介绍的方法就不可用了。此时使用interrupt()方法来打破阻塞的情况,当interrupt()方法被调用时,会跑出interruptedException异常,可以通过在run()方法中捕获这个异常来让线程安全退出

1.3 线程 的六种状态

1
2
3
4
5
6
NEW  - 创建状态,线程创建之后,但是还未启动。
RUNNABLE - 运行状态,处于运行状态的线程,但有可能处于等待状态,例如等待CPU、IO等。
WAITING - 等待状态,一般是调用了wait()、join()、LockSupport.spark()等方法。
BLOCK - 阻塞状态,等待锁的释放,例如调用了synchronized增加了锁。
TERMINATED - 终止状态,一般是线程完成任务后退出或者异常终止。

1.4 wait,notify,sleep,join等重要方法

wait 阻塞
notify 释放
sleep 睡眠
join 线程没执行完之前,会一直阻塞在join方法处

1.5 守护线程

2. 线程安全

2.1 各种各样的锁

悲观锁和乐观锁
共享锁和独占锁
公平锁和非公平锁
可重入锁和非可重入锁
JVM对synchronized锁的优化

synchronized深入理解
JVM synchronize锁

ReentrantLock原理解读

2.2 并发容器

ConcurrentHashMap
CopyOnWriteArrayList
阻塞队列 - BlockingQueue接口
非阻塞队列 - ConcurrentLinkedQueue

2.3 ThreadLocal

ThreadLocal是一个关于创建线程局部变量的类。使用场景如下所示:

  1. 实现单个线程单例以及单个线程上下文信息存储,比如交易id等。
  2. 实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例。
  3. 承载一些线程相关的数据,避免在方法中来回传递参数。
    https://blog.csdn.net/weixin_44050144/article/details/113061884
    https://www.jianshu.com/p/377bb840802f
    • ThreadLocal 对数据的存储通过 Thread 的 ThreadLocalMap完成
    • ThreadLocal本身作为key ,Entry包装了key和value
    • Entry 继承自WeakReference ,没有Next,也没有链表
    • 开发地址法处理hash冲突
    • ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
    • 显示的调用remove来避免内存泄漏

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

2.4 atomic包,6种原子类

2.5 线程池

避免存在大量Thread,重用线程池内部的线程.从而避免了线程的创建和销毁带来的性能开销,同时能有效控制线程池的最大并发数,避免大量线程因互相抢占系统资源而导致阻塞现象发生

Executors , ThreadPoolExecutor ,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ThreadPoolExecutor(int corePoolSize, // 1
int maximumPoolSize, // 2
long keepAliveTime, // 3
TimeUnit unit, // 4
BlockingQueue<Runnable> workQueue, // 5
ThreadFactory threadFactory, // 6
RejectedExecutionHandler handler ) { //7
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
}
  1. corePoolSize: 核心线程池大小 , 线程池里一直不会被销毁的线程数量
  2. maximumPoolSize: 最大线程池容量
  3. workQueue: 线程等待队列
  4. keepAliveTime: 非核心线程空闲时的存活时间,该参数只有在 线程数量 > corePoolSize情况下才有用
  5. handler: 饱和策略,假如线程池已满,并且没有空闲的线程,这个时候不再允许提交任务
1
2
3
4
AbortPolicy:拒绝提交,直接抛出异常,也是默认的饱和策略;
CallerRunsPolicy:线程池还未关闭时,用调用者的线程执行任务;
DiscardPolicy:丢掉提交任务;
DiscardOldestPolicy:线程池还未关闭时,丢掉阻塞队列最久为处理的任务,并且执行当前任务。

2.5.1 FixedThreadPool

适用场景:可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
  1. corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池
  2. workQueue 为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出

2.5.2 CachedThreadPool

适用场景:快速处理大量耗时较短的任务

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
  1. corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制
  2. keepAliveTime = 60s,线程空闲60s后自动结束
  3. workQueue 为 SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue, 对其的操作必须是放和取交替完成的,典型的生产者-消费者模型,它不存储元素,每一次的插入必须要等另一个线程的移除操作完成。

2.5.3 SingleThreadExecutor

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

2.6 线程配合

CountDownLatch ,CyclicBarrier

2.7 Java内存模型

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

volatile 原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

java内存模型

happens-before:

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

2.8 cas 无锁编程

CAS即比较并替换,它是通过硬件来保证操作的原子性.去修改某个值,修改失败则重试.

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

无锁效率高:

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
  • 线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高 速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思

  1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  2. 但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响

2.9 AQS

AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

注意:AQS是自旋锁:在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功

并发编程(七)J.U.C之AQS原理、ReentrantLock原理

Java并发之AQS详解
02_可重入锁(递归锁)+LockSupport+AQS源码分析
Java 并发一步一脚印(专栏)

3. 生产者消费者

三种实现

4. 线程通信

  1. synchronized加wait/notify方式
  2. ReentrantLock加Condition方式
  3. 闭锁方式、栅栏模式(CountDownLatch,CyclicBarrier)
  4. Semaphore 信号量
  5. join
  6. Volatile 共享变量