3.并发 共享变量
并发
并发导致共享变量问题
@Slf4j(topic = "c.share")
public class TestShareVar {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
i++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
i--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}
}
15:31:26.186 c.share [main] - i = -37
问题
以上的结果可能是正数、负数、零。因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++
而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
时序图
单线程模式下
sequenceDiagram participant t1 as 线程1 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法,线程内 i = 1 t1 ->> i: putstatic i 写入 1 i ->> t1: getstatic i 获取 1 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: isub 减法,线程内 i = 0 t1 ->> i: putstatic i 写入 0sequenceDiagram participant t1 as 线程1 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法,线程内 i = 1 t1 ->> i: putstatic i 写入 1 i ->> t1: getstatic i 获取 1 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: isub 减法,线程内 i = 0 t1 ->> i: putstatic i 写入 0
多线程模式下-出现负数的情况
sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 -->> t1: 上下文切换 i->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 ->> i: putstatic i 写入 1 t1 -->> t2: 上下文切换 t2 ->> i : putstatic i 写入 -1sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 -->> t1: 上下文切换 i->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 ->> i: putstatic i 写入 1 t1 -->> t2: 上下文切换 t2 ->> i : putstatic i 写入 -1
多线程模式下-出现正数的情况
sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 -->> t2: 上下文切换 i->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 ->> i : putstatic i 写入 -1 t2 -->> t1: 上下文切换 t1 ->> i: putstatic i 写入 1sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 -->> t2: 上下文切换 i->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 ->> i : putstatic i 写入 -1 t2 -->> t1: 上下文切换 t1 ->> i: putstatic i 写入 1
临界区 Critical Section
-
一个程序运行多个线程本身是没有问题的
-
问题出在多个线程访问共享资源
-
多个线程读共享资源其实也没有问题
-
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
-
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized 应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized 俗称【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
synchronized 语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
采用synchronized改造
@Slf4j(topic = "c.share")
public class TestShareVar {
static int i = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
test2();
}
public static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}
public static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
synchronized (lock){
i++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
synchronized (lock){
i--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}
}
19:03:44.568 c.share [main] - i = 0
时序图如下:
sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i participant lock as 对象锁(lock) t2 ->> lock: 线程2尝试获取锁 note over t2,lock: 拥有锁 i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 i = -1 t2 -->> t1: 上下文切换 t1 -x lock: 尝试获取锁,被阻塞(blocked) t1 -->> t2: 上下文切换 t2 ->> i: putstatic i 写入 -1 note over t2,lock : 拥有锁 t2 ->> lock: 释放锁并唤醒阻塞线程 note over t1,lock: 拥有锁 i ->> t1: getstatic i 读取 -1 t1 ->> t1: iconst_1 准备常量 1 t1 ->> t1: iadd 加法 i = 0 t1 ->> i: putstatic i 写入 0 note over t1, lock: 拥有锁 t1 ->> lock: 释放锁并唤醒阻塞线程sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i participant lock as 对象锁(lock) t2 ->> lock: 线程2尝试获取锁 note over t2,lock: 拥有锁 i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 i = -1 t2 -->> t1: 上下文切换 t1 -x lock: 尝试获取锁,被阻塞(blocked) t1 -->> t2: 上下文切换 t2 ->> i: putstatic i 写入 -1 note over t2,lock : 拥有锁 t2 ->> lock: 释放锁并唤醒阻塞线程 note over t1,lock: 拥有锁 i ->> t1: getstatic i 读取 -1 t1 ->> t1: iconst_1 准备常量 1 t1 ->> t1: iadd 加法 i = 0 t1 ->> i: putstatic i 写入 0 note over t1, lock: 拥有锁 t1 ->> lock: 释放锁并唤醒阻塞线程
面向对象的方式改造代码
@Slf4j(topic = "c.test8")
public class Test8 {
public static void main(String[] args) throws InterruptedException {
TestLock lock = new TestLock();
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
lock.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
lock.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", lock.getI());
}
}
class TestLock {
private Integer i = 0;
public void increment(){
synchronized (this){
i++;
}
}
public void decrement(){
synchronized (this){
i--;
}
}
public Integer getI(){
synchronized (this){
return i;
}
}
}
方法上的synchronized
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
贴在方法上等于锁住的this对象
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
贴在静态方法上等价于锁住的是类对象
线程八锁问题
其实就是考察 synchronized 锁住的是哪个对象
情况1:12 或 21
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况2:1s后12,或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况3:3 1s 12 或 23 1s 1 或 32 1s 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}
情况4:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
情况5:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况6:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况7:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
情况8:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果他们都没有被共享则线程安全
- 如果他们被共享了,则看他们的状态是否可改变:
- 如果只有读操作,则线程安全
- 如果有读写操作,者这段代码是临界区,需要考虑线程安全问题
局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量引用的对象就不一定
- 如果该对象没有脱离方法的作用范围,它是线程安全的
- 如果该对象逃离了方法的作用范围,需要考虑线程安全问题
常见线程安全类
- String
- Integer
- StringBuffffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的
Monitor 对象头
普通对象
Mark Word
:主要用来存储对象自身的运行时数据Klass Word
: 指向Class对象
数组对象
相对于普通对象多了记录数组长度
Mark Word 结构
32位虚拟机
64位虚拟机
Monitor(锁)
Monitor 被翻译为监视器或管程
每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁(重量级)以后,该对象头的Mard word中就被设置指向Monitor对象的指针
当没有线程执行同步代码块内容时:
当线程1执行到同步代码块时,对象的mark word 指针会指向Monitor, 并且其他线程如果也访问到同步代码块的时候会进入阻塞队列中,等待Thread-1释放锁:
整体流程如下:
- 刚开始
Monitor
的owner
为null
- 当Thread-1执行到
synchronized(obj)
就会将Monitor
的owner
置为:Thread-1
,Monitor
中只能有一个Owner
- 在
Thread-1
上锁的过程中,如果Thread-2
,Thread-3
也执行到synchronized(obj)
,就会进入EntryList
进入Blocked
状态 - Thread-1执行完同步代码后,就会唤醒EntryList中的所有线程来竞争锁,竞争的时候是非公平的。
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则
synchronized 字节码
public class TestSynchronizedCode {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
打印字节码
javap -c TestSynchronizedCode.class
Compiled from "TestSynchronizedCode.java"
public class cn.com.wuhm.juc.three.n4.TestSynchronizedCode {
static final java.lang.Object lock;
static int counter;
public cn.com.wuhm.juc.three.n4.TestSynchronizedCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // lock 的引用
3: lot
4: astore_1 // lock引用 slot 1
5: monitorenter // 将 lock对象的MarkWord置为Monitor指针
6: getstatic #3 // 获取常量 counter:0
9: iconst_1 // 准备常数 1
10: iadd // + 1
11: putstatic #3 // counter = 1
14: aload_1 // lock 引用
15: monitorexit // 将lock对象的MarkWord重置,并唤醒EntryList
16: goto 24 // 跳转24行
19: astore_2 // lock引用 slot 2
20: aload_1 // lock 引用
21: monitorexit // 将lock对象的MarkWord重置,并唤醒EntryList
22: aload_2
23: athrow // throw e
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
static {};
Code:
0: new #4 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field lock:Ljava/lang/Object;
10: iconst_0
11: putstatic #3 // Field counter:I
14: return
}
==从上面的字节码中可以看出,同步代码块执行完后或有异常发生时会自动释放锁资源==
轻量级锁/偏向锁/重量级锁
轻量级锁
轻量级锁使用场景:如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(也就是没有竞争),那么就可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍然是 synchronized
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 创建 锁记录
(Lock Record)
对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中
Object reference
指向锁对象,并尝试用cas
替换Object
的Mark Word
,将Mark Word
的值存入锁记录
-
如果 cas 替换成功,对象头中存储了
锁记录地址和状态 00
,表示由该线程给对象加锁,这时图示如下 -
如果
cas
失败,有两种情况-
如果是其它线程已经持有了该
Object
的轻量级锁,这时表明有竞争,进入锁膨胀过程 -
如果是自己执行了
synchronized
锁重入,那么再添加一条Lock Record
作为重入的计数
-
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
-
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
-
成功,则解锁成功
-
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
-
(轻量级)锁膨胀(为重量级锁)
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
-
当
Thread-1
进行轻量级加锁时,Thread-0
已经对该对象加了轻量级锁 -
这是Thread-1加轻量级锁失败,进入锁膨胀流程
- Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntryList
-
当
Thread-0
退出同步块解锁时,使用cas
将Mark Word
的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor
地址找到Monitor
对象,设置Owner
为null
,唤醒EntryList
中BLOCKED
线程
自旋优化
重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)
自旋重试成功的情况
线程1(core1) | 对象Mark word | 线程2(core2) |
---|---|---|
- | 10(重量级锁) | - |
访问同步块获取Monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块代码 | 10(重量锁)重量锁指针 | - |
执行同步块代码 | 10(重量锁)重量锁指针 | 访问同步块获取Monitor |
执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
执行完毕 | 10(重量锁)重量锁指针 | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁)重量锁指针 | 成功(加锁) |
- | 10(重量锁)重量锁指针 | 执行同步块代码 |
… | … |
自旋重试成功的情况
线程1(core1) | 对象Mark word | 线程2(core2) |
---|---|---|
- | 10(重量级锁) | - |
访问同步块获取Monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块代码 | 10(重量锁)重量锁指针 | - |
执行同步块代码 | 10(重量锁)重量锁指针 | 访问同步块获取Monitor |
执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块代码 | 01(无锁) | … |
执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块代码 | 10(重量锁)重量锁指针 | 阻塞 |
… |