JUC-第3章 Java线程

1共享带来的问题

以上的结果可能是正数、负数、零。为什么呢?因为Java中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析

临界区 Critical Section

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题

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

竞态条件 Race Condition

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

2 synchronized 解决方案

应用之互斥

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

·阻塞式的解决方案:synchronized,Lock

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

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

虽然 java中互斥和同步都可以采用synchronized 关键字来完成,但它们还是有区别的:互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

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

思考

如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性

如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象

如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象

面向对象改进

把需要保护的共享变量放入一个类

3 方法上的 synchronized

4 变量的线程安全分析

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

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

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

局部变量是否线程安全?

局部变量是线程安全的

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

  • 如果该对象没有逃离方法的作用范围,它是线程安全的
  • 如果该对象逃离方法的作用范围,需要考虑线程安全

5 Monitor 概念

5.1 Java 对象头以 32 位虚拟机为例)

普通对象

(Mark Word 主要用来存储对象自身的运行时数据Klass Word 指向Class对象)

数组对象

(相对于普通对象多了记录数组长度)

5.2 Mark Word 结构

不同对象和状态下结构和含义也不同

64 位虚拟机 Mark Word

5.3 原理之 Monitor(锁)

Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性.

Monitor 被翻译为监视器管程 

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

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

synchronized 必须是进入同一个对象的 monitor 才有上述的效果,不加 synchronized 的对象不会关联监视器,不遵从以上规则)

5.5 原理之 synchronized 进阶 (锁升级)

1. (不涉及Monitor的)轻量级锁

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

轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

·创建锁记录(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的值恢复给对象头

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

2. (轻量级)锁膨胀(为重量级锁)

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

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

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

为Object对象申请Monitor锁,让 Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

当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是操作系统赋予的id 和 Thread的id是不同的)。

偏向状态

一个对象创建时:

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

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

如果没有开启偏向锁,那么对象创建后,markword值为 0x01 即最后 3 位为 001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。

批量重偏向

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

当(某类型对象)撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给(所有这种类型的状态为偏向锁的)对象加锁时重新偏向至新的加锁线程

批量撤销(偏向) 

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

  1. 锁消除

锁粗化 (up没有找到真正能证明锁粗化的例子,所以没讲)

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

6 wait / notify

6.1 原理之wait / notify

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

6.2 API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法.

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

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

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

3) sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁

4) 它们状态 TIMED_WAITING

7 Park & Unpark

7.1基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程

LockSupport.park();

// 恢复某个线程的运行

LockSupport.unpark(暂停线程对象)

7.2 特点

与 Object的 wait & notify 相比wait,notify 和notifyAll 必须配合Object Monitor一起使用,而 park,unpark 不必

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

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

7.3原理

每个线程都有自己的一个(C代码实现的) Parker 对象,由三部分组成 _counter , _cond 和_mutex

先调用park 再调用unpark


1. 当前线程调用Unsafe.park() 方法
2. 检查 _counter,本情况为0,这时获得 _mutex 互斥锁
3. 线程进入_cond条件变量阻塞
4. 设置_counter = 0


1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 唤醒 _cond 条件变量中的 Thread_0
3. Thread_0 恢复运行
4. 设置 _counter 为 0
先调用unpark 再调用park


1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 当前线程调用 Unsafe.park() 方法
3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
4. 设置 _counter 为 0

8重新理解线程状态转换

假设有线程 Thread t

情况1 NEW --> RUNNABLE

当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况2 RUNNABLE <--> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

调用 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 RUNNABLE <--> TIMED_WAITING

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 RUNNABLE <--> TIMED_WAITING

当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING

注意是当前线程在t 线程对象的监视器上等待

当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <--> TIMED_WAITING

当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

当前线程等待时间超过了 n 毫秒,当前线程从TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <--> TIMED_WAITING

当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE --> TIMED_WAITING

调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

情况 9 RUNNABLE <--> BLOCKED

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

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

情况 10 RUNNABLE --> TERMINATED

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

9多把锁

一间大屋子有两个功能:睡觉、学习,互不相干。现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低,解决方法是准备多个房间(多个对象锁)

将锁的粒度细分:

好处,是可以增强并发度 

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

10活跃性

线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况。

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

定位死锁 

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

避免死锁要注意加锁顺序

另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

活锁

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

饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束。

11 ReentrantLock

相对于 synchronized 它具备如下特点

·可中断

·可以设置超时时间

·可以设置为公平锁

·支持多个条件变量

与 synchronized 一样,都支持可重入

可重入

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

可打断 lock.lockInterruptibly()

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

锁(可设置)超时

立刻返回结果 lock.tryLock()

(可设置是否为)公平锁

公平: 先来就能先执行

不公平: 不保证先来就先执行

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

改为公平锁ReentrantLock lock = new ReentrantLock(true);

(多个)条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

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

使用要点: 

·await 前需要获得锁

·await 执行后,会释放锁,进入 conditionObject 等待

·await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁

·竞争 lock 锁成功后,从 await 后继续执行

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值