文章目录

  • 一 进程与线程
    • 1 区别与联系
    • 2 Java内存区域
    • 3 线程组
    • 4 线程的上下文切换
    • 5 并发与并行
    • 6 线程的生命周期与状态
  • 二 线程间的通信和同步
    • 1 线程同步:锁
    • 2 线程同步:等待-通知机制
    • 3 线程同步:volatile 信号量
    • 4 线程通信:管道
  • 三 线程死锁
    • 1 死锁的四个必要条件
    • 2 死锁解决:预防死锁、避免死锁、检测与解除死锁
  • 四 并发编程的相关方法
    • 1 sleep() 与 wait()
    • 2 run() 与 start()
    • 3 join()
    • 4 线程创建的四种方法
    • 5 获取与设置优先级(不可靠)、守护线程
  • 五 synchronized 关键字
    • 1 简介
    • 2 使用方法
    • 3 注意事项
  • 六 volatile 关键字
    • 1 作用
    • 2 双重校验锁实现 单例模式(线程安全)
    • 3 与 synchronized 的关系
  • 七 ReentrantLock 可重入锁
    • 1 可重入锁
    • 2 和 synchronized 异同
  • 八 ThreadLocal
    • 1 概念
    • 2 get 和 set 方法的源码
    • 3 ThreadLocalMap - key 的弱引用和 GC
  • 九 并发容器
    • 1 ConcurrentHashMap
    • 2 CopyOnWriteArrayList
    • 3 ConcurrentLinkedQueue
    • 4 BlockingQueue
    • 5 ConcurrentSkipListMap
  • 十 线程池
    • 1 为什么使用线程池
    • 2 ThreadPoolExecutor 构造方法
    • 3 ThreadPoolExecutor 的状态
    • 4 任务处理流程

一 进程与线程

1 区别与联系

  • 进程是系统分配资源的基本单位,线程是 CPU 调度的基本单位
  • 进程和线程本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)
  • 线程是轻量级进程,进程在其执行的过程中可以产生多个线程。与进程不同的是属于同一进程的多个线程共享进程的堆和方法区资源 (JDK1.8 之后的元空间),但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多
  • 使用多线程而非多进程实现并发的优势:进程间的通信比较复杂,而线程间的通信比较简单;进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小

2 Java内存区域

  • 线程私有程序计数器的目的是,线程切换后能恢复到正确的执行位置
  • 线程私有虚拟机栈和本地方法栈的目的是,保证线程中的 局部变量(存放在栈帧中的局部变量表) 不被别的线程访问到
  • 堆和方法区是所有 线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

3 线程组

  • 线程组是一个树状的结构,每个线程组下面可以有多个线程或者 线程组
  • 每个 Thread 必然存在于一个 ThreadGroup 中,Thread 不能独立于 ThreadGroup 存在
  • 如果在 new Thread 时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组
  • ThreadGroup 是一个标准的向下引用的树状结构,这样设计的原因是防止"上级"线程被"下级"线程引用而无法有效地被GC回收
  • 线程组可以起到统一控制线程的优先级和检查线程的权限的作用

4 线程的上下文切换

  • 线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等
  • 线程切换时,需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场,并加载下一个将要占用 CPU 的线程上下文

举例说明:线程A切换到线程B
1.先挂起线程A,将其在CPU中的状态保存在内存
2.在内存中检索下一个线程B的上下文,并将其在 CPU 的寄存器中恢复,开始执行B线程
3.当B执行完,根据程序计数器中指向的位置恢复线程A

5 并发与并行

  • 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行,可以来回切换);
  • 并行: 单位时间内,多个任务同时执行

6 线程的生命周期与状态

  1. 线程创建之后它将处于 NEW(新建) 状态
  2. 调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片后就处于 RUNNING(运行) 状态。以上两种状态都属于 RUNNABLE
  3. 当线程执行 wait()join() 方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)join(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态
  4. 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态
  5. 线程在执行 Runnable 的 run() 方法之后将会进入到 TERMINATED(终止) 状态

反复调用同一个线程的start()方法是否可行?假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的 start() 方法是否可行?
两个问题的答案都是不可行,因为 threadStatus 的值会改变,调用 start() 的前提是 threadStatus==0 ,此时再次调用 start() 方法会抛 IllegalThreadStateException异常


二 线程间的通信和同步

1 线程同步:锁

  • 线程同步的根本目的是让线程按照一定的顺序执行
  • 示例
public class ObjectLock {private static Object lock = new Object();static class ThreadA implements Runnable {@Overridepublic void run() {synchronized (lock) {for (int i = 0; i < 100; i++) {System.out.println("Thread A " + i);}}}}static class ThreadB implements Runnable {@Overridepublic void run() {synchronized (lock) {for (int i = 0; i < 100; i++) {System.out.println("Thread B " + i);}}}}public static void main(String[] args) throws InterruptedException {new Thread(new ThreadA()).start();Thread.sleep(10);new Thread(new ThreadB()).start();  // 线程A先获得锁并执行,A执行结束后释放锁,B获得锁并执行}
}

2 线程同步:等待-通知机制

  • 基于 Object 类的 wait() 方法和 notify()随机叫醒一个正在等待的线程) ,notifyAll()(叫醒所有正在等待的线程) 方法实现
  • 一个锁同一时刻只能被一个线程持有。假如线程A现在持有了一个锁 lock 并开始执行,它可以使用 lock.wait() 让自己进入等待状态,lock 被释放
  • 线程B获得了 lock 这个锁并开始执行,它可以在某一时刻,使用 lock.notify(),通知之前持有 lock 锁并进入等待状态的线程A,指示线程A继续执行(需要注意的是,这个时候线程B并没有释放锁 lock,除非线程B这个时候使用 lock.wait() 释放锁,或者线程B执行结束自行释放锁,线程A才能得到 lock 锁)
public class WaitAndNotify {private static Object lock = new Object();static class ThreadA implements Runnable {@Overridepublic void run() {synchronized (lock) {for (int i = 0; i < 5; i++) {try {System.out.println("ThreadA: " + i);lock.notify();lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}lock.notify();}}}static class ThreadB implements Runnable {@Overridepublic void run() {synchronized (lock) {for (int i = 0; i < 5; i++) {try {System.out.println("ThreadB: " + i);lock.notify();lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}lock.notify();}}}public static void main(String[] args) throws InterruptedException {new Thread(new ThreadA()).start();Thread.sleep(1000);new Thread(new ThreadB()).start();}
}// 输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
...

3 线程同步:volatile 信号量

  • volatile 关键字能够保证内存的可见性,如果用 volatile 关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的;同时,它可以禁止指令重排
  • 多个线程(超过2个)需要相互合作,用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量
public class Signal {private static volatile int signal = 0;static class ThreadA implements Runnable {@Overridepublic void run() {while (signal < 5) {if (signal % 2 == 0) {System.out.println("threadA: " + signal);signal++;}}}}static class ThreadB implements Runnable {@Overridepublic void run() {while (signal < 5) {if (signal % 2 == 1) {System.out.println("threadB: " + signal);signal = signal + 1;}}}}public static void main(String[] args) throws InterruptedException {new Thread(new ThreadA()).start();Thread.sleep(1000);new Thread(new ThreadB()).start();}
}

4 线程通信:管道

  • JDK提供了 PipedWriterPipedReaderPipedOutputStreamPipedInputStream。其中,前面两个是基于字符的(处理单元为2字节),后面两个是基于字节流的(处理单元为1字节)

三 线程死锁

1 死锁的四个必要条件

  • 互斥条件:该资源任意一个时刻只由一个线程占用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

2 死锁解决:预防死锁、避免死锁、检测与解除死锁

  1. 预防死锁
  • 破坏请求与保持条件 :一次性申请所有的资源
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件
  1. 避免死锁——银行家算法
  2. 检测与解除死锁

操作系统:死锁


四 并发编程的相关方法

1 sleep() 与 wait()

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁
  • 都可以暂停线程的执行
  • wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒
  • wait() 可以指定时间,也可以不指定;而 sleep() 必须指定时间
  • wait() 释放CPU资源,同时释放锁;sleep() 释放CPU资源,但是不释放锁,所以易死锁

为什么 sleep 函数的精度很低?

  • sleep函数并不能起到定时的作用,主要作用是延时。在一些多线程中可能会看到sleep(0),其主要目的是让出时间片
  • 当系统越繁忙的时候它精度也就越低,因为它的精度取决于线程自身优先级、其他线程的优先级,以及线程的数量等因素,所以说sleep 函数是不能用来精确计时的

2 run() 与 start()

  • 调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,实现多线程工作
  • 直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以不是多线程工作

3 join()

  • 在 a 线程中调用 b 线程的 join() 方法,线程 a 进入阻塞状态,直到线程 b 完全执行完,线程 a 从阻塞状态中恢复
  • 如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就使用 join()

4 线程创建的四种方法

  • Runnable 接口不会返回结果或抛出检查异常,Callable 接口可以,如果任务不需要返回结果或抛出异常推荐使用 Runnable,这样代码看起来会更加简洁
  1. 通过继承 Thread 类,并重写 run() 方法,使用时直接创建该类的对象
public class Demo {public static class MyThread extends Thread {@Overridepublic void run() {System.out.println("MyThread");}}public static void main(String[] args) {Thread myThread = new MyThread();myThread.start();}
}
  1. (优先采用)实现 Runnable 接口并重写 run() 方法,使用时将该类的对象作为参数传递到 Thread 类的构造方法中。这种方法不受单继承的限制,更适合处理多线程共享数据的情况
public class Demo {public static class MyThread implements Runnable {@Overridepublic void run() {System.out.println("MyThread");}}public static void main(String[] args) {new Thread(new MyThread()).start();}
}
  1. 实现 Callable 接口,并重写 call() 方法。call() 可以有返回值;可以抛出异常,被外面的操作捕获,获取异常的信息;Callable 支持泛型。
//1.创建一个实现Callable的实现类
class NumThread implements Callable{//2.实现call方法,将此线程需要执行的操作声明在call()中@Overridepublic Object call() throws Exception {int sum = 0;for (int i = 1; i <= 100; i++) {if(i % 2 == 0){System.out.println(i);sum += i;}}return sum;}
}public class ThreadNew {public static void main(String[] args) {//3.创建Callable接口实现类的对象NumThread numThread = new NumThread();//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象FutureTask futureTask = new FutureTask(numThread);//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()new Thread(futureTask).start();try {//6.获取Callable中call方法的返回值//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。Object sum = futureTask.get();System.out.println("总和为:" + sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}
  1. 通过线程池:提高响应速度(减少创建新线程的时间);降低资源消耗(重复利用线程池中线程,不需要每次都创建);便于线程管理
public class ThreadPool {public static void main(String[] args) {//1. 提供指定线程数量的线程池ExecutorService service = Executors.newFixedThreadPool(10);ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象service.execute(new NumberThread());//适合适用于Runnable// service.submit(Callable callable);//适合使用于Callable//3.关闭连接池service.shutdown();}}

5 获取与设置优先级(不可靠)、守护线程

  • Java 只是给操作系统一个优先级的 参考值,线程最终在操作系统的调用顺序由操作系统的线程调度算法决定的
  • 优先级获取:thread_instance.getPriority()
  • 优先级设置:setPriority(int LEVEL),默认5,最高10,最低1,优先级越高,先执行的 概率 更大
  • 线程组也具有优先级,如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级被线程组的最大优先级取代
  • 守护线程默认的优先级比较低
  • 如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束,就免去了还要继续关闭子线程的麻烦

五 synchronized 关键字

1 简介

  • Java 多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁
  • 一句话概括:synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
  • 在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。因为操作系统实现线程之间的切换时需要从用户态转换到内核态(通过 trap 指令),这个状态之间的转换需要相对比较长的时间

2 使用方法

  1. 修饰实例方法(锁定实例):作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method() {// TODO
}
  1. 修饰静态方法(锁定类):给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁。如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized static void method() {// TODO
}
  1. 修饰代码块(指定锁定类型):指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁(适用继承 Runnable 接口实现)。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁(适用继承 Thread 实现)。

要求是多个线程共用一把锁

synchronized(this) {// TODO
}

3 注意事项

  • 不要使用 synchronized(String a),因为 JVM 中,字符串常量池具有缓存功能
  • 构造方法不能使用 synchronized 关键字修饰。因为构造方法本身就属于线程安全的,不存在同步的构造方法一说

六 volatile 关键字

1 作用

  1. 禁止 JVM 的指令重排。指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致
  2. 指示 JVM,变量是共享且不稳定的,每次使用它都到主存中进行读取(保证变量的可见性)
    因为在 Java 内存模型中,在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而 在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器,CPU cache)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致

    使用 volatile 关键字,指定变量每次使用时都从主存读取

2 双重校验锁实现 单例模式(线程安全)

public class Singleton {private volatile static Singleton uniqueInstance;  // 对象实例,需要用volatile修饰private Singleton() {  // 构造方法,设置为private}public  static Singleton getUniqueInstance() {  // 创建实例//先判断对象是否已经实例过,没有实例化过才进入加锁代码if (uniqueInstance == null) {//类对象加锁,保证只能创建一个实例synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();// 执行过程:// 1.为 uniqueInstance 分配内存空间// 2.初始化 uniqueInstance// 3.将 uniqueInstance 指向分配的内存地址}}}return uniqueInstance;}
}

必须使用 volatile 关键字的原因:
由于 JVM 具有指令重排的特性,uniqueInstance = new Singleton() 执行顺序有可能变成注释中的 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例(仅仅是刚分配了内存空间)
线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 使用 volatile 禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

3 与 synchronized 的关系

  • synchronized 关键字和 volatile 关键字是互补而非对立的关系
  • volatile 关键字是线程同步的 轻量级 实现,性能比 synchronized 关键字好
  • volatile 关键字只能用于变量(仅仅保证对单个volatile变量的读/写具有原子性),而 synchronized 关键字可以修饰方法以及代码块
  • volatile 关键字主要用于解决变量在多个线程之间的 可见性 ,而 synchronized 关键字解决的是多个线程之间访问资源的同步性

七 ReentrantLock 可重入锁

该部分的参考

1 可重入锁

  • 可重入锁,指的是一个线程能够对一个临界资源重复加锁
  • AQS 有一个变量 state 用于记录同步状态:初始情况下,state = 0,表示 ReentrantLock 目前处于解锁状态。如果有线程调用 lock 方法进行加锁,state 就由0变为1,如果该线程再次调用 lock 方法加锁,就执行 state++。线程每调用一次 unlock 方法释放锁,会让 state–。通过查询 state 的数值,即可知道 ReentrantLock 被重入的次数了
  • 现在有方法 m1 和 m2,两个方法均使用了同一把锁对方法进行同步控制,同时方法 m1 会调用 m2。线程 t 进入方法 m1 成功获得了锁,此时线程 t 要在没有释放锁的情况下,调用 m2 方法。由于 m1 和 m2 使用的是同一把可重入锁,所以线程 t 可以进入方法 m2,并再次获得锁,而不会被阻塞住;假如 lock 是不可重入锁,那么上面的示例代码必然会引起死锁情况的发生
void m1() {lock.lock();try {// 调用 m2,因为可重入,所以并不会被阻塞m2();} finally {lock.unlock()}
}void m2() {lock.lock();try {// do something} finally {lock.unlock()}
}

2 和 synchronized 异同

  • synchronized 使用的是对象或类进行加锁,而 ReentrantLock 内部是通过 AQS(AbstractQueuedSynchronizer)中的同步队列进行加锁
  • 公平与非公平指的是线程获取锁的方式:
    公平模式下,线程在同步队列中通过 FIFO 的方式获取锁,每个线程最终都能获取锁,缺点是效率低
    在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队,缺点是可能出现线程饥饿
class Window implements Runnable{private int ticket = 100;//1.实例化ReentrantLockprivate ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while(true){try{//2.调用锁定方法lock()lock.lock();if(ticket > 0){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);ticket--;}else{break;}}finally {//3.调用解锁方法:unlock()lock.unlock();}}}
}

八 ThreadLocal

参考链接

1 概念

  • ThreadLocal 类主要解决的就是让每个线程绑定自己的值,这个值不能被其它线程访问到
  • 每个 Thread 中都具备一个容器 ThreadLocalMap,而 ThreadLocalMap 可以存储以 ThreadLocal 为 key (其实是 ThreadLocal 的弱引用),Object 对象为 value 的键值对
  • ThrealLocal 类中可以通过 Thread.currentThread() 获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的 ThreadLocalMap 对象
  • ThreadLocalMap 不使用拉链法解决哈希冲突,而是向后探测:如果先遇到了空位置则直接插入;如果先遇到了 key 过期的数据则进行垃圾回收并替换

2 get 和 set 方法的源码

     public void set(T value) {Thread t = Thread.currentThread(); // 获取当前的线程ThreadLocalMap map = getMap(t);    // 每一个线程都维护各自的一个容器(ThreadLocalMap)if (map != null)map.set(this, value); // 这里的key对应的是ThreadLocalelsecreateMap(t, value); // 默认map是null,第一次往其中添加数据时,执行初始化}public T get() {Thread t = Thread.currentThread(); // 获取当前的线程ThreadLocalMap map = getMap(t);    // 当前线程的ThreadLocalMapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);   // this指的是ThreadLocal对象if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;     // entry.value就可以获取到工具箱了return result;}}return setInitialValue();}

3 ThreadLocalMap - key 的弱引用和 GC

  • ThreadLocalMap 中,key 是它对应的 ThreadLocal 的弱引用
  • 强引用不存在的话,那么 key 就会被回收,也就是会出现 value 没被回收,key 被回收的情况,导致 value 永远存在,出现内存泄漏

九 并发容器

1 ConcurrentHashMap

  • 数据结构:
类型 数据结构 使用的锁
ConcurrentHashMap JDK1.7 Segment 数组 + HashEntry 数组 + 链表/红黑树 Segment(本质是 ReentrantLock),每次锁若干 HashEntry
ConcurrentHashMap JDK1.8 Node 数组 + 链表/红黑树 synchronized,每次锁一个 Node
Hashtable 数组+链表 synchronized,每次锁全表
  1. 在 JDK1.7 的时候,ConcurrentHashMap 采用分段锁机制,对整个桶数组进行了分割分段(Segment,每个 Segment 都是一个可重入锁),每一个 Segment 只锁容器其中一部分数据,多线程访问容器里不同数据段的数据不会存在锁竞争,提高并发访问率
static class Segment<K,V> extends ReentrantLock implements Serializable {}

  1. JDK1.8 的时候已经摒弃了 Segment 的概念,synchronized 只锁定当前链表或红黑二叉树的首节点,并发控制使用 synchronized 和 CAS 来操作,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本

  2. Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低

2 CopyOnWriteArrayList

  • 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector
  • CopyOnWriteArrayList 读取完全不用加锁,写入也不会阻塞读取操作
  • 写时复制的思想:想要对一块内存进行修改时,不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了

3 ConcurrentLinkedQueue

  • 高效的并发队列,非阻塞队列(通过 CAS 操作实现)
  • 使用链表实现,可以看做一个线程安全的 LinkedList

4 BlockingQueue

  • 接口,阻塞队列(通过加锁实现),适合用于作为数据共享的通道
  • 被广泛使用在 “生产者-消费者” 问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止

5 ConcurrentSkipListMap

  • 使用跳表实现的 Map,使用跳表的数据结构进行快速查找
  • 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整,而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,只需要部分锁
  • 跳表内所有的元素都是排序的,对跳表进行遍历会得到有序的结果,适用于数据需要有序的环境

十 线程池

参考链接

1 为什么使用线程池

  • 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程
  • (主要原因)控制并发的数量:并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃
  • 统一管理线程

2 ThreadPoolExecutor 构造方法

  • Java中的线程池顶层接口是 Executor 接口,ThreadPoolExecutor 是这个接口的实现类
参数 含义 说明 是否必须
int corePoolSize 线程池中核心线程数最大值 核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)
int maximumPoolSize 线程池中线程总数最大值 核心线程数量 + 非核心线程数量
long keepAliveTime 非核心线程闲置超时时长 非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置 allowCoreThreadTimeOut(true),则会也作用于核心线程
TimeUnit unit keepAliveTime 的单位 枚举类型
BlockingQueue workQueue 阻塞队列,维护着等待执行的 Runnable 任务对象 下面补充说明
ThreadFactory threadFactory 线程创建工厂 用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂
RejectedExecutionHandler handler 拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略 下面补充说明
  • 常用的阻塞队列
  1. LinkedBlockingQueue:底层数据结构是链表,默认大小是 Integer.MAX_VALUE,也可以指定大小
  2. ArrayBlockingQueue:底层数据结构是数组,需要指定队列的大小
  3. SynchronousQueue:同步队列,内部容量为0,每个 put 操作必须等待一个 take 操作,反之亦然
  4. DelayQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素
  • 有关拒绝处理策略
  1. ThreadPoolExecutor.AbortPolicy默认策略,丢弃任务并抛出 RejectedExecutionException 异常
  2. ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(也就是最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

3 ThreadPoolExecutor 的状态

状态 说明
RUNNING 线程池创建后处于 RUNNING 状态
SHUTDOWN 调用 shutdown() 方法后处于 SHUTDOWN 状态,线程池不能接受新的任务,正在执行的线程不中断,完成阻塞队列的任务(存疑)
STOP 调用 shutdownNow() 方法后处于 STOP 状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,工作线程全部停止,阻塞队列为空
TIDYING 当所有的任务已终止,线程池会变为 TIDYING 状态,接着会执行 terminated() 函数
TERMINATED 线程池处在 TIDYING 状态时,并且执行完 terminated() 方法之后 , 线程池被设置为 TERMINATED 状态

4 任务处理流程

  1. 线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(在核心线程数量 < corePoolSize 时,让核心线程数量快速达到 corePoolSize)。注意,这一步需要获得全局锁
  2. 线程总数量 >= corePoolSize 时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(线程复用)。在加入队列前后,都进行线程池状态是否是 RUNNING 的检查
  3. 当缓存队列满了,说明这个时候任务非常多,创建非核心线程去执行这个任务。注意,这一步需要获得全局锁
  4. 缓存队列满了, 且总线程数达到了 maximumPoolSize,则会采取拒绝策略进行处理

为什么在步骤2中,要二次检查线程池的状态?

  • 在多线程的环境下,线程池的状态是时刻发生变化的。有可能刚获取线程池状态后线程池状态就改变了。判断是否将 command 加入workqueue 是线程池之前的状态。倘若没有二次检查,万一线程池处于非 RUNNING 状态(在多线程环境下很有可能发生),那么 command 永远不会执行
  • 类似于单例模式的双重校验

后端学习 - 并发编程相关推荐

  1. 【Java并发编程】一、为什么需要学习并发编程?

    原因: 1.硬件的驱动与互联网发展之间的鸿沟越来越大. 2.多核的服务器在不断的发展. 3.大型互联网厂商的系统并发量轻松过百万,传统的中间件和数据库已经不能为我们遮风挡雨了,反而成了瓶颈所在. 如何 ...

  2. python学习并发编程

    一.并发编程理论基础 并发编程得应用: 网络应用:爬虫(直接应用并发编程) 网络架构django.flask.tornado 源码-并发编程 socket server 源码-并发编程 计算机操作系统 ...

  3. 读 RocketMQ 源码,学习并发编程三大神器

    这个系列会针对NLP比赛,经典问题的解决方案进行梳理并给出代码复现~也算是找个理由把代码从TF搬运到torch.Chapter1是CCF BDC2019的赛题:金融信息负面及主体判定,属于实体关联的情 ...

  4. 学习并发编程的好网站

    http://ifeve.com/category/concurrency-translation/

  5. 简明高效的 Java 并发编程学习指南

    你好,我是宝令,<Java 并发编程实战>专栏作者,很高兴你能看到这篇内容. 对于一个Java程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一.因为并发编程是Java语言中最 ...

  6. 如何学习Java并发编程

    只要从事JAVA开发的小伙伴们,都会或多或少地接触到并发编程.对于初学者来说,这一部分内容比较晦涩难懂,并且嵌入了很多新技术.本篇博文旨在为小伙伴们提供学习Java并发编程的指导性建议.当然,这仅仅是 ...

  7. 并发编程(1)学习攻略如何才能学好并发编程?

    并发编程并不是一门相对独立的学科,而是一个综合学科.并发编程相关的概念和技术看上非常零散,相关度也很低,总给你一种这样的感觉:我已经学习很多相关技术了,可还是搞不定并发编程.那如何才能学习好并发编程呢 ...

  8. 【极客时间】《Java并发编程实战》学习笔记

    目录: 开篇词 | 你为什么需要学习并发编程? 内容来源:开篇词 | 你为什么需要学习并发编程?-极客时间 例如,Java 里 synchronized.wait()/notify() 相关的知识很琐 ...

  9. Java并发编程有多难?这几个核心技术你掌握了吗?

    本文主要内容索引 1.Java线程 2.线程模型 3.Java线程池 4.Future(各种Future) 5.Fork/Join框架 6.volatile 7.CAS(原子操作) 8.AQS(并发同 ...

最新文章

  1. Ubuntu中Atom编辑器显示中文乱码的处理方法
  2. 本硕非科班,单模型获得亚军!
  3. linux shell的配置文件信息
  4. 集成IDE anaconda
  5. 6.1 无监督学习-机器学习笔记-斯坦福吴恩达教授
  6. LinuxWorld 2007:Linux从狂热走向理性
  7. java.net.UnknownServiceException: CLEARTEXT communication to wanandroid.com not permitted by network
  8. Java----前端验证之验证码额实现
  9. 中后端管理系统前后分离、前端框架的实现拙见
  10. 生产上完成TopN统计流程
  11. siesta在Linux运行,siesta-3.0-b
  12. xhr返回值_XMLHttpRequest发送POST、GET请求以及接收返回值
  13. php 创建目录_使用 Zephir 轻松构建 PHP 扩展
  14. 网络编程BaseIO介绍
  15. c语言sort可以给字符排序吗,字符串排序 (C++代码)sort的第三个参数
  16. 基于SNN脉冲神经网络的Hebbian学习训练过程matlab仿真
  17. 在EXCEL中插入超级链接
  18. 2021年十大网络用语发布
  19. Windows远程桌面 无法进行复制粘贴的问题解决方法
  20. Cocos2dx游戏开发系列笔记5:继续润色《忍者飞镖射幽灵》

热门文章

  1. .Net Core 之 Ubuntu 14.04 部署过程
  2. 如何在 ASP.NET MVC 中集成 AngularJS(3)
  3. 微软借Bletchley项目将云计算信息加入区块链
  4. [开源 .NET 跨平台 数据采集 爬虫框架: DotnetSpider] [一] 初衷与架构设计
  5. MySQL-01:下载安装配置及初始化命令
  6. php实现文件留言,PHP文件操作及实例:留言板
  7. 第三方app_为什么第三方APP不能下载呢?
  8. 【ArcGIS遇上Python】ArcGIS Python获取某个字段的唯一值(获取指定字段的不同属性值)
  9. linux c之c语言符合标准的头文件和linux常用头文件
  10. 用C语言实现数组反序