深入理解Java并发编程之共享模型管程

本章内容:

共享问题、synchronized、线程安全分析Monitor、wait/notify、线程状态转换、活跃性、Lock

4.1共享带来的问题

Java的体现

问题分析:

以上的结果可能是正数、负数、零。为什么呢?因为Java中对静态变量的自增,自减并不是原子操作。

对于i++指令而言(i为静态变量),实际会产生如下的JVM字节码指令

临界区Critical Section

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

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

  • 多个线程读共享资源其实也没有问题

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

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

竟态条件

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

4.2 synchronized解决方案

应用之互斥

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

  • 阻塞式的解决方案:synchronized

  • Lock非阻塞式的解决方案∶原子变量

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

synchronized

面向对象改进

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

4.3方法上的synchronized

4.4 synchronized原理进阶

轻量级锁

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

轻量级锁对使用者是透明的,即语法仍然是synchronized假设有两个方法同步块,利用同一个对象加锁

4.5变量的线程安全分析

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

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

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

  • 如果只有读操作,则线程安全

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

局部变量是否线程安全?

  • 局部变量是线程安全的

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

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

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

局部变量线程安全分析

案例分析见并发编程4.4

常见线程安全类

  • String

  • Lnteger

  • String

  • Buffer

  • Random

  • Vector

  • Hashtable

  • java.util.concurrent包下的类

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

  • 他们的每个方法是原子的

  • 但主要他们多个方法的组合不是原子的

线程安全类方法的组合

分析以下代码是否线程安全?

不可变类线程安全性

String、Integer等都是不可变类,因为其内部的状态不可以被改变,因此他们的方法都是线程安全的。

实例分析

例1:线程不安全 其中HashMap方法与Date方法均属于线程不安全

例二:线程不安全,count为成员变量,有可能被其他线程访问更改

例3:线程不安全,start有可能被其他线程修改

例4

例4线程安全

例5线程不安全,conn成员变量有可能被其他线程修改

例6线程安全

例7:foo为抽象类,其子类行为不明确,有可能子类改变基类的成员变量

4.6 Monitor概念

Java对象头

32位虚拟机

4.7 wait notify

API介绍

  • obj.wait()让进入object 监视器的线程到waitSet等待

  • obj.notify()在object 上正在waitSet等待的线程中挑一个唤醒

  • obj.notifyAl1()让object 上正在waitSet 等待的线程全部唤醒

wait()方法会释放对象的锁,进入WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify为止

wait(long n)有时限的等待,到n毫秒后结束等待,或是被notify。

4.8wait notify的正确姿势

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

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

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

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

4)它们状态TIMED_WAITING

案例看《并发编程》p73

4.9 park&Unpark

基本使用:他们是LockSupport类中的方法

特点

与Object的 wait & notify相比

  • wait , notify和notifyAll必须配合Object Monitor一起使用,而 park , unpark 不必

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

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

案例看《并发编程》p74

4.10重新理解线程状态转换

假如有线程Thread t

情况1 NEW -->RUNNABLE

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

情况2 RUNNABLE <-->WAITING

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

  • 调用obj. wait()方法时,t线程从RUNNABLE 一> WAITING

  • 调用obj . notify() , obj.notifyAl1(, 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

4.11多把锁

举例:

一间大屋子有两个功能︰睡觉、学习,互不相干。

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

将锁的粒度细分

  • 好处,是可以增强并发度

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

4.12活跃性

死锁

一个线程需要同时获取多把锁,这时就容易发生死锁

例:t1线程获得A对象锁,接下来想获取B对象的锁t2线程获得B对象锁,接下来想获取[A对象的锁。

定位死锁

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

  • 避免死锁要注意加锁顺序

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

哲学家就餐问题见《并发编程》P82

活锁

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

饥饿

下图是一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

顺序加锁的解决方案

4.13 ReentrantLock

相对于synchronized它具备如下特点

  • 中断

  • 可以设置超时时间

  • 可以设置为公平锁

  • 支持多个条件变量

与synchronized一样,都支持可重入

基本语法

可重入

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

示例

可打断、锁超时、公平锁,分别举例见并发编程P90

条件变量

synchronized 中也有条件变量,即waitSet休息室,当条件不满足时进入waitSet等待。ReentrantLock的条件变量比 synchronized强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized是那些不满足条件的线程都在一间休息室等消息

  • 而ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await前需要获得锁

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

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

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

4.14本章总结

  • 分析多线程访问共享资源时,哪些代码片段属于临界区

  • 使用synchronized互斥解决临界区的线程安全问题。

  • 掌握synchronized 锁对象语法

  • 掌握synchronzied加载成员方法和静态方法语法

  • 掌握wait/notify同步方法

  • 使用lock 互斥解决临界区的线程安全问题

  • 掌握lock 的使用细节:可打断、锁超时、公平锁、条件变量

  • 学会分析变量的线程安全性、掌握常见线程安全类的使用

  • 了解线程活跃性问题:死锁、活锁、饥饿

  • 应用方面

  • 互斥∶使用synchronized或Lock达到共享资源互斥效果

  • 同步∶使用wait/notify 或 Lock的条件变量来达到线程间通信效果

  • 原理方面

  • monitor、synchronized 、wait/notify 原理

  • synchronized进阶原理

  • park & unpark原理·

  • 模式方面

  • 同步模式之保护性暂停

  • 异步模式之生产者消费者

  • 同步模式之顺序控制

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值