【Single Threaded Execution模式】
以一个线程执行,就像独木桥同一时间内只允许一个人通行一样,该模式用于设置限制。以确保同一时间内只能让一个线程执行处理。
Single Threaded Execution模式有时候也成为临界区或临界域。
非线程安全的类
public class Gate {
private int counter=0;
private String name="Nobody";
private String address="Nowhere";
public void pass(String name,String address){
this.counter+=1;
this.name=name;
this.address=address;
check();
}
public String toString(){
return "No."+counter+": "+name+", "+address;
}
private void check(){
if(name.charAt(0)!=address.charAt(0)){
System.out.println("*****BROKEN***** "+toString());
}
}
}
提示:*****BROKEN***** No.5076704: Chris, Alaska
出错原因:在某个线程执行check方法时,其他线程不断执行pass方法,改写了name字段和address字段。线程改写共享的实例字段时并未考虑其他线程的操作。
线程安全的类
将pass方法和toString方法修改为synchronized方法。
之所以显示BROKEN,是因为pass方法中的代码被多个线程交错运行。
而synchronized方法能够确保该方法同时只由一个线程执行。
【Single Threaded Execution】模式中登场角色
SharedResource共享资源
SharedResource角色是可被多个线程访问的类,包含很多方法,但这些方法主要分为如下两类。
1.safeMethod:多个线程同时调用也不会发生问题的方法。
2.unsafeMethod:多个线程同时调用会发生问题,因此必须加以保护的方法。
而unsafeMethod(不安全的方法)在被多个线程同时执行,实例状态有可能会发生分歧。这是就需要保护该方法,使其不被多个线程同时访问。
Single Threaded Execution模式会保护unsafeMethod,使其同时只能由一个线程访问。Java则是通过将unsafeMethod声明为synchronized方法来进行保护。
我们将只允许单个线程执行的程序范围称为临界区。
【临界区的大小和性能】
Single Threaded Execution模式会降低程序性能
理由1:获取锁花费时间
进入synchronized方法时,线程需要获取对象的锁,该处理会花费时间。如果SharedResource角色的数量减少了,那么要获取的锁的数量也会相应地减少,从而就能够抑制性能的下降了。
理由2:线程冲突引起的等待
当线程Alice执行临界区内的处理时,其他想要进入临界区的线程会阻塞。这种情况称为线程冲突。发送冲突时,程序的整体性能会随着线程等待时间的增加而下降。
如果尽可能地缩小临界区的范围,降低线程冲突的概率,那么就能够抑制性能的下降。
【相关的设计模式】
Guarded Suspension模式
在Single Threaded Execution模式中,是否发生线程等待取决于“是否有其他线程正在执行受保护的unsafeMethod”;而在Guarded Suspension模式中,是否发生等待 取决于“对象的状态是否合适”。另外,在构建Guarded Suspension模式时,“检查对象状态 ”部分就使用了Single Threaded Execution模式。
Read-Write Lock模式
在Single Threaded Execution模式中,如果受保护的unsafeMethod正在被一个线程执行,那么想要执行该方法的其他所有线程都必须等待该线程执行结束;而在Read-Write Lock模式中,多个线程可以同时执行read方法。这时要进行等待的只有想要执行write方法的线程。、另外,在构建Read-Write Lock模式时,“检查线程种类和个数”部分就使用了Single Threaded Execution模式。
Immutable模式
在Single Threaded Execution模式中,unsafeMethod必须要加以保护,确保只允许一个线程执行;而Immutable模式中的Immutable角色,其对象的状态不会发生变化。因此,所有方法都无须进行保护。换而言之,Immutable模式中的所有方法都是safeMethod。
Thread-Specific Storage模式
在Single Threaded Execution模式中,会有多个线程访问SharedResource角色。所以,我们需要保护方法,对现场进行交通管制;而Thread-Specific Storage模式会确保每个线程都有其固有的区域,且这块固有区域仅由一个线程访问,所以也就无需保护方法。
实例不同,锁也就不一样。
【原子操作】
synchronized方法只允许一个线程同时执行。如果某个线程正在执行synchronized方法,其他线程就无法进入该方法。也就是说,从多线程的观点来看,这个synchronized方法执行的操作是“不可分割的操作”。这种不可分割的操作通常称为原子操作。
【java.util.concurrent包和计数信号量】
【计数信号量和Semaphore类】
本章介绍的Single Threaded Execution模式用于确保某个区域“只能由一个线程”执行。下面我们将这种模式进一步扩展。以确保某个区域“最多只能由N个线程”执行。这时就要用计数信号量来控制线程数量。
接下来更进一步扩展,假设能够使用的资源个数有N个,而需要这些资源的线程个数又多于N个。这就导致资源竞争,因此需要进行交通管制。这种情况下也需要用到计数信号量。
java.util.concurrent包提供了表示计数信号量的Semaphore类。
资源的许可数将通过Semaphore的构造函数来制定。
Semaphore的acquire方法用于确保存在可用资源。当无可用资源时,线程会立即从acquire方法返回,同时信号量内部的资源个数会减1.如无可用资源,线程则阻塞在acquire方法内,直到出现可用资源。
Semaphore的release方法用于释放资源。释放资源后,信号量内部的资源个数会增加1.另外,如果acquire中存在等待的线程,那么其中一个线程会被唤醒,并从acquire方法返回。
【示例程序】
模拟多个线程使用数量有限的资源。BoundedResource是表示数量有限的资源的类。它会在构造函数中指定资源的个数。
use方法:“使用”1个资源
semaphore.acquire();:用于确认“是否确实存在可用资源”。当所有资源都已被使用时,线程会阻塞在该方法中。
当线程从acquied方法返回时,则一定存在可用资源。线程随后将调用doUse()方法,并在最后执行以下语句,释放所用的资源。
semaphore.release();
由于acquire方法和release方法必须成对调用,所以这么使用finally创建了Before/After模式。
在doUse方法中,permits - semaphore.availablePermits()表示当前正在使用中的资源个数。
class Log {
public static void println(String s) {
System.out.println(Thread.currentThread().getName()+": "+s);
}
}
//资源个数有限
class BoundedResource {
private final Semaphore semaphore;
private final int permits;
private final static Random random = new Random(314159);
//构造函数(permits为资源个数)
public BoundedResource(int permits){
this.semaphore=new Semaphore(permits);
this.permits=permits;
}
//使用资源
public void use() throws InterruptedException {
semaphore.acquire();
try {
doUse();
} finally {
semaphore.release();
}
}
//实际使用资源
protected void doUse() throws InterruptedException {
Log.println("BEGIN: used = " + (permits - semaphore.availablePermits()));
Thread.sleep(random.nextInt(500));
Log.println("END: used = " + (permits - semaphore.availablePermits()));
}
}
//使用资源的线程
class UserThreadd extends Thread {
private final static Random random = new Random(26535);
private final BoundedResource resource;
public UserThreadd(BoundedResource resource) {
this.resource=resource;
}
public void run() {
try {
while(true) {
resource.use();
Thread.sleep(random.nextInt(3000));
}
} catch(InterruptedException e){}
}
}
public class Main2 {
public static void main(String[] args) {
// 设置3个资源
BoundedResource resource = new BoundedResource(3);
// 10个线程使用资源
for(int i=0;i<10;i++){
new UserThreadd(resource).start();
}
}
}
从运行结果可以看出,10个线程交替使用资源,但同时可以使用的资源最多只能是3个。
【练习题1 使错误更容易发生】
在非线程安全的Gate类检查出第一个错误的时候,counter字段的值已经变为了1010560.也就是说,在检查出第一个错误时,pass方法已经执行了100万次以上。请试着修改一下Gate类,使其在counter值很小时就能够检查出错误。
答:延长临界区可以提高检查出错误的可能性。
例如,在pass方法中的“给name赋值”和“给address赋值”之间调用sleep方法。
【练习题2 private字段的使用】
在本章中,Gate类中的字段都声明为了private。
private int counter=0;
private String name="Nobody";
private String address="Nowhere";
为什么要将这些字段声明为private呢?另外如果将这些字段声明为protected或public,会什么样呢?请从类的安全性这个角度来分析一下。
答:之所以将字段声明为pirvate,是为了便于开发人员确认类的安全。
private字段只有在该类内部才可以访问。因此,只要确认该类中声明的方法是否在安全地访问字段,便可以确认字段的安全性,则无需确认该类以外的类。
protected字段可以被该类的子类和同一个包的类访问。因此,确认安全性时,必须对子类和同一个包内的类也进行确认。
public字段则可以被任何类访问。因此,确认安全性时,必须对访问该字段的所有类进行确认。
【练习题3 读到一半的源代码】
如下是Point类的一部分源代码,请根据所读代码判断下面关于Point类的描述,正确打√,错误打×。
public final class Point {
private int x;
private int y;
public Point(int x,int y){
this.x=x;
this.y=y;
}
public synchronized void move(int dx,int dy) {
x+=dx;
y+=dy;
}
}
√1.无法创建Point类的子类。
答:由于Point类声明为了final,所以无法创建子类。
√2.给Point类的x字段赋值语句不可以写在Point类之外的类中。
答:由于x字段声明为了private,所以Point类之外的类不可以对其赋值。
√3.对于Point类的实例,此处move方法只能同时由一个线程执行。
答:由于move方法声明为了synchronized,所以该方法同时只能由一个线程执行。
×4.该Point类即使由多个线程使用也是安全的。
答:如果只适用move方法,那么Point类就是安全的。但Point类中未读的部分可能会有如下所示的分别对字段赋值的方法,这样一来就无法断言该Point类是安全的。
public synchronized void setX(int x){
this.x=x;
}
public synchronized void setY(int y){
this.y=y;
}
这两个方法确实都是synchronized方法,但是如果加上这两个方法,该类就不安全了。
因为x和y必须一起赋值。如果定义setX和setY这样的方法,线程就会分别给字段赋值。在保护类时,这样没有意义。
【练习题4 安全性的确认】
下面的SecurityGate类模拟的是一个机密设施入口的门。进入(enter)时,人数(counter)会递增1;出来(exit)时,人数(counter)会递减1.计数获取(getCounter)方法能获取当前停留在设施内的人数。
这里的各方法并未声明为synchronized。请问这个类在多线程下是否安全?
public class SecurityGate {
private int counter=0;
public void enter(){
counter++;
}
public void exit(){
counter--;
}
public int getCounter(){
return counter;
}
}
答:不安全,为了确保安全,enter、exit、getCounter方法都必须声明为synchronized方法。