目录

1.线程安全问题

1.1Java线程安全问题体现

临界区

竞态条件

2.针对原子性问题的解决方案

2.1互斥和同步的区别

2.2synchronized关键字

2.3synchronized底层原理

2.31Java对象头

2.3.2Monitor(锁)

2.4synchronized原理之字节码

2.5synchronized进阶原理

2.5.1synchronized优化原理_轻量级锁

3.自旋优化

4.偏向锁

2.25锁消除

2.6变量的线程安全分析

2.7常见线程安全类:

2.8wait notify

2.8.1原理介绍之wait/notify

2.8.2API介绍

2.9wait notify正确使用姿势

2.10.保护性暂停模式

原理之join

3。Park_Unpark使用

特点

unpark可以在park之前调用,也可以在park之后调用​编辑

3.1重新理解线程状态转换

4.多把锁

4.1多把互不相干的锁

将锁的粒度细分

5.活跃性

5.1死锁

5.2定位死锁

5.3活锁

6.ReentrantLock

基本语法

可重入

可打断

锁超时

条件变量

总结


1.线程安全问题

1.1Java线程安全问题体现

两个线程对初始值为0的静态变量一个做自增,一个做自减,结果是0吗?

/**
     *  JMM之原子性问题,由于上下文切换导致的指令交错进行,也就是!原子性问题
     */

    // 共享资源
    static int count;
    public static void main(String[] args) throws InterruptedException {


        // 会产生原子性问题的情况,  本质: 多线程指令交错执行
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        },"t1");
        // t1线程的解决:
        /*Thread t1 = new Thread(() -> {
            synchronized ("abc") {
                for (int i = 0; i < 5000; i++) {
                    count++;
                }
            }
        },"t1");*/
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        },"t2");
        // t2的解决版:
        /*Thread t2 = new Thread(() -> {
            synchronized ("abc") {
                for (int i = 0; i < 5000; i++) {
                    count--;
                }
            }
        },"t1");
*/
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count);

上诉代码会有原子性问题!

Java内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

可能出现错误情况:

临界区

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

竞态条件

2.针对原子性问题的解决方案

为了避免出现临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized, Lock。
  • 非阻塞式:原子变量,concurrent包下的变量的一些api

2.1互斥和同步的区别

互斥: 指不同线程不能同时对共享资源进行操作,不同线程之间是互斥的.必须当前线程执行结束
          才能别的线程来执行.
同步: 线程排队执行,两个线程之间也不能同时操作.但是是按照某种先后次序来运行,依赖于要执行的任务

同步是更加复杂的互斥,是按照某种先后次序来运行的...

2.2synchronized关键字

2.3synchronized底层原理

2.31Java对象头

以32位虚拟机为例

普通对象

数组对象

其中MarkWord结构为

2.3.2Monitor(锁)

Monitor被翻译为监视器管程

每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁
Monitor对象是操作系统提供的一个对象!

Monitor结构如下:

2.4synchronized原理之字节码

getstatic  #2  拿到lock锁对象  引用地址

dup 复制一份

astore_1 lock引用 ->slot 1(存储到)解锁时用

执行synchronized monittorenter  将lock对象MarkWord置为Monitor指针

count++;

aload_1 拿到临时对象 找到lock引用

monitorexit   将lock对象MarkWord重置,唤醒EntryList

hashcode 分带年龄存储在monitor中

2.5synchronized进阶原理

2.5.1synchronized优化原理_轻量级锁

轻量级锁是对重量级锁的一种优化,是对象结构对象头中锁的一种状态。轻量级锁的使用一定是处于不存在多个线程竞争下
    的场景.首先轻量级锁能提升同步性能的依据是"对于绝大部分的锁,在一个同步周期内是不存在线程竞争的!".所以一旦出现
    两条或者以上的线程竞争同一把锁的情况,轻量级锁会进行锁膨胀升级为重量级锁.将锁对象Mark Word中锁标志位从00
    转变为10. 如果不存在竞争的话,轻量级锁便通过CAS操作避免了使用互斥量而消耗的性能..

代码进入到同步代码块后,先看对象的锁标志位状态.如果是无锁状态,虚拟机会现在当前栈帧中创建一个Lock Record锁记录 的空间,该空间用于存储对象头中的Mark Word的拷贝..然后虚拟机常用CAS操作.将共享对象的对象头中Mark Word中前30 个bit替换成指向锁记录的指针.如果操作成功,说明当前线程获取到了对象的锁.并将Mark Word中锁标识位的01变为00.也就将 锁对象和线程绑定在一起了.如果操作失败了,说明一定有存在竞争该锁对象的其他的线程,会先检查Mark Word中指向的是否是当前线程栈帧中的锁记录,如果是说明是当前线程获取到了锁,就直接执行代码块.如果不是说明当前对象锁是被其他线程获取到了. 此时就会进行锁膨胀升级为重量级锁.且与其竞争的线程进入阻塞状态!    

注意可能出现锁重入现象!当前线程又对同一个对象尝试加锁!锁重入会再创建一个锁记录作为重入的计数!

CAS操作尝试将锁对象的Mark Word恢复回去!!! 

   

锁膨胀

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

当Thread-1进行轻量级加锁时,thread-0已经对该对象加了轻量级锁

这时thread-1加轻量级锁失败,进入锁膨胀流程

        即为Object对象申请Monitor锁,让Object指向重量级锁地址

        然后自己进入Monitor的EntryList BLOCKED

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

线程会先在自己帧栈中创建lockrecord,将对象markword拷贝到自己Lock Record中。随后线程通过CAS操作尝试将对象的markword更新为指像自身的Lock Record的指针。

3.自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋要使用cpu,自旋优化选择多核cpu情况下。

自旋重试几次失败就要进入阻塞状态

4.偏向锁

轻量级锁在没有竞争时(就自己这个线程,每次重入仍然需要执行CAS操作。

Java6中引入偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的MarkWord(MarkWord不存储重量级锁的指针,不存储锁记录的地址了,存线程ID,加锁发现线程ID是自己的)头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

MarkWord中biased_lock中表示是否启用偏向锁

即使释放锁markword还是会记录线程id

1.撤销-调用对象hashCode

调用hashcode就会禁用偏向锁,因为Markword偏向锁存储线程id要占用54个字节,hashcode会占用31个字节

2.撤销——其它线程使用对象

t1线程使用对象,偏向锁,t1使用完之后,t2来使用偏向锁升级为轻量级锁。

3.撤销——调用wait/notify

wait/notify只有重量级锁才能使用

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID

当撤销偏向锁阈值超过20次后,jvm会觉得,我是不是偏向错了呢,于是会给这现象对象加锁时重新偏向至加锁线程。

批量撤销

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

性能消耗——轻量级锁

CAS操作

2.25锁消除

JIT 即时编译器 ,对其中热点代码进行优化 分析局部遍历不肯能逃出作用范围,就会进行锁消除

2.6变量的线程安全分析

数组,对象是堆中的,不是线程安全的

成员变量安全问题:

错误分析:

定义一个静态变量ArrayList,局部变量中定义ArrayList引用ArrayList();

2.7常见线程安全类:

String

Integer

StringBuffer

Vector

HashTable

java.util.concurrent包下的类

线程安全类方法的组合不是线程安全的

String,Integer这些类只能读不能改,线程安全

2.8wait notify

为什么需要wait?——————这里可以联想到Redis的多路复用

2.8.1原理介绍之wait/notify

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

2.8.2API介绍

IllegalMonitorStateException

它们都是线程之间协作的手段,属于Object对象的方法,必须要获得对象锁才能使用该API

2.9wait notify正确使用姿势

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

方法,使用,释放锁

注意: 关于notify()时,可能会唤醒WaitSet中不想唤醒的线程.   ====>   虚假唤醒的问题!
   notify()是按照JVM规范随机唤醒,而hotspot虚拟机规定队列式唤醒!

1.防止虚假唤醒

生产者把队列元素添加满了之后,进入休眠状态,三个线程生产者

一个线程消费者消费一个元素后唤醒线程,唤醒的线程接着上次wait()继续执行明明当前线程条件还未满足,但还是会往wait()方法下面的代码块中
去执行! 这就是虚假等待问题造成的未满足条件却继续向下执行!所以要使用while来判断,而不是if()

2.多个线程唤醒的情况

while循环能确保线程在被唤醒后重新验证等待条件,有效处理虚假唤醒和多线程竞争的场景,保证线程协作的正确性。

2.10.保护性暂停模式

即Guarded Suspension,用在一个线程等待另一个线程的执行结果

要点

场景引用:

GuardedObject对象

        结合wait()notify的正确姿势使用!

本质:  通过GuardedObject对象来获取到两个线程通信之间的数据!

增加超时功能

原理之join

是一种让线程同步的方法.让线程ab从并行执行,变为串行执行

  • join()方法底层是调用了join(0),底层调用了wait(0);  要一直等待,要线程执行完之后主线程才会继续...
  • join(timeout)方法底层调用wait(delay),内部是一个while循环,每次来判断delay(还需要等待的时间),来决定是否还等待...


在main()线程中调用t1.join()
    底层在t1线程没有执行结束的时候,对主线程采用了等待唤醒机制,底层实际上调用的wait()      方法,直到join的线程执行结束后被notify()[由JVM来唤醒]

join()底层使用了wait()方法来实现线程同步,join()方法的内部实现是保护性暂停模式的体现!
区别: 

  • - 保护性暂停模式是一个线程等待另一个线程的执行结果
  • - join()是一个线程等待另一个线程的结束!

3。Park_Unpark使用

特点
unpark可以在park之前调用,也可以在park之后调用

原理:

每个线程都会有一个Parker对象,由三部分组成

3.1重新理解线程状态转换

一. WAITING  <---> RUNNABLE

情况二

4.多把锁

4.1多把互不相干的锁

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,容易发生死锁。

5.活跃性

5.1死锁

处于死锁的线程全部都处于BLOCK状态,都在entryList中等待了.

5.2定位死锁

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

5.3活锁

两个线程互相改变对方线程的结束条件,导致谁也无法结束!

如何避免活锁??
- 给发生活锁的线程添加一些随机睡眠时间,保证一个线程先执行结束...
- ReentrantLock

6.ReentrantLock

相对于synchronized它具备如下特点

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

与synchronized一样,都支持可重入

基本语法

可重入

可打断

注意: 这里可打断是调用一个可打断的api,调用.lock()的线程和synchronized的在等待的时候都不可以倍打断
  如果想让当前线程在等待的过程中可以被其他线程调用interrupt()方法打断的话,在加锁的时候通过 lock.lockInterruptibly()方法来加锁..
这个api如果当前线程处于等待阻塞状态,允许其他线程来使用interrupt()来打断..

对interrupt()的复习:

1.场景: (1)可以打断处于阻塞状态(sleep wait join)的线程 注意无法打断等待synchronized的线程...
     (2)打断正在运行的线程(程序继续运行..打断标记为true)

锁超时

主动的方式避免死等的一种手段...
等待多久还没获取到锁就不等了,执行别的事情了!
调用lock.tryLock()的api,tryLock()可以带参数,是等待时间...

思想: 避免无限制的等待,避免死锁.

条件变量

总结

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐