一文读懂JAVA并发编程(基础)

2.1进程与线程

进程:指令和数据组成,运行时指令加载到cpu,数据加载到内存。可以看成程序的示例。

线程:一个进程由多个线程组成,最小调度单位。

对比:进程基本是相互独立,进程拥有共享资源如内存。线程上下文切换成本低。

添加图片注释,不超过 140 字(可选)

任务调度器将cpu时间片分给不同程序使用。

并发 同一时刻线程轮流使用cpu。

并行 多个线程同时运行。

2.3应用

需要等待结果返回,才能继续运行就是同步

不需要等待结果返回,就能继续运行就是异步

多线程需要在多核 cpu 才能提高效率,单核仍然时是轮流执行,单核时由于线程切换反而导致时间增加。

  1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活

  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分。

  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。

3.Java线程

创建和运行线程

  1. 直接使用Thread

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

  1. 使用Runnable配合Thread Thread 代表线程 Runnable 可运行的任务(线程要执行的代码)

添加图片注释,不超过 140 字(可选)

用lambda简化

添加图片注释,不超过 140 字(可选)

Thead 检测到Runnable有值会使用runnable方法 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了 用 Runnable 更容易与线程池等高级 API 配合 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活 三,FutureTask 配合 Thread

添加图片注释,不超过 140 字(可选)

3.3 查看进程线程的方法 windows 任务管理器可以查看进程和线程数,也可以用来杀死进程 tasklist 查看进程 taskkill 杀死进程 linux ps -fe 查看所有进程 ps -fT -p 查看某个进程(PID)的所有线程 kill 杀死进程 top 按大写 H 切换是否显示线程 top -H -p 查看某个进程(PID)的所有线程 jstack pid 查看java线程更详细信息 Java jps 命令查看所有 Java 进程 jstack 查看某个 Java 进程(PID)的所有线程状态 jconsole 来查看某个 Java 进程中线程的运行情况(图形界面) 3.4 * 原理之线程运行 每个线程启动后,虚拟机就会为其分配一块栈内存。每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。 每个方法一个栈帧,方法存在方法区里。

添加图片注释,不超过 140 字(可选)

线程上下文切换:cpu时间片用完;垃圾回收;有更高优先级线程要运行;线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法。切换时线程要保存当前状态(程序计数器,线程私有),频繁切换影响性能。

添加图片注释,不超过 140 字(可选)

3.6 start与run 直接调用run是在主线程中执行了run,没有启动新的线程使用的是main线程, 使用start是启动新的线程,通过新的线程间接执行run方法中的代码,线程状态从new到runnable。 3.7 sleep 与yield sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞) 2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException 3. 睡眠结束后的线程未必会立刻得到执行 4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

1.调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程 2. 具体的实现依赖于操作系统的任务调度器(时间片想让不一定让出去)

时间片指挥分给就绪状态线程。

线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

sleep:在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield或sleep来让出cpu的使用权给其他程序。

Join

等待线程结束

3.9 interrupt 方法详解

打断 sleep,wait,join 的线程 这几个方法都会让线程进入阻塞状态

打断 sleep 的线程, 会清空打断状态,打断标记为false;打断正常线程打断标记为true。

打断park线程,打断标记为true,打断标记为真时,再次park()无法停止。可以使用 Thread.interrupted() 清除打断状态。

3.11 主线程与守护线程

只要有一个线程还在进行,java进程就不会结束。守护线程,只要其它非守

护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

添加图片注释,不超过 140 字(可选)

线程五种状态

添加图片注释,不超过 140 字(可选)

【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

【阻塞状态】 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们

【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

线程六种状态

添加图片注释,不超过 140 字(可选)

NEW 线程刚被创建,但是还没有调用 start() 方法 RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的

RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)

BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分

补充:

  1. 调用start()

  2. 调用 obj.wait() RUNNABLE --> WAITING 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 竞争锁成功,t 线程从 WAITING --> RUNNABLE 竞争锁失败,t 线程从 WAITING --> BLOCKED

  3. 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING 注意是当前线程在t 线程对象的监视器上等待 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

  4. 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

  5. t 线程用 synchronized(obj) 获取了对象锁后 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

  6. 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING 注意是当前线程在t 线程对象的监视器上等待 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

  7. 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

  8. 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE --> TIMED_WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

  9. t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

  10. 当前线程所有代码运行完毕,进入 TERMINATED

4. 共享模型之管程

添加图片注释,不超过 140 字(可选)

临界区

多个线程对共享资源读写操作时会发生指令交错,会出现问题。

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。

静态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

4.2 synchronized 解决方案

它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

添加图片注释,不超过 140 字(可选)

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

4.3 synchronized

添加图片注释,不超过 140 字(可选)

Synchronized加在成员方法上相当于锁住this对象

添加图片注释,不超过 140 字(可选)

Synchronized加在静态方法上相当于锁住类对象

线程八锁

添加图片注释,不超过 140 字(可选)

锁住this对象

添加图片注释,不超过 140 字(可选)

情况3:3 1s 12 或 23 1s 1 或 32 1s 1

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

锁住的对象不同,没有互斥。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

锁住对象不同

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

锁住的是同一个类对象。

4.4 变量的线程安全分析

成员变量和静态变量是否线程安全?

如果它们没有共享,则线程安全

如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

如果只有读操作,则线程安全

如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

局部变量是线程安全的

但局部变量引用的对象则未必

如果该对象没有逃离方法的作用访问,它是线程安全的

如果该对象逃离方法的作用范围,需要考虑线程安全,如子类重写方法,新建线程导致线程不安全。私有修饰符可以保证线程安全。

局部变量会在内存中存在多份,不存在共享。

添加图片注释,不超过 140 字(可选)

局部变量i++ 反编译后只有一行指令iinc 这个与静态变量不同。

添加图片注释,不超过 140 字(可选)

它们的每个方法是原子的 但注意它们多个方法的组合不是原子的。

添加图片注释,不超过 140 字(可选)

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。

添加图片注释,不超过 140 字(可选)

Servlet只有一个示例

添加图片注释,不超过 140 字(可选)

不安全,count可以被共享,count++临界区

添加图片注释,不超过 140 字(可选)

不安全,成员变量被共享。

添加图片注释,不超过 140 字(可选)

第二个属于无状态,安全。

添加图片注释,不超过 140 字(可选)

不安全

添加图片注释,不超过 140 字(可选)

局部变量,安全

添加图片注释,不超过 140 字(可选)

4.6 Monitor 概念

Java 对象头

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

Markword与monitor关联时存monitor的地址。每个对象关联一个monitor

添加图片注释,不超过 140 字(可选)

synchronized 原理进阶

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。有竞争会升级成重量级锁,轻量级锁对使用者是透明的,即语法仍然是 synchronized。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

2.锁膨胀

加锁失败申请重量级锁,object指向重量级锁地址,进入block队列。之前加锁成功解锁同重量级解锁流程。

3.自选优化

未竞争到锁。开始自旋等待避免上下文切换。适合多核cpu。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

4.偏向锁

轻量级锁重入时,每次都要执行CAS操作

一个对象创建时:

如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0

偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数-XX:BiasedLockingStartupDelay=0 来禁用延迟

在代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁。

优先级

偏向锁>轻量级锁>重量级锁

调用 hashCode 会导致偏向锁被撤销。

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

Wait\notify 会撤销偏向锁

批量重偏向

当撤销偏向锁阈值超过 20 次后,会在给这些对象加锁时重新偏向至加锁线程。

批量撤销

当撤销偏向锁阈值超过 40 次后,整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的。

锁消除

JIT会优化热点代码

1.wait notify 原理

添加图片注释,不超过 140 字(可选)

持有Owner的锁对象调用wait方法(obj.wait),就会使当前线程进入WaitSet中,变为WAITING状态。

处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别:

BLOCKED状态的线程是在竞争对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态

WAITING状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态

BLOCKED状态的线程会在锁被释放的时候被唤醒,但是处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒

WAITING 线程会在 Owner 线程(必须拥有锁)调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待

  • obj.notify() 在 object上正在 waitSet 等待的线程中挑一个唤醒

  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

  • wait(Long timeOut) 有时限的等待

wait notify 的正确姿势

sleep(long n) 和 wait(long n) 的区别

1) sleep 是 Thread 方法,而 wait 是 Object 的方法

2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用

3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 (用wait可以提高并发性)

4) 它们状态都是TIMED_WAITING

Wait notify使用模板

添加图片注释,不超过 140 字(可选)

Park & Unpark

添加图片注释,不超过 140 字(可选)

Park后线程进入wait状态

wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必

park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】

park & unpark 可以先 unpark,而 wait & notify 不能先 notify

park

添加图片注释,不超过 140 字(可选)

多次unpark counter还是1

Unpark

添加图片注释,不超过 140 字(可选)

先upark再park

添加图片注释,不超过 140 字(可选)

多把锁

多把不相干的锁

准备多个资源,避免影响并发度。

将锁的粒度细分

好处,是可以增强并发度

坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

活跃性

死锁

t1 线程 获得 A对象锁,接下来想获取B对象的锁

t2 线程 获得 B对象锁,接下来想获取A对象的锁

这样会发生死锁

改变加锁顺序会改变死锁,但会产生饥饿

定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束

饥饿

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

ReentrantLock

相对于 synchronized 它具备如下特点

可中断 可以设置超时时间 可以设置为公平锁 支持多个条件变量

与 synchronized 一样,都支持可重入

添加图片注释,不超过 140 字(可选)

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断

//如果没有竞争那么此方法就会获取lock对象锁

//如果有竞争就进入阻塞队列,可以被其他线程用interrupt方法打断

lock.lockInterruptibly();

锁超时

立即失败

tryLock()

超时失败

tryLock(n, TimeUnit.SECONDS)

公平锁

ReentrantLock 默认是不公平的

ReentrantLock lock = new ReentrantLock(false);

条件变量

synchronized 中也有条件变量,当条件不满足时进入 waitSet 等待 ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点: await 前需要获得锁 await 执行后,会释放锁,进入 conditionObject 等待 await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁 竞争 lock 锁成功后,从 await 后继续执行

Condition waitCigaretteQueue = lock.newCondition();

使用流程:

Await前需要获得锁

Await执行后回释放锁,进入conditionObject等待

Await的线程被唤醒去重新竞争lock锁

竞争成功后从await后继续执行

共享模型之内存

Java 内存模型

原子性 - 保证指令不会受到线程上下文切换的影响

可见性 - 保证指令不会受 cpu 缓存的影响

有序性 - 保证指令不会受 cpu 指令并行优化的影响

可见性

添加图片注释,不超过 140 字(可选)

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

添加图片注释,不超过 140 字(可选)

  1. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率

添加图片注释,不超过 140 字(可选)

  1. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值

添加图片注释,不超过 140 字(可选)

解决

volatile(易变关键字)不会在缓存中获取变量的值,从主存中获取,保证共享变量在多个线程的可见性。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

Synchronize也可解决该问题。

Volatile不能解决指令交错,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性。

有序性

添加图片注释,不超过 140 字(可选)

流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

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

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

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

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

如何保证有序性

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去

而有序性的保证也只是保证了本线程内相关代码不被重排序

double-checked locking 问题

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

21 24可能会指令重排

共享变量完全交给synchronized管理,不会出现问题。

Dcl问题解决

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

happens-before

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

线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

添加图片注释,不超过 140 字(可选)

线程 start 前对变量的写,对该线程开始后对该变量的读可见

添加图片注释,不超过 140 字(可选)

线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待 它结束)

添加图片注释,不超过 140 字(可选)

线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)

添加图片注释,不超过 140 字(可选)

对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子

添加图片注释,不超过 140 字(可选)

线程安全单例习题

饿汉式:类加载就会导致该单实例对象被创建

懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

添加图片注释,不超过 140 字(可选)

  1. 子类会覆盖父类方法导致不安全

添加图片注释,不超过 140 字(可选)

  1. 不设置私有,别的类会无限创建。不能。

  2. 静态变量jvm保证线程安全

  3. 创建单例时可以有更多控制;支持泛型。

添加图片注释,不超过 140 字(可选)

1.创建时有几个就是几个

2.没有·

3.不能

4.不能

5.饿汉式

6.加构造方法

添加图片注释,不超过 140 字(可选)

可以保证线程安全,锁加在了类对象上,锁的范围过大,性能低。Null上不能加锁,对象可变不能加锁。

添加图片注释,不超过 140 字(可选)

1.指令会重排序

添加图片注释,不超过 140 字(可选)

3.防止首次创建并发问题。

添加图片注释,不超过 140 字(可选)

  1. 静态内部类实现懒汉式。类在第一次被用到才会加载。

  2. 不会,由jvm保证。

共享模型之无锁

CAS 与 volatile

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。

Cas操作需要volatile的支持才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么无锁效率高

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时 候,发生上下文切换,进入阻塞。

打个比喻 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火, 等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大

但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还 是会导致上下文切换。

CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

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

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

CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子整数

J.U.C 并发包提供了: AtomicBoolean AtomicInteger AtomicLong

添加图片注释,不超过 140 字(可选)

原子引用

AtomicReference

AtomicMarkableReference

AtomicStampedReference

ABA 问题及解决

Cas只能判断贡献变量的值变没变,不能判断是否被修改过。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

加入版本号解决。AtomicStampedReference

添加图片注释,不超过 140 字(可选)

只查看是否更改过,增加状态位 AtomicMarkableReference

添加图片注释,不超过 140 字(可选)

原子数组 AtomicIntegerArray AtomicLongArray AtomicReferenceArray

添加图片注释,不超过 140 字(可选)

不安全的数组

添加图片注释,不超过 140 字(可选)

安全的数组

添加图片注释,不超过 140 字(可选)

字段更新器

AtomicReferenceFieldUpdater // 域字段

AtomicIntegerFieldUpdater

AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。

添加图片注释,不超过 140 字(可选)

原子累加器

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

在有竞争时,设置多个累加单元,Therad-0累加 Cell[0],而Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的Cell变量,因此减少了CAS 重试失败,从而提高性能。

原理之伪共享

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中 CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

添加图片注释,不超过 140 字(可选)

一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因 此缓存行可以存下 2 个的 Cell 对象。Core-0 要修改 Cell[0] Core-1 要修改 Cell[1] 无论谁修改成功,都会导致对方 Core 的缓存行失效。

@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 空隙,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

添加图片注释,不超过 140 字(可选)

Longadder源码流程

添加图片注释,不超过 140 字(可选)

LongAccumulate

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

Sum方法

添加图片注释,不超过 140 字(可选)

Unsafe

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

添加图片注释,不超过 140 字(可选)

. 共享模型之工具

线程池

  1. 自定义线程池

添加图片注释,不超过 140 字(可选)

ThreadPoolExecutor

添加图片注释,不超过 140 字(可选)

  1. 线程池状态 ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量。

添加图片注释,不超过 140 字(可选)

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 进行赋值 构造方法

添加图片注释,不超过 140 字(可选)

阻塞队列放不下会启用救急线程,有生存时间。如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

没有救济线程,阻塞队列无界。 适用于任务量已知,相对耗时的任务。

添加图片注释,不超过 140 字(可选)

全部是救急线程,生存时间60s。队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的。 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线 程。 适合任务数比较密集,但每个任务执行时间较短的情况。

添加图片注释,不超过 140 字(可选)

希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 个线程,保证池的正常工作 Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改 FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因 此不能调用 ThreadPoolExecutor 中特有的方法 Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

添加图片注释,不超过 140 字(可选)

Shutdown 线程池状态变为 SHUTDOWN - 不会接收新任务 - 但已提交任务会执行完 - 此方法不会阻塞调用线程的执行 调用shuntdown不会等线程结束会继续向下执行。AwaitTermination可以设置等待时间 shutdownNow 线程池状态变为 STOP - 不会接收新任务 - 会将队列中的任务返回 - 并用 interrupt 的方式中断正在执行的任务

添加图片注释,不超过 140 字(可选)

AQS 阻塞式锁和相关的同步器工具的框架

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

非公平锁实现原理 加锁解锁流程

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

可重入原理

加锁时state++,解锁时state--直至减为0才解锁。

添加图片注释,不超过 140 字(可选)

3.可打断原理

不可打断模式:在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。

可打断模式:可直接被打断。

4.公平锁实现原理

先检查 AQS 队列中是否有前驱节点, 没有才去竞争。

5.条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

. 读写锁

ReentrantReadWriteLock

读读可以并发,读写、写写互斥。

读锁不支持条件变量

重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

重入时降级支持:即持有写锁的情况下去获取读锁

读写锁要配合使用,只用读锁没什么效果。

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

  1. Semaphore

信号量,用来限制能同时访问共享资源的线程上限。

可以实现线程连接池,限制线程连接数。

添加图片注释,不超过 140 字(可选)

获取到资源后,state-1直至减到0,其余线程进入aqs队列等待。

添加图片注释,不超过 140 字(可选)

释放许可state+1

添加图片注释,不超过 140 字(可选)

CountdownLatch

用来进行线程同步协作,等待所有线程完成倒计时。 其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。

添加图片注释,不超过 140 字(可选)

用future获取有返回值的任务

添加图片注释,不超过 140 字(可选)

CyclicBarrier

循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置计数个数,每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足计数个数时,继续执行。

计数变为0会恢复。

添加图片注释,不超过 140 字(可选)

CyclicBarrier与CountDownLatch的主要区别在于CyclicBarrier 是可以重用的 CyclicBarrier

线程安全集合类概述

添加图片注释,不超过 140 字(可选)

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: Blocking、CopyOnWrite、Concurrent

Blocking 大部分实现基于锁,并提供用来阻塞的方法

CopyOnWrite 之类容器修改开销相对较重

Concurrent 类型的容器 内部很多操作使用 cas 优化,一般可以提供较高吞吐量;弱一致性 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的 求大小弱一致性,size 操作未必是 100% 准确;读取弱一致性 。

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历。

ConcurrentHashMap

单词计数

添加图片注释,不超过 140 字(可选)

ConcurrentHashMap 原理

Hashmap会自动扩容,jdk7多线程下会发生并发死链问题。

JDK 8 ConcurrentHashMap

添加图片注释,不超过 140 字(可选)

构造器分析

实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建

添加图片注释,不超过 140 字(可选)

无锁

JDK 7 ConcurrentHashMap

它维护了一个 segment 数组,每个 segment 对应一把锁

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

get 流程:get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容。

size 计算流程:计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回。

LinkedBlockingQueue 原理

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

把哑元节点出队,把要出队的节点当作头节点,Item赋值为空

加锁分析

用了两把锁和 dummy 节点

用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行

用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 消费者与消费者线程仍然串行 生产者与生产者线程仍然串行(队头出队,队尾入队)

线程安全分析

当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争

当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争

当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞

性能比较

LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

Linked 支持有界,Array 强制有界

Linked 实现是链表,Array 实现是数组

Linked 是懒惰的,而 Array 需要提前初始化 Node 数组

Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的

Linked 两把锁(锁住队列头队列尾),Array 一把锁

ConcurrentLinkedQueue

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争 只是这【锁】使用了 cas 来实现

CopyOnWriteArrayList

get 弱一致性

迭代器弱一致性

数据库的 MVCC 都是弱一致性的表现 并发高和一致性是矛盾的,需要权衡

  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值