图解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定义临界区,一次只允许一个线程执行
标签:设计模式,Java,name,counter,线程,address,Gate,多线程,String 来源: https://www.cnblogs.com/zionyang/p/14436557.html