在0和1的世界里,线程们的发展史凝结了血与泪...
线程一族在由无到有、由有到优的进化过程中,遭遇过很多挫折、磨难和困难。
曾几何时,线程一族内部硝烟四起,纷争不断。
为了建设更加富强、民主、文明、和谐的零壹世界,线程拉比们对关于不同肤色不同种族的线程如何和平共处的课题进行了广泛地探讨。
在和平共处这一宏大课题中的一个不很起眼的小问题,就是线程中广为流传的“读写者问题”。
读者,其抽象对象是只需要读取数据的线程;与之对应,写者,就是可以进行读写操作的线程。
众所周知,多个读者一起读数据完全ok,但不允许多个写者一起写数据(不然写的是个球?),且,不允许读者和写者对数据同时进行操作(不然读者会疯掉)。所以和谐的情形是,要么一个或几个读者同时读数据,要么只有一个写者在写数据,要么数据处于不被访问的状态。
如何制定线程们的行为准则(编写线程的内部代码)去让两种线程遵守这样的规则从而使得读写线程们和谐地一起做事(并发执行)呢?
聪明的拉比们想到了线程世界的灯塔--信号量(Semaphore)。
信号量可以标识一种状态,而某个线程能否不违背规则地执行需要看环境的状态(有没有读者读啦,有没有写者写啦,有没有排队到自己啦等等)。因此,信号量理所应当地成为了线程们一起和谐做事的控制灯。
线程的先驱者按照实现思路及难易程度来依次制定了(实现了)三种解决读写者问题的规则(算法):“读者优先”->“公平竞争”->“写者优先”。
咳咳... 谈正事。
1.“读者优先算法”
为了使得读者和写者互不冲突,且写者之间互不冲突,我们定义了一个信号量-“读写锁”(wrt),这足够了;
由于读者可以多个,我们需要记录读者数量,为了记录读者的数量,我们定义了readcount,有了readcount我们需要对readcount进行加减操作,而readcount本身是一个共享数据,为了使得各个读线程在对readcount进行操作时互不冲突,我们定义了一个信号量-“改数锁”(mutex)。
下面我们利用上述信号量去模拟实现读写者问题中的读线程、写线程、共享数据(注意看注释,这里不详细写了):
import java.util.concurrent.Semaphore; //ShareData.java public class SharedData { public Semaphore mutex = new Semaphore(1);//确保更新“读者数”时的互斥(姑且称之为“改数锁”)->必要锁 //initialized to 1, used to ensure mutual exclusion when the variable readcount is updated. public Semaphore wrt = new Semaphore(1);//确保最多只能有一个写者(姑且称之为“读写锁”)->核心锁 //initialized to 1, common to both the reader and writer processes, used as a mutual exclusion semaphore for the writer. public int readcount = 0;//写者数 //initialized to 0, keeps track of how many processes are reading the object. public String fileName = "hello"; //模拟数据库 }
import java.io.BufferedReader; //Reader.java import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; public class Reader implements Runnable { SharedData sharedData; public Reader(SharedData sharedData) { super(); //继承父类的run() this.sharedData = sharedData; } public void read() { //按照行从hello中读取文件,并显示到控制台 try { FileInputStream fis = new FileInputStream(sharedData.fileName); BufferedReader br = new BufferedReader(new InputStreamReader(fis)); String s; try { while ((s = br.readLine()) != null) { System.out.println("Reader " + Thread.currentThread().getName() + ": " + s); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { fis.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void run() { while (true) { try { sharedData.mutex.acquire();//为改“读者数”加锁 sharedData.readcount++; if (sharedData.readcount == 1)//如果此时读者数为1,那么为“不让写者进入”加锁 sharedData.wrt.acquire(); sharedData.mutex.release();//为改“读者数”解锁 // reading is performed read(); //全部读取出来咯 sharedData.mutex.acquire();//为改“读者数”加锁 sharedData.readcount--; if (sharedData.readcount == 0)//如果此时读者数为0,那么为“不让写者进入”解锁 sharedData.wrt.release(); sharedData.mutex.release();//为改“读者数”解锁 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { Thread.sleep(1000); //线程休眠一秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
import java.io.FileNotFoundException; //Writer.java import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.Scanner; public class Writer implements Runnable { private SharedData sharedData; public Writer(SharedData shareData) { super(); this.sharedData = shareData; } public void write() { //向hello中写入控制台输入的一行数据 try { FileOutputStream fos = new FileOutputStream(sharedData.fileName); PrintWriter pw = new PrintWriter(fos); Scanner sc = new Scanner(System.in); for (int i = 0; i < 1; i++) { System.out.print("Writer " + Thread.currentThread().getName() + " input:"); pw.println("Writer " + Thread.currentThread().getName() + sc.next()); pw.flush(); } try { fos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void run() { while (true) { try { sharedData.wrt.acquire();//请求互斥信号量资源(加锁) //writing is performed write(); sharedData.wrt.release();//释放互斥信号量资源(解锁) } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { Thread.sleep(1000); //线程休眠一秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
2.“公平竞争算法”
读者优先算法基本满足了读写者问题中的各项。
我们考虑下面一种情况:当读者占领资源,并且等待者中既有读者又有写者时,第一种算法会出现什么情况呢?
答案自然是,由于读者已经通过更改信号量wrt使得写者无法进行写操作,无论队列中读者和写者的顺序如何,都会让读者进入的。
这样下去如果一直有读者在队列中,或者说一直有读线程产生,那写线程将一直执行不了,而被“饿死”。
为了避免这种情况的产生,我们定义了一种信号量-“队列锁”(queue),用来标识是否排队排到自己;当排到一个写者时,写者将更改这个信号量,使得在它被执行之前,后面的读者不会“插队”。
下面自己看代码注释(也就增加了几行代码):
import java.util.concurrent.Semaphore; //ShareData.java public class SharedData { public Semaphore mutex = new Semaphore(1);//确保更新“读者数”时的互斥(姑且称之为“改数锁”)->必要锁 //initialized to 1, used to ensure mutual exclusion when the variable readcount is updated. public Semaphore wrt = new Semaphore(1);//确保最多只能有一个写者(姑且称之为“读写锁”)->核心锁 //initialized to 1, common to both the reader and writer processes, used as a mutual exclusion semaphore for the writer. public Semaphore queue=new Semaphore(1);//确保队列顺序执行,即公平竞争(姑且称之为“队列锁”)->公平竞争锁 public int readcount = 0;//写者数 //initialized to 0, keeps track of how many processes are reading the object. public String fileName = "hello";//模拟数据库 }
import java.io.FileNotFoundException; //Writer,java import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.Scanner; public class Writer implements Runnable { private SharedData sharedData; public Writer(SharedData shareData) { super(); this.sharedData = shareData; } public void write() { //向hello中写入控制台输入的一行数据 try { FileOutputStream fos = new FileOutputStream(sharedData.fileName); PrintWriter pw = new PrintWriter(fos); Scanner sc = new Scanner(System.in); for (int i = 0; i < 1; i++) { System.out.print("Writer " + Thread.currentThread().getName() + " input:"); pw.println("Writer " + Thread.currentThread().getName() + sc.next()); pw.flush(); } try { fos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void run() { while (true) { try { sharedData.queue.acquire();//等待队列资源 sharedData.wrt.acquire();//请求互斥信号量资源(加锁) sharedData.queue.release();//排队成功,释放队列资源 //writing is performed write(); sharedData.wrt.release();//释放信号量资源(解锁) } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { Thread.sleep(1000); //线程休眠一秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
import java.io.BufferedReader; //Reader.java import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; public class Reader implements Runnable { SharedData sharedData; public Reader(SharedData sharedData) { super(); //继承父类的run() this.sharedData = sharedData; } public void read() { //按照行从hello中读取数据,并显示到控制台 try { FileInputStream fis = new FileInputStream(sharedData.fileName); BufferedReader br = new BufferedReader(new InputStreamReader(fis)); String s; try { while ((s = br.readLine()) != null) { System.out.println("Reader " + Thread.currentThread().getName() + ": " + s); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { fis.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void run() { while (true) { try { sharedData.queue.acquire();//等待队列资源 sharedData.mutex.acquire();//为改“读者数”加锁 sharedData.readcount++; if (sharedData.readcount == 1)//如果此时读者数为1,那么为“不让写者进入”加锁 sharedData.wrt.acquire(); sharedData.mutex.release();//为改“读者数”解锁 sharedData.queue.release();//排队成功,释放队列资源 // reading is performed read(); //全部读取出来咯 sharedData.mutex.acquire();//为改“读者数”加锁 sharedData.readcount--; if (sharedData.readcount == 0)//如果此时读者数为0,那么为“不让写者进入”解锁 sharedData.wrt.release(); sharedData.mutex.release();//为改“读者数”解锁 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { Thread.sleep(1000); //线程休眠一秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
3.“写者优先算法”
我们对比上面两种算法发现,咦?第一种算法因为写者必须要看“读写锁”是否锁住才能判断是否进入写操作,而读者不用。故此读者优先级自然地相对较高。
这不公平啊!凭什么只能读者优先或者公平竞争?我“写线程”家族表示不服!
于是,我们考虑实现一下写者优先算法,来平衡一下线程世界不安定的情绪。
怎末去实现写者优先呢?
经过研究读者优先的算法我们发现,是因为读者优先算法中,写者的每次申请都要看“读写锁”是否打开,而读者只需要第一个读者申请就会一直锁住“读写锁”,直到队列中没有读者。同理可以借助“队列锁”,定义一个writecount记录队列中的写者数,第一个写者将“队列锁”占用,直到队列中没有写者了才打开“队列锁”,从而实现写者优先。
由于涉及到多个写者对writecount的操作,我们定义了信号量--“改数锁2号”(writerFirst),确保此种操作的互斥性。
具体实现如下(如果觉得抽象的话,注意对比代码及注释):
import java.util.concurrent.Semaphore; //ShareData.java public class SharedData { public Semaphore mutex = new Semaphore(1);//确保更新“读者数”时的互斥(姑且称之为“改数锁”)->必要锁 //initialized to 1, used to ensure mutual exclusion when the variable readcount is updated. public Semaphore wrt = new Semaphore(1);//确保最多只能有一个写者(姑且称之为“读写锁”)->核心锁 //initialized to 1, common to both the reader and writer processes, used as a mutual exclusion semaphore for the writer. public Semaphore queue=new Semaphore(1);//确保队列顺序执行,即公平竞争(姑且称之为“队列锁”)->公平竞争锁 public Semaphore writerFirst=new Semaphore(1);//确保更新“写者数”时的互斥(姑且称之为“改数锁2号”)->必要锁 public int readcount = 0;//写者数 public int writecount = 0;//读者数 //initialized to 0, keeps track of how many processes are reading the object. public String fileName = "hello";//模拟共享数据 }
import java.io.BufferedReader; //Reader.java import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; public class Reader implements Runnable { SharedData sharedData; public Reader(SharedData sharedData) { super(); //继承父类的run() this.sharedData = sharedData; } public void read() { //按照行从hello中读取数据,并显示到控制台 try { FileInputStream fis = new FileInputStream(sharedData.fileName); BufferedReader br = new BufferedReader(new InputStreamReader(fis)); String s; try { while ((s = br.readLine()) != null) { System.out.println("Reader " + Thread.currentThread().getName() + ": " + s); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { fis.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void run() { while (true) { try { sharedData.queue.acquire();//等待队列资源 sharedData.mutex.acquire();//为改“读者数”加锁 sharedData.readcount++; if (sharedData.readcount == 1)//如果此时读者数为1,那么为“不让写者进入”加锁 sharedData.wrt.acquire(); sharedData.mutex.release();//为改“读者数”解锁 sharedData.queue.release();//排队成功,释放队列资源 // reading is performed read(); //全部读取出来咯 sharedData.mutex.acquire();//为改“读者数”加锁 sharedData.readcount--; if (sharedData.readcount == 0)//如果此时读者数为0,那么为“不让写者进入”解锁 sharedData.wrt.release(); sharedData.mutex.release();//为改“读者数”解锁 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { Thread.sleep(1000); //线程休眠一秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
import java.io.FileNotFoundException; //Writer,java import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.Scanner; public class Writer implements Runnable { private SharedData sharedData; public Writer(SharedData shareData) { super(); this.sharedData = shareData; } public void write() { //向hello中写入控制台输入的一行数据 try { FileOutputStream fos = new FileOutputStream(sharedData.fileName); PrintWriter pw = new PrintWriter(fos); Scanner sc = new Scanner(System.in); for (int i = 0; i < 1; i++) { System.out.print("Writer " + Thread.currentThread().getName() + " input:"); pw.println("Writer " + Thread.currentThread().getName() + sc.next()); pw.flush(); } try { fos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void run() { while (true) { try { sharedData.writerFirst.acquire();//改数必备 sharedData.writecount++; if (sharedData.writecount == 1) sharedData.queue.acquire();//如果是第一个写者,则占用队列锁 sharedData.writerFirst.release(); sharedData.wrt.acquire();//请求互斥信号量资源(加锁) //writing is performed write(); sharedData.wrt.release();//释放互斥信号量资源(解锁) sharedData.writerFirst.acquire(); sharedData.writecount--; if (sharedData.writecount == 0) sharedData.queue.release();//如果队列中没有了写者,释放队列资源 sharedData.writerFirst.release(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { Thread.sleep(1000); //线程休眠一秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
写个主函数验证一下写者优先:
/** * Created by 王锋 on 2018/6/22. */ public class Main { /** * 写者优先算法 */ public static void main(String[] args) { SharedData sharedData=new SharedData();//共享资源 Reader reader=new Reader(sharedData);//读者 Writer writer=new Writer(sharedData);//写者 //由于下面均开辟了新线程,理论上应该各个线程并发执行; //但我们加入了mutex、wrt等信号量,故结果大大不同咯 new Thread(writer).start(); //写者类型的线程启动(实际执行run()函数) new Thread(reader).start(); new Thread(writer).start(); new Thread(reader).start(); new Thread(reader).start(); } }
运行结果:
可以看到由于队列中一直有写线程(thread-0和thread-2),所以没有轮到读线程进行读取。
其它两种算法可以自行验证。