共享资源在同一时间内仅允许一个线程访问的模式即是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模式,欢迎大家继续阅读。