「BUAA OO Unit 2」 多线程分享

Posted by saltyfishyjk on 2022-04-16
Words 1.6k and Reading Time 5 Minutes
Viewed Times

「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. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

    1
    2
    3
    synchronized void method() {
    // CODE
    }
  2. 修饰静态方法:给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的锁

    class信息存储在method area中。

    1
    2
    3
    synchronized void staic method() {
    // TODO
    }
  3. 修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

    1
    2
    3
    synchronized(this) {
    // TODO
    }

上面的第一种介绍中主要侧重了从代码模板上的三种形式,对于谁获得了锁,获得了谁的锁可能表述的不是非常清晰,以下从对象锁类锁进行第二个角度的归类。

对象锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)。

方法锁形式

synchronized修饰普通实例方法,锁对象默认为this

1
2
3
public synchronized void method() {
// CODE
}
代码块形式

手动指定锁定对象,可以是this,也可以是其他对象

1
2
3
synchronized (obj) { // obj可以为this或其他对象
// CODE
}

类锁

synchronized修饰静态的方法或指定锁对象为Class对象。这种情况下,所有对象共用一把锁。

静态方法形式
1
2
3
public static synchronized void method() {
// TODO
}
代码块形式
1
2
3
synchronized (classA.class) {
// TODO
}

调试可用技巧

  1. jprofiler
  2. setName()设置线程名
  3. 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结束,这个对象才可以被继续访问。更一般化来讲,对于共享资源的读写,我们使其互斥,保证了数据的唯一性和安全性。

再以官方输出包为例,官方输出包类似打印机,我们希望同一时刻只有一台打印机在进行一个打印操作,因此,我们将官方输出包的打印操作封装为同步方法,保证了其安全,避免了时间戳错位的可能。

与同步一起的重要操作还有waitsleepnotifyAll()notify())。以生产者-消费者模型举例,当我们的消费者需要托盘中的产品时,如果没有,那么需要其进入waitsleep进行等待,而非时刻占用CPU资源进行轮询。这时,如果我们的托盘中来了新商品,则需要其对消费者notifyAll()唤醒。更一般化来讲,我们将信使操作交给共享对象负责,由共享对象根据自己状态的变化来唤醒相关线程,节约了CPU资源。

多线程的本意是想尽可能地提高效率,而加锁是一种降低效率确保安全的行为,因此,我们希望尽可能的划分清楚共享对象/类和其中需要加锁的资源,尽可能减少加锁对线程效率的伤害,即,对于不涉及共享资源的操作,我们不设置其为同步的,以提高效率。

Part 3 在多线程进行架构设计及其具体在第一次作业中的体现

多线程问题丰富多样,我们以生产者-消费者模型为例窥得多线程下架构设计的一角。

以第一次作业为例,我们的生产者为输入线程InputThread,托盘(共享对象)为候乘表WaitTable,消费者为电梯Elevator。为了简化描述,我们在这里略去了调度器。

InputThread获得标准输入,调用WaitTableput方法置入新的passenger;当输入为空时,调用WaitTablesetEnd方法作为标志信号,提示输入结束,条件允许时可以结束。值得注意的是,我们在这里并没有涉及电梯如何操作,输入线程作为生产者不关心消费者的行为。

Elevator自行运行,执行开关门和上下行的操作。在电梯停放在某层时,根据自身是否为空检索本层或所有楼层的WaitTable中的passenger,对于满足条件的passenger,调用WaitTableget方法,将其从WaitTable转移到电梯内。这里的接客策略由电梯或额外的调度器提供支持,需要WaitTable提供的只有get的服务。当WaitTable为空且没有结束时,电梯挂起,等待WaitTable中获取新passenger时唤醒。当WaitTablel为空且输入结束,电梯内部送客结束,结束线程。

WaitTable作为共享对象,对外提供getput方法,并且根据设计的完善和一致性,在这样的同步方法内都提供notifyAll,唤醒相关线程。作为信使,其还提供setEnd()isEnd()方法,让输入结束的讯号通过这个共享对象传递。


This is copyright.