软件构造 7-3 Locks and Synchronization

7.3 锁与同步

一. 同步

  程序员来负责多线程之间对 mutable 数据的共享操作,通过“同步”策略,避免多线程同时访问数据。

  锁的实质是把一系列操作组合成一个原子操作。使用锁机制,获得对数据的独家 mutation 权,其他线程被阻塞,不得访问。
在这里插入图片描述

二. 锁

  LockJava 语言提供的内嵌机制。每个 object 都有相关联的 lock 。使用以下方式加锁:

synchronized (lock) { // thread blocks here until lock is free
	// now this thread has the lock
	balance = balance + 1;
	// exiting the block releases the lock
}

  拥有 lock 的线程可独占式的执行该部分代码,这叫互斥


  Lock 保护共享数据,注意:要互斥,必须使用同一个 lock 进行保护。


1. Monitor 设计模式

  ADT 所有方法都是互斥访问:

/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
	private String text;
	...
	public SimpleBuffer() {
		synchronized (this) {
			text = "";
			checkRep();
		}
	}
	public void insert(int pos, String ins) {
		synchronized (this) {
			text = text.substring(0, pos) + ins + text.substring(pos);
			checkRep();
		}
	}
	public void delete(int pos, String ins) {
		synchronized (this) {
			text = text.substring(0, pos) + text.substring(pos+len);
			checkRep();
		}
	}
	public int length() {
		synchronized (this) {
			return text.length();
		}
	}
	public String toString() {
		synchronized (this) {
			return text;
		}
	}
}

  用 ADT 自己做 lock,所有对 ADTrep 的访问都加锁。

  但该模式效率低,只能顺序不能并行执行操作。

  其实也可以通过:

/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
	private String text;
	...
	public SimpleBuffer() {
		text = "";
		checkRep();
	}
	public synchronized void insert(int pos, String ins) {
		text = text.substring(0, pos) + ins + text.substring(pos);
		checkRep();
	}
	public synchronized void delete(int pos, String ins) {
		text = text.substring(0, pos) + text.substring(pos+len);
		checkRep();
	}
	public synchronized int length() {
		return text.length();
	}
	public synchronized String toString() {
		return text;
	}
}

  Java 默认构造方法互斥,故上述程序的构造函数不添加 synchronized 关键字。

  改变方式如下,但仍然得保证这样的两个方法不会出现 interleaving 的方式。

public class MsLunch {
	private long c1 = 0
	private long c2 = 0
	private Object lock1 = new Object();
	private Object lock2 = new Object();
	public void inc1() {
		synchronized(lock1) {
			c1++;
		}
	}
	public void inc2() {
		synchronized(lock2) {
			c2++;
		}
	}
}

  原来的方式与改变后的方式相比:

  • 后者需要显式的给出 lock ,且不一定非要是 this
  • 后者可提供更细粒度并发控制

  然而,同步机制给性能带来极大影响除非必要,否则不要用Java 中很多 mutable 的类型都不是 threadsafe 就是这个原因。

  故而加锁需尽可能减小 lock 的范围,并且避免在方法 spec 中加 synchronized ,而是在方法代码内部更加精细的区分哪些代码行可能有 threadsafe 风险,为其加锁。

  原则上,要先去思考清楚到底 lock 谁,然后再 synchronized(…)

public static synchronized boolean findReplace(EditBuffer buf ,...)

  注意,上一方法 (存在 static )使用时是对类上锁,静态操作与类相关,范围很大,对性能带来极大损耗。


  Synchronized 不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑 lock
  所有关于 threadsafe 的设计决策也都要在 ADT 中记录下来。


  对同一个 mutable 对象的操作,必须要在各线程里用 synchronized 全部保护起来。


  加锁原则:

  • 任何共享mutable 变量/对象必须被 lock 所保护
  • 涉及到多个 mutable 变量的时候,它们必须被同一个 lock 所保护

  例如在 monitor pattern 中, ADT 所有方法都被同一个 synchronized(this) 所保护

class Poem {
	private String title;
	private List<String> lines = Collections.synchronizedList(...);
	public synchronized void operation(String filter) {
		Iterator<String> iter = lines.iterator();
		while (iter.hasNext()) {
			String line = iter.next();
			if (line.contains(filter))
				iter.remove();
		}
		title = title.toUpperCase();
	}
}

  happens-before 机制使用寄存器时检测寄存器值是否改变,改变了直接将改变后的值放入其中。可以防止但无法完全避免 race condition

三. 死锁

  死锁:多个线程竞争 lock ,相互等待对方释放 lock

T1:synchronized(a){ synchronized(b){}  }
T2:synchronized(b){ synchronized(a){}  }

在这里插入图片描述
  多个线程使用多个锁并且顺序不同,容易出现死锁。

/** An EditBuffer represents a threadsafe mutable
 *  string of characters in a text editor. */
public interface EditBuffer {
	/** 
	 * Modifies this by inserting a string.
	 * @param pos position to insert at
	 * 				  (requires 0 <= pos <= current buffer length)
	 * @param ins string to insert
	 */
	public void insert(int pos, String ins);
	
	/**
	 * Modifies this by deleting a substring
	 * @param pos starting position of substring to delete
	 * 				  (requires 0 <= pos <= current buffer length)
	 * @param len length of substring to delete
	 * 				  (requires 0 <= len <= current buffer length - pos)
	 */
	public void delete(int pos, int len);

	/**
	 * @return length of text sequence in this edit buffer
	 */ 
	public int length();

	/**
	 * @return content of this edit buffer
	 */
	public String toString();
}

  该程序在执行以下两个线程会出现死锁:

Wizard harry = new Wizard("Harry Potter");
Wizard snape = new Wizard("Severus Snape");
//thread A                   //thread B
harry.friend(snape);         snape.friend(harry);
harry.defriend(snape);       snape.defriend(harry);

  解决死锁的方式:

  • 锁排序。但需要排序,效率较低;需要预先知道使用哪些锁
public void friend(Wizard that) {
	Wizard first, second;
	if (this.name.compareTo(that.name) < 0) {
		first = this; second = that;
	} else {
		first = that; second = this;
	}
	synchronized (first) {
		synchronized (second) {
			if (friends.add(that)) {
				that.firend(this);
			}
		}
	}
}
  • 粗粒度锁(将多个锁变为一个锁)。
public class Wizard {
	private final Castle castle;
	private final String name;
	private final Set<Wizard> friends;
	...
	public void friens(Wizard that) {
		synchronized (castle) {
			if (this.friends.add(that)) {
				that.friend(this);
			}
		}
	}
}

四. wait(), notify(), and notifyAll()

1. 保护块

  某些情况下,某些条件未得到满足,所以一直在空循环检测,直到条件被满足。这是极大浪费。如:

public void guardedJoy() {
	// Simple loop guard. Wastes
	// processor time. Don't do this!
	while(!joy) {} // 其他线程中更改 joy 之后,再往下执行
	System.out.println("Joy has been achieved");
}

2. 解决方案

  • o.wait() 释放锁 o,阻塞当前线程,将其添加入o 的阻塞队列中
  • o.notify() 通知被阻塞的线程恢复运行,o 的阻塞队列中随机一个被唤醒
  • o.notifyAll() 通知被阻塞的线程恢复运行,o 的阻塞队列中所有线程被唤醒

  这三个操作要求使用在synchronized(obj) 同步块中,并且 wait 前为 obj

synchronized (obj)
	while (<condition does not hold>)
		obj.wait();
	
	... // Perform action appropriate to condition
}

  若不在方法前添加对象,直接使用如 wait() 实际上等价于使用 this.wait() ,锁就是当前对象,调用方法也应该是使用当前对象加锁。

public synchronized void guardedJoy() {
	// This guard only loops once for each special event,
	// which may not be the event we're waiting for
	while(!joy) {
		try {
			wait();
		} catch (InterruptedException e) {}
	}
	System.out.println("Joy and efficiency have been achieved");
}
2.1 Object.wait()

  该操作使 object 所处的当前线程进入阻塞 等待状态,直到其他线程调用该对象的 notify() 操作。

2.2 Object.notify() / notifyAll()

  Object.notify() 随机选择一个在该对象上调用 wait 方法的线程,解除其阻塞状态。

  notifyAll() 所有在该对象上调用 wait 方法的线程,解除其阻塞状态。

public synchronized notifyJoy() {
	joy = true;
	notifyAll();
}
2.3 线程的执行情况

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值