《图解Java多线程设计模式》学习笔记(三)Single Threaded Execution模式

首页 > 《图解Java多线程设计模式》学习笔记(三)Single Threaded Execution模式 > 列表

《图解Java多线程设计模式》学习笔记(三)Single Threaded Execution模式

一、Single Threaded Execution
  • 以一个线程运行
  • 也成为临界区,临界域
二、不使用Single Threaded Execution的程序
1. 场景
  • 一个门只允许一个人通过
  • 三个人频繁通过这个门
  • 人通过们后,统计人数递增
  • 程序会记录人信息
2. 代码
// 表示人通过的门
public class Gate {
    // 记录已通过门的人数
    private int counter = 0;
    // 最后一个通过人的姓名
    private String name = "Nobody";
    // 最后一个通过人的出生地
    private String address = "Nowhere";

    // 通过门
    public void pass(String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

    // 当前门的状态
    @Override
    public String toString() {
        return "Gate{" +
                "counter=" + counter +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    // 人姓名与出生地首字母不符说明记录异常
    private void check() {
        if (name.charAt(0) != address.charAt(0)) {
            System.out.println("***** BROKEN *****" + toString());
        }
    }
}
public class UserThread extends Thread {
    // 要通过的门
    private final Gate gate;
    // 姓名
    private final String myName;
    // 出生地
    private final String myAddress;

    // 不在字段声明时赋值,在构造函数中初始化字段的形式叫空白final
    public UserThread(Gate gate, String myName, String myAddress) {
        this.gate = gate;
        this.myName = myName;
        this.myAddress = myAddress;
    }

    // 表示这个人不断通过门
    @Override
    public void run() {
        System.out.println(myName + " BEGIN");
        while (true) {
            gate.pass(myName, myAddress);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        System.out.println("Testing Gate,hit CTRL+C to exit.");
        Gate gate = new Gate();
        new UserThread(gate,"Alice","Alaska").start();
        new UserThread(gate,"Bobby","Brazil").start();
        new UserThread(gate,"Chris","Canada").start();
    }
}
Testing Gate,hit CTRL+C to exit.
Alice BEGIN
Chris BEGIN
Bobby BEGIN
***** BROKEN *****Gate{counter=212316276, name='Alice', address='Brazil'}
***** BROKEN *****Gate{counter=212318781, name='Bobby', address='Canada'}
***** BROKEN *****Gate{counter=212321373, name='Bobby', address='Alaska'}
***** BROKEN *****Gate{counter=212322213, name='Bobby', address='Alaska'}
***** BROKEN *****Gate{counter=212322955, name='Bobby', address='Alaska'}
3. 运行结果

程序出错了,说明Gate类是不安全的。

4. 分析原因

pass方法被多个线程执行

this.counter++;
this.name = name;
this.address = address;
check();

这四条语句可能是交错执行的

三、使用Single Threaded Execution的程序
package SingleThreadExecution;

/**
 * @author yzy
 * @date 2021/2/23 11:17
 */

// 表示人通过的门
public class Gate {
    // 记录已通过门的人数
    private int counter = 0;
    // 最后一个通过人的姓名
    private String name = "Nobody";
    // 最后一个通过人的出生地
    private String address = "Nowhere";

    // 通过门
    public synchronized void pass(String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

    // 当前门的状态
    @Override
    public synchronized String toString() {
        return "Gate{" +
                "counter=" + counter +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    // 人姓名与出生地首字母不符说明记录异常
    private void check() {
        if (name.charAt(0) != address.charAt(0)) {
            System.out.println("***** BROKEN *****" + toString());
        }else{
            System.out.println(this.toString());
        }
    }
}

对pass和toString加上synchronized,这样输出结果正常了。

Testing Gate,hit CTRL+C to exit.
Chris BEGIN
Alice BEGIN
Bobby BEGIN
....
Gate{counter=744937, name='Chris', address='Canada'}
Gate{counter=744938, name='Chris', address='Canada'}
Gate{counter=744939, name='Chris', address='Canada'}
Gate{counter=744940, name='Chris', address='Canada'}
Gate{counter=744941, name='Chris', address='Canada'}
Gate{counter=744942, name='Chris', address='Canada'}
Gate{counter=744943, name='Chris', address='Canada'}
Gate{counter=744944, name='Chris', address='Canada'}
Gate{counter=744945, name='Chris', 
四、Single Threaded Execution中的角色

共享资源(SharedResource):

  • 上述例子中由Gate类扮演SharedResource角色
  • 共享资源是可以被多个线程访问的类
  • 共享资源主要包括两个方法:
    • safeMethod:多个线程同时调用也不会发生问题的方法
    • unsafeMethod:多个线程同时调用会发生问题,必须加以保护的方法
五、扩展思路
1. 使用场景
  • 多线程时
  • 多个线程访问时
  • 状态有可能发生变化时
  • 需要确保安全性时
2. 生存性与死锁
  • Single Threaded Execution存在发生死锁的危险
  • 死锁:两个线程分别持有锁,并相互等待对方释放锁。
  • Single Threaded Execution满足以下条件会发生死锁:
    • 存在多个共享角色
    • 线程持有某个共享角色的锁时,还想获取其他共享角色的锁
    • 获取共享角色锁的顺序是不固定的
  • 例子:共享角色相当于勺子和叉子。一人拿勺子,一人拿叉子,还都想获取对方的叉子或勺子。而且拿叉子和勺子两个操作不分先后顺序。
  • 解决:只要打破以上三个条件中的一个,即可防止死锁的发生。
3. 可用性和继承反常

多线程程序,继承会引起一些麻烦问题,成为继承反常

4. 临界区的大小和性能

一般情况下,Single Threaded Execution模式会降低程序性能,原因如下:

  • 获取锁花费时间:进入synchronized方法时线程要获取锁,这个处理花费时间。
    减少共享角色数量可减少获取锁的数量,从而减少性能下降。
  • 线程冲突引起等待:当线程执行临界区内的处理,其他想要进入临界区的线程会阻塞,该现象称为线程冲突。
    缩小临界区范围可降低冲突概率。
六、延伸
1. synchronized语法与Befor/After模式
synchronize void method(){
    
}
  • synchronized可看做在"{"处获取锁,在 "}"处释放锁
void method(){
    lock();
    
    
    unlock();
}
  • 假设有获取锁的方法lock,和释放锁的unlock,那上面代码是否跟synchronized功能一样。
    其实不一样,如果lock和unlock之间有return或者异常处理,则会导致锁无法被释放,除非把unlock放在finally中。
2. synchronized在保护什么

如Gate中,将pass声明为synchronized,则synchronized就保护着counter,name,address这三个字段。

3. 该以什么单位来保护

若在Gate中加入以下代码:

  public synchronized void setName(String name) {
        this.name = name;
    }

    public synchronized void setAddress(String address) {
        this.address = address;
    }

Gate类就不安全了,将pass声明为synchronized的目的是防止多个线程交错执行赋值操作,而加入两个set方法,则破坏了这个目的。在保护Gate时,要将字段合在一起保护。

4. 原子操作
  • 不可分割的操作通常成为原子操作。
  • 例如pass声明了synchronized,pass方法就是原子操作。
5. long与double的操作不是原子的
  • 假设某线程执行n = 123;
    另一线程执行n = 456;
    则最后n不是123就是456,因为基本类型赋值和引用是原子的,不用担心值会混杂在一起。
  • 但long和double不是。n = 123L; n = 456L;
    最后n可能是123L,456L,0L甚至31415926L。
  • 所以对这种类型执行操作,在synchronized方法中进行。
  • 或者在字段上声明volatile,对该字段的操作就变成原子的了。
七、总结
  • 修改多个线程共享的实例时,实例会失去安全性
  • 所以要找出实例中状态不稳定的范围,设置成临界区
  • 用synchronized定义临界区,一次只允许一个线程执行