java并发 day02 临界区和竞态条件、synchronized、线程安全 、对象头 、Monitor管程 、 wait notify、park&unpark、 ReentrantLock

临界区和竞态条件

临界区 Critical Section

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

竞态条件 Race Condition

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

为了避免临界区的竞态条件发生,有多种手段可以达到目的。
1、阻塞式的解决方案:synchronized,Lock

2、非阻塞式的解决方案:原子变量

synchronized

在这里插入图片描述

synchronized 实际上是使用对象锁保证了临界区代码的原子性,临界区的代码对外是不可分割的部分,不会被线程的上下文切换所打断。
有2个线程,1个加锁(synchronized),另一个不加锁是否可行?
不行,上下文切换到不加锁的线程的时候,依旧可以对共享数据进行修改,无法保证数据的可见性。

在这里插入图片描述

在这里插入图片描述

synchronized的底层原理

在这里插入图片描述

1、synchronized的底层涉及java的Mark Word和monitor两个方面。
2、从字节码的角度上看,当字节码指令执行到monitorenter的时候,也就是执行到synchronized加锁的时候,锁对象的Mark Word会指向monitor。
3、如果monitor的owner区没有线程则会占有owner区,反之会进入EntryList进入阻塞状态。
4、当线程占有owner区,并且字节码指令执行到monitorexit的时候,执行线程会退出owner区并唤醒EntryList的线程。
5、当临界区代码发生异常的时候,也会执行monitorexit释放锁的字节码指令,保证发生异常的时候锁一定会被释放。

线程安全

变量的线程安全问题

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

如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
----如果只有读操作,则线程安全
----如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

局部变量是线程安全的
但局部变量引用的对象则未必
----如果该对象没有逃离方法的作用访问,它是线程安全的
----如果该对象逃离方法的作用范围,需要考虑线程安全

在这里插入图片描述

在这里插入图片描述

局部变量引用的对象是共享的,所以需要考虑线程安全问题。

在这里插入图片描述
在这里插入图片描述

list 是局部变量,每个线程调用时会创建其不同实例,没有共享

常见线程安全类

常见线程安全类

1、String
2、Integer
3、StringBuffer
4、Random
5、Vector
6、Hashtable
7、java.util.concurrent 包下的类(JUC)

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每个方法是原子的。

但注意它们多个方法的组合不是原子的,见后面分析

在这里插入图片描述

这并不是线程安全的,因为foo()方法需要被子类重写,它的因为是不固定的,可能会导致不安全的发生,这种方法被称之为外星方法。解决方法,使用final修饰

String类为什么要使用final修饰?

保证String类中的方法行为不会被子类重写,以此来保证线程安全的问题。体现的是一种闭合原则。

对象头

java对象由两部分组成,java对象头+实例数据+对齐填充字节
java对象头 = Mark Word + Klass Word (+Array Length)
实例数据:java代码中能够看到的属性和他们的值
对齐填充字节:JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数

java的对象头(32位的虚拟机)

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(64bits)                                               |
|-----------------------------------------------------------------------------------------------------------------|
|                       Mark Word(32bits)                           |  Klass Word(32bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|     hashcode:25                      | age:4 | biased_lock:0 | 01 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
|     thread:23              | epoch:2 | age:4 | biased_lock:1 | 01 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_lock_record:30                                    | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_heavyweight_monitor:30                            | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|


java的对象头(64位的虚拟机)

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(128bits)                                              |
|-----------------------------------------------------------------------------------------------------------------|
|                                   Mark Word(64bits)               |  Klass Word(64bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|    unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:0| 01 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
|    thread:54|      epoch:2       |unused:1|age:4|biase_lock:1| 01 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|                        ptr_to_lock_record:62                 | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                       ptr_to_heavyweight_monitor:62          | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|


数组对象
在这里插入图片描述

32位和64位的数组对象的长度都是4个字节。

数据解读

Klass Word指向了对象所存储的class,是指向类的指针。
Mark Word 存储了hashCode和锁信息。
age:垃圾回收的分代年龄
biased_block:偏向锁的状态

在这里插入图片描述

Monitor管程

Monitor :监视器或管程。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
不加 synchronized 的对象不会关联监视器,不遵从以上规则
monitor是操作系统提供的

Monitor 结构

在这里插入图片描述

1、当使用synchronized同步锁的时候,Mark Word会指向monitor
2、刚开始 Monitor 中 Owner 为 null ,当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
3、在 Thread-2上锁的过程中,如果 Thread-1,Thread-3 也来执行 synchronized(obj),就会进入EntryList BLOCKED
4、Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
5、WaitSet 是之前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify)

synchronized的优化

monitor是由操作系统提供的,成本比较高,对程序运行的性能是有影响的。

1、轻量级锁

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

在这里插入图片描述

CAS交换成功,对象头存储了锁记录地址和状态00(轻量级锁),表示有该线程给对象加锁。

在这里插入图片描述

CAS交换失败:
1、如果是其它线程占有了锁对象,这时表明有竞争,进入锁膨胀状态升级为重量级锁
2、如果是相同的锁对象,则会执行synchronized锁重入,同时增加一条Lock Record作为锁重入的计数(每次锁重入都会执行CAS)

在这里插入图片描述

2、重量级锁

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

在这里插入图片描述

Thread1进行轻量级加锁时,Thread0已经占有了锁对象。这时会进入锁膨胀状态:
1、即为锁对象申请monitor,让锁对象执行重量级锁的地址
2、进入Monitor的EntryList阻塞区

在这里插入图片描述

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

3、自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能

4、偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用CAS 将线程 ID 设置到对象的 Mark Word 头,之后发生这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
使用线程ID替换Mark Word(偏向当前线程)

在这里插入图片描述

偏向锁状态

在这里插入图片描述

1、如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
2、偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
3、如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

4、开启偏向锁的时候,对象头并不会保存hashCode。所以,如果要获取hashCode,则必须得撤销偏向锁。
5、撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW),所以撤销偏向锁是很耗性能的。
6、在调用hashCode时记得使用 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode

撤销偏向锁

1、调用hashCode自动撤销偏向锁
2、当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
3、调用wait/notify会撤销偏向锁(重量级锁)
4、撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
5、偏向锁的撤销需要等待全局安全点(safe point),暂停持有偏向锁的线程,检查持有偏向锁的线程状态。首先遍历当前JVM的所有线程,如果能找到偏向线程,则说明偏向的线程还存活,此时检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁。可以看出撤销偏向锁的时候会导致stop the word。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID(撤销偏向锁)
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程t2

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

5、锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。(JIT即时编译器的优化)

6、锁粗化

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。(JIT即时编译器的优化)

wait notify原理

在这里插入图片描述

1、Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 并释放锁
2、BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
3、BLOCKED 线程会在 Owner 线程释放锁时唤醒
4、WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争

api
在这里插入图片描述

它们都是线程之间进行协作的手段,都属于 Object对象的方法。必须获得此对象的锁,才能调用这几个方法。(必须使用synchronized获取锁对象,再通过锁对象调用相关方法,重量级锁)

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

1、 sleep 是 Thread 方法,而 wait 是 Object 的方法
2、 sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
3、sleep 在睡眠的同时,不会释放对象锁的,但 wait在等待的时候会释放对象锁
4、 它们状态 TIMED_WAITING

wait-notify的正确使用
在这里插入图片描述

park&unpark

在这里插入图片描述

与 Object 的 wait & notify 相比

1、wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
2、park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
3、park & unpark 可以先 unpark,而 wait & notify 不能先 notify

每个线程都有一个parker对象,由三部分组成_counter,_cond和_mutex

调用park
在这里插入图片描述

调用unpark
在这里插入图片描述

先调用unpark再调用park
在这里插入图片描述

线程状态转换

在这里插入图片描述

情况1: NEW --> RUNNABLE

调用start()方法

情况2:RUNNABLE <–> WAITING

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

情况3: RUNNABLE <–> WAITING

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

情况4:RUNNABLE <–> WAITING

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

情况 5\6\7\8 RUNNABLE <–> TIMED_WAITING

5、调用obj.wait(long n)
6、调用t.join(long n)
7、调用Thread.sleep(long n)
8、调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时

情况 9 RUNNABLE <–> BLOCKED

t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED

情况 10 RUNNABLE <–> TERMINATED

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

活跃性

1、死锁

一个线程需要同时获取多把锁,这时就容易发生死锁
t1线程获取A锁,等待B锁,t2线程获取B锁,等待A锁。彼此之间的锁无法释放而导致死锁。
顺序加锁可以解决死锁。

2、定位死锁

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

3、活锁

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

在这里插入图片描述

4、饥饿

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束。读写锁时会涉及饥饿问题。

ReentrantLock

特点

可中断
可以设置超时时间
可以设置为公平锁 ,防止线程饥饿,先进先出
支持多个条件变量
可重入

在这里插入图片描述

可重入

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

@Slf4j
public class ReentranckLock {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
          lock.lock();
          try{
              log.info("main...");
              m1();
          }finally {
              lock.unlock();
          }
    }
    private static void m1() {
        lock.lock();
        try {
            log.info("m1。。。");
        }finally {
            lock.unlock();
        }
    }
}

可中断(中断死锁)

@Slf4j
public class ReentranckLock {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
          Thread t1 = new Thread("t1"){
              @Override
              public void run() {
                  try {

                      lock.lockInterruptibly();
                      log.info("t1获取锁。。。。");
                  } catch (InterruptedException e) {
                      log.info("打断t1");
                      e.printStackTrace();
                  }finally {
                      log.info("t1释放锁。。。。。");
                      lock.unlock();
                  }
              }
          };
          lock.lock();
          log.info("main获得锁。。。。");
          t1.start();
          try {
              t1.interrupt();
              log.info("执行打断");
          }finally {
              lock.unlock();
          }
    }
}

锁超时(解决死锁)

@Slf4j
public class ReentranckLock {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
          Thread t1 = new Thread(()->{
              log.info("尝试获取锁");
              if (!lock.tryLock(2, TimeUnit.SECONDS)) {
                  log.info("获取不到锁");
                  return;
              }
              try {
                  log.info("获取到了锁");
              }finally {
                  lock.unlock();
              }
          },"t1");
          lock.lock();
          log.info("获取到了锁");
          t1.start();
    }
}

公平锁

ReentrantLock 默认是不公平的
ReentrantLock lock = new ReentrantLock(true);

条件变量

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的
Condition condition = new ReentrantLock().newCondition();

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

halulu.me

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值