第一章Single Threaded Execution模式 能通过这座桥的只有一个人

【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方法。

猜你喜欢

转载自blog.csdn.net/u012530451/article/details/82227557