「BUAA OO Unit 2」 多线程分享
Part 0 前言
第六周研讨课,笔者依旧准备了讲稿和分享,但是没有选上,在这里和大家分享。
Part 1 如何在Java中进行多线程编程
三种使用线程的方法
继承Thread
需要继承Thread
类,并重写run
方法,调用start()
启动线程。
实现Runnable
需要实现run
方法,通过Thread
调用start()
启动线程。在这里我们更推荐使用实现Runnable
而非继承Thread
,因为这样可以让我们的类继承其他类,而Java单继承的特性意味着继承了Thread
则不能继承其他类。
实现Callable
这个方法在本课程中没有多介绍,此处暂不展开。
对象头、锁和同步
对象实例结构
Java的实例对象储存在Heap中,其结构如上图示。
锁和同步
如上图示,对象头部分储存的信息包含最近持有该对象锁的线程ID。我们分为两个角度介绍锁,以下首先从代码形式介绍。
我们使用的synchronized
包含三种情况,分别是:
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
1
2
3synchronized void method() {
// CODE
}修饰静态方法:给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的锁。
class信息存储在method area中。
1
2
3synchronized void staic method() {
// TODO
}修饰代码块:指定加锁对象,对给定对象/类加锁。
synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁1
2
3synchronized(this) {
// TODO
}
上面的第一种介绍中主要侧重了从代码模板上的三种形式,对于谁获得了锁,获得了谁的锁可能表述的不是非常清晰,以下从对象锁和类锁进行第二个角度的归类。
对象锁
包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)。
方法锁形式
synchronized
修饰普通实例方法,锁对象默认为this
1 | public synchronized void method() { |
代码块形式
手动指定锁定对象,可以是this
,也可以是其他对象
1 | synchronized (obj) { // obj可以为this或其他对象 |
类锁
指synchronized
修饰静态的方法或指定锁对象为Class
对象。这种情况下,所有对象共用一把锁。
静态方法形式
1 | public static synchronized void method() { |
代码块形式
1 | synchronized (classA.class) { |
调试可用技巧
- jprofiler
setName()
设置线程名- IDEA debug : https://www.bilibili.com/video/BV1Hf4y1t77J?from=search&seid=11484063781420021590&spm_id_from=333.337.0.0
Part 2 如何合理使用同步锁
在我们的代码中,主要使用synchronized
进行同步,其代码结构在上一个部分已经介绍过,以下介绍如何合理使用。
以生产者-消费者模型为例,我们的共享数据是托盘,其中生产者向其中put,消费者从其中get,这两个操作是互斥的,分别加锁表示只有put/get结束,这个对象才可以被继续访问。更一般化来讲,对于共享资源的读写,我们使其互斥,保证了数据的唯一性和安全性。
再以官方输出包为例,官方输出包类似打印机,我们希望同一时刻只有一台打印机在进行一个打印操作,因此,我们将官方输出包的打印操作封装为同步方法,保证了其安全,避免了时间戳错位的可能。
与同步一起的重要操作还有wait
、sleep
和notifyAll()
(notify()
)。以生产者-消费者模型举例,当我们的消费者需要托盘中的产品时,如果没有,那么需要其进入wait
或sleep
进行等待,而非时刻占用CPU资源进行轮询。这时,如果我们的托盘中来了新商品,则需要其对消费者notifyAll()
唤醒。更一般化来讲,我们将信使操作交给共享对象负责,由共享对象根据自己状态的变化来唤醒相关线程,节约了CPU资源。
多线程的本意是想尽可能地提高效率,而加锁是一种降低效率确保安全的行为,因此,我们希望尽可能的划分清楚共享对象/类和其中需要加锁的资源,尽可能减少加锁对线程效率的伤害,即,对于不涉及共享资源的操作,我们不设置其为同步的,以提高效率。
Part 3 在多线程进行架构设计及其具体在第一次作业中的体现
多线程问题丰富多样,我们以生产者-消费者模型为例窥得多线程下架构设计的一角。
以第一次作业为例,我们的生产者为输入线程InputThread
,托盘(共享对象)为候乘表WaitTable
,消费者为电梯Elevator
。为了简化描述,我们在这里略去了调度器。
InputThread
获得标准输入,调用WaitTable
的put
方法置入新的passenger;当输入为空时,调用WaitTable
的setEnd
方法作为标志信号,提示输入结束,条件允许时可以结束。值得注意的是,我们在这里并没有涉及电梯如何操作,输入线程作为生产者不关心消费者的行为。
Elevator
自行运行,执行开关门和上下行的操作。在电梯停放在某层时,根据自身是否为空检索本层或所有楼层的WaitTable
中的passenger,对于满足条件的passenger,调用WaitTable
的get
方法,将其从WaitTable
转移到电梯内。这里的接客策略由电梯或额外的调度器提供支持,需要WaitTable
提供的只有get
的服务。当WaitTable
为空且没有结束时,电梯挂起,等待WaitTable
中获取新passenger时唤醒。当WaitTablel
为空且输入结束,电梯内部送客结束,结束线程。
WaitTable
作为共享对象,对外提供get
和put
方法,并且根据设计的完善和一致性,在这样的同步方法内都提供notifyAll
,唤醒相关线程。作为信使,其还提供setEnd()
和isEnd()
方法,让输入结束的讯号通过这个共享对象传递。
This is copyright.