Single Threaded Execution模式

本文介绍了Single Threaded Execution模式,它是多线程的基础。通过代码示例展示了如何在共享资源上实现单线程访问,分析了不加同步关键字可能导致的问题,并解释了synchronized的作用,强调了原子性和临界区的概念。最后提到了计数信号量Semaphore类在限制并发访问资源上的应用。
摘要由CSDN通过智能技术生成

        共享资源在同一时间内仅允许一个线程访问的模式即是Single Threaded Execution,它也是多线程模式的基础,可以看作我们理解多线程的前导课。首先我们通过代码来对Single Threaded Execution产生一个直观的认识,然后再逐步分析其中的知识点:  

        场景:共享资源对象门(Gate)类、客户端线程(UserThread)类、主线程。Gate表示人通过的门,counter代表人数,name代表通过的人的姓名、address代表此人的出生地。假设每个人的姓名的首字母和出生地的首字母是相同的。check方法用于检测当前门的状态是否异常(姓名首字母不等于出生地首字母)。UserThread消费Gate,调用同一Gate对象的pass方法。

        Gate类代码如下:

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;
        this.check();
    }

    private void check(){
        if(this.name.charAt(0) != this.address.charAt(0)){
            System.out.println("***** BROKEN *****" + toString());
        }
    }

    public String toString(){
        return "No." + this.counter + ": " + this.name + ", " + this.address
    }
}

UserThread类:

public class UserThread extends Thread{
    private final Gate gate;
    private final String myName;
    private final String myAddress;

    public UserThread(Gate gate, String name, String address){
        this.gate = gate;
        this.myName = name;
        this.myAddress = address;
    }

    @overrid
    public void run() {
        System.out.println(this.myName + "Begin");
        while(true){
            this.gate.pass(myName, myAddress);
        }
    }
}

(Tips:由于初始化之后UserThread中的变量不会再被赋值,因此将变量设置为final;同时不在字段声明时,而在构造函数中赋初值在java中叫做blank final)

        Main类代码如下:

public class Main{
    public static void main(String[] args){
        Gate gate = new Gate();
        new UserThread(gate, "Alice", "Alaska").start();
        new UserThread(gate, "Bob", "Brazil").start();
        new UserThread(gate, "Cindy", "Canada").start();
    }
}

        我们需要注意的是,注入到三个UserThread的gate对象需要是同一个,否则不会存在多个线程访问相同资源的情况。

        控制台输出如下:

        

        因此Gate是非线程安全的类,所谓非线程安全,即是指多个线程访问相同对象时,可能存在状态不一致的情况,从而产生意外的输出。

        考虑两个线程交替执行paas方法的情况,可能的情形如下所示:

        还有一种可能的情况: 

         

        在上述两种情况下,两个线程会竞争向Gate对象中写入值,两个线程均不知道对方已经修改了变量。

        因此需要避免多个线程同时访问临界区(将仅允许单线程访问的程序范围称为临界区)的变量造成的不一致性。只需要在非线程安全的方法上加上synchronized关键字。如下所示:

public class Gate{
    private int count = 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();
    }

    public synchronized String toString(){
        //代码同上
    }
    
    private void check(){
        //代码同上
    }
}

        进程在访问同步方法前,需要获得对象锁。需要注意的是,锁是在对象层级的。一旦某个进程获得对象锁,访问该对象的临界区,其余线程访问该对象的所有同步(synchronized)方法都会被阻塞。但不同进程可以同时访问不同对象的同步方法,这是因为每个实例都拥有一个独立的锁。这在上一章已经阐释过了。

        同时,我们没有在check方法上加synchronized,这是因为check方法由private修饰,同时调用地方仅有pass方法。因此对pass加synchronized就相当于为check加上了同步锁。

        在加上同步关键字之后,两个线程交替运行的场景如下:

        另一种可能的情况:

         因此,线程在访问临界区时,需要先获得对象锁,其他线程访问相同对象临界区时,需要等到对象锁释放。这就叫做Single Threaded Execution模式,临界区的资源某一时段仅允许一个线程进行访问。

        在介绍完Single Threaded Execution,我们来思考一下什么时候使用该模式。首先是需要多线程,其次存在共享的资源对象,同时这些线程之间会相互影响共享资源对象的状态。

        共享资源对象中包含safeMethod和unsafeMethod。其中safeMethod为线程安全的类,多个线程同时调用也不会发生问题;而unsafeMethod被多个线程同时调用时,实例状态可能发生不一致性,因此需要使用Synchronized修饰符加以保护。如下图所示:

         上面的线程获得对象锁访问同步方法,下面的线程在访问同步方法时就被阻塞在外,这种状况就叫做线程冲突。

        好啦,SIngle Threaded Execution的基本概念就介绍到这里。接下来我们思考更加深入的几个问题:

        (1)原子性:被synchronized方法修饰的操作具有原子性,也即不可分割性。例如上例中pass方法的counter++;name和address的赋值以及check方法的调用,当一个线程完成这些所有操作后,才会释放对象锁。原子性有些类似于数据库的事物;

        (2)该以什么单位来保护:如果我们的代码如下:

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

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

        可以看到,name和address被分别赋值。当一个线程进入同步方法setName后,为name变量赋值完毕后,退出该方法并释放对象锁另一个线程可进入同步方法setAddress,为address赋值。而我们的初衷是为了让name和address保持一致。显然上述代码无法确保一致性。

        最后再介绍一下计数信号量和Semaphore类

        Single Threaded Execution模式设置的synchronized方法确保了临界区同一时间内只能被一个线程访问。如果我们希望设置同一时间某个区域能被N个线程同时访问呢?这就可以借助于计数信号量和Semaphore类来实现。

        话不多说上代码:

import java.util.concurrent.Semaphore;

class BoundedResource{
    private final Semaphore semaphore;
    private final int permits;//允许访问资源的线程数
    private final static Random random = new Random(3678654);//构造随机数
    
    public BoundedResource(int permits) {
        this.semaphore = new Semaphore(permits);
        this.permits = permits;
    }
    
    //使用资源
    public void use() throws InterruptedException {
        //acquire方法用于确认存在可用资源,当存在可用资源时,立即从acquire方法返回,同时信号量内部可用资源数量减1
        //如无可用资源,线程阻塞在acquire方法
        semaphore.acquire();
        try{
            this.doUse();
        }finally{
            //释放资源,信号量内部可用资源数量加1
            semaphore.release();
        }
    }

    //实际使用资源
    private void doUse() throws InterruptedException {
        System.out.println("BEGIN: used = " + (permits - semaphore.avaliablePermits()));
        Thread.sleep(random.nextInt(500));
        System.out.println("ENDL used = " + (permits - semaphore.avaliablePermits()));
    }
}

class UserThread extends Thread{
    private final Random random = new Random(256438);
    private final BoundedResource boundedResource;

    public UserThread(BoundedResource boundedResource){
        this.boundedResource = boundedResource
    }

    @override
    public void run(){
        try{
            while(true){
                resource.use();
                Thread.sleep(random.nextInt(3000));
            }
        }catch(InterruptedException){
            
        }
    }
}

public class Main{
    BoundedResource boundedResource = new BoundedResource(3);
    for(int I=0;i<10;i++){
        new UserThread(boundedResource).start();
    }
}

        上面的代码新建了10个线程循环使用资源,当进入use方法的线程数量超过设定的阈值时,多余线程会阻塞在acquire方法,知道资源被释放。运行实例结果如下:

         最后,需要注意的是,程序中大量使用的ArrayList为非线程安全的类。java.util.Hashtable和java.util.ConcurrentHashMap都是线程安全的类,前者使用single Threaded execution模式,后者将存储空间划分为多段,互不干扰

        Java提供了很多方法确保集合为线程安全的类:

         Single Threaded Execution模式就介绍到这里,下一章我们将介绍Immutable模式,欢迎大家继续阅读。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值