并发编程系列—深入理解管程

共享模型之管程

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源

多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题

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

竞态条件 Race Condition

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

3.1 synchronized

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

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

语法:

synchronized(对象) // 线程1, 线程2(blocked)
{
	//临界区
}

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

临界区:对共享代码有读写操作的代码段

方法上的 synchronized

class Test{
	public synchronized void test() {
        
	}
}//等价于
class Test{
	public void test() {
   		 synchronized(this) {
             
   		 }
    }
}

静态方法

class Test{
	public synchronized static void test() {
        
	}
}
//等价于
class Test{
    public static void test() {
   	 	synchronized(Test.class) {
            
   	 	}
    }
}

synchronized原理进阶

1、轻量级锁

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

static final Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块 A
		method2();
	}
}
public static void method2() {
    synchronized( obj ) {
    	// 同步块 B
    }
}
  1. 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

  2. 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

  3. 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁

  4. 如果 cas 失败,有两种情况 :

    如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

  5. 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减

  6. 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

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

2、锁膨胀

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

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
	// 同步块
	}
}
  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

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

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

  3. 当 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。以后只要不发生竞争,这个对象就归该线程所有。

tatic final Object obj = new Object();
public static void m1() {
	synchronized( obj ) {
	// 同步块 A
	m2();
	}
}
public static void m2() {
synchronized( obj ) {
	// 同步块 B
	m3();
	}
}
public static void m3() {
	synchronized( obj ) {
    // 同步块 C
	}
}

对象头格式

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

线程八锁

情况1:输出12或者21

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 1秒后1

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 1秒后12 32 1秒后1 23 1秒后 1

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 1秒后 1

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 (注意听)

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

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

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

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();
}

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

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

  • 如果他们被共享了,根据他们的状态是否能够被改变,分为

    只有读操作,则线程安全

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

局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量的引用未必安全

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

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

    privatefinal 提供【安全】的意义所在,请体会开闭原则中的【闭】

3.2 常见线程安全类

String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类

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

  • 它们的每个方法是原子的
  • 注意它们多个方法的组合不是原子的
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {   //线程不安全
	table.put("key", value);
}

判断下列变量是否线程安全

public class MyServlet extends HttpServlet {
// 不安全 只有Hashtable
Map<String,Object> map = new HashMap<>();
// 安全
String S1 = "...";
// 安全 
final String S2 = "...";
// 不安全
Date D1 = new Date();
// 不安全
final Date D2 = new Date();

3.3 Monitor 概念

以32位虚拟机为例

Integer 8+4字节

int 4字节

普通对象

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

在这里插入图片描述

其中 Mark Word 结构为
在这里插入图片描述
64 位虚拟机 Mark Word

在这里插入图片描述

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 时会分析

3.4 wait notify

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

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

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

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

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

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

  4. 它们状态 TIMED_WAITING

原理

在这里插入图片描述

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

使用模板:

synchronized(lock) {
	while(条件不成立) {
		lock.wait();
	}
// 干活
}
//另一个线程
synchronized(lock) {
	lock.notifyAll();
}

3.5 Park & Unpark

它们是 LockSupport 类中的静态方法

LockSupport.park();  // 暂停当前线程  

LockSupport.unpark(暂停线程对象)  // 恢复某个线程的运行  

与 Object 的 wait & notify 相比

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

原理

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

在这里插入图片描述
当前线程调用 Unsafe.park() 方法,检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁,线程进入 _cond 条件变量阻塞,设置 _counter = 0

在这里插入图片描述
调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1, 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0

在这里插入图片描述

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

3.6 线程状态转换

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

3.7 锁的活跃性

死锁:

一个线程需要同时获取多把锁,这时就容易发生死锁
t1线程获得 A对象锁,接下来想获取B对象的锁 t2 线程获得 B对象锁,接下来想获取A对象的锁例

Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
	synchronized (A) {
		log.debug("lock A");
		sleep(1);
		synchronized (B) {
            log.debug("lock B");
			log.debug("操作...");
		}
	}
}, "t1");
Thread t2 = new Thread(() -> {
	synchronized (B) {
	log.debug("lock B");
	sleep(0.5);
		synchronized (A) {
		log.debug("lock A");
		log.debug("操作...");
		}
	}
}, "t2");
t1.start();
t2.start();

定位死锁可以使用jconsole实现死锁定位

活锁:

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

public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
    new Thread(() -> {
    // 期望减到 0 退出循环
        while (count > 0) {
            sleep(0.2);
            count--;
            log.debug("count: {}", count);
            }
        }, "t1").start();
    new Thread(() -> {
    // 期望超过 20 退出循环
        while (count < 20) {
            sleep(0.2);
            count++;
            log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

饥饿 :

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

3.8 ReentrantLock 重入锁

支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择

相对于 synchronized 它具备如下特点:可中断、可以设置超时时间、可以设置为公平锁、支持多个条件变量

基本语法

// 获取锁
reentrantLock.lock();
try {
	// 临界区
} finally {
	// 释放锁
	reentrantLock.unlock();
}

可重入

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

可打断

lock.lockInterruptibly(); //该语句实现加锁,并且可打断 , 如果当前线程未被中断,则获取锁定。

锁超时

lock.tryLock() //尝试获得锁,仅在调用时锁定被另一个线程保持的情况下,才获取该锁定。

lock.tryLock(long timeout, TimeUnit unit) 如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

公平锁

ReentrantLock 默认是不公平

ReentrantLock lock = new ReentrantLock(true); //设置锁为公平的

公平锁一般没有必要,会降低并发度。

条件变量 Condition

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

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

Condition

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的 。

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await() //造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
condition.signal() //唤醒一个等待线程。
condition.signalall() //唤醒所有等待线程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值