1.单线程执行模式(Single Threaded Excution)
案例
/**
* 共享资源
*/
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.address = address;
this.name = name;
verify();
}
private void verify() {
if(this.name.charAt(0) != this.address.charAt(0)){
System.out.println("======Broken======"+toString());
}
}
@Override
public String toString() {
return "No."+counter+":"+name+","+address;
}
}
/**
* 共享资源的使用者
*/
public class User extends Thread{
private final String myName;
private final String myAddress;
private final Gate gate;
public User(String myName, String myAddress, Gate gate) {
this.myName = myName;
this.myAddress = myAddress;
this.gate = gate;
}
@Override
public void run() {
System.out.println(myName+" Begin");
while(true){
this.gate.pass(myName,myAddress);
}
}
}
public class Client {
public static void main(String[] args) {
Gate gate = new Gate();
User bj = new User("BaoBao","Beijing",gate);
User sh = new User("GuangLao","GuangZhou",gate);
User gz = new User("ShangLao","ShangHai",gate);
bj.start();
sh.start();
gz.start();
}
}
分析:
出现上述错误的原因是因为,
- 线程t1在执行pass的方法的时候,如果将pass方法执行完,name和address的首字母应该是相同的,由于没有加锁,执行到,将address和name分别修改为 Shanghai 和 ShangLao,此时将CPU执行权切换给了线程t2
- 线程t2只在pass方法中执行到了,将address修改为Beijing,就又将CPU的执行权切回给线程t1
- 此时线程t1在判断完之后,发现它们的首字母是不相同的,于是就通过了判断,进入了if语句块中,此时CPU的执行权切换给线程t2
- 线程t2继续执行将name也进行了修改为了BaoBao,此时CPU的执行权切换给线程t1
- t1执行了打印语句,所以出现了上述的首字母是相同的,但是却进入了if语句进行打印的情况
而以上的原因就在于多个线程在操作共享资源的时候,一个线程在进行读写操作的时候,另一个线程也进行写操作,从而导致最终输出的数据混乱
而单线程执行模式的解决思路就是,在一个线程进行操作的时候,其他线程只能等待,只有我这个线程执行完我的操作,其他线程才可以再操作共享资源
根据上述思路,我们只需要对操作共享资源的方法所在的类进行处理
package SingleThread;
/**
* 共享资源
*/
public class Gate {
//门记录的通过门的人的数量
private int counter = 0;
//门记录通过门的人的名称
private String name = "Nobody";
//门记录通过门的人的地址
private String address = "Nowhere";
/**
* 临界区:
*/
public synchronized void pass(String name,String address){
/**
* 多个线程对共享资源进行操作就会发生竞争
*/
this.counter++;
this.address = address;
this.name = name;
verify();
}
private void verify() {
if(this.name.charAt(0) != this.address.charAt(0)){
System.out.println("======Broken======"+toString());
}
}
@Override
public synchronized String toString() {
return "No."+counter+":"+name+","+address;
}
}
上述就保证了在执行整个pass方法的时候,只有一个线程执行完该整个方法,其他线程才可以执行,否则只能等待
其实jdk早期的集合Vector和Hashtable就是使用了这个模式的思想来保证线程安全,在所有提供给外界的方法都使用synchronized来进行修饰,来保证对该方法的单线程执行,关于它们详细讲解的链接如下:待补充
但是这种模式有一个致命的缺点:
- 就是效率问题,我们让程序在多线程环境下运行,就是为了并发,提高程序的运行效率,而使用这种模式又回到了单线程,使得程序运行的效率非常低
但其实我们可以看到对共享资源的操作既有读也有写,我们可以使用下面的模式对它进行改进
2.读写锁模式(ReadWriteLock)
(1)描述
对于共享资源,每个线程只有以下两个操作:
- 读
- 写
同一时刻,两个线程,可能同时出现的情况如下:
情况 | 线程1 | 线程2 | 是否允许两个线程并发操作呢? |
1 | 读 | 读 | 允许 |
2 | 读 | 写 | 不允许 |
3 | 写 | 读 | 不允许 |
4 | 写 | 写 | 不允许 |
- 而如果我们都是对所有对共享资源都使用synchronized来单线程执行的话,显然,我们可以发现,当多个线程都是读操作的时候,他们完全可以并发进行,
- 所以,我们对读写的情况进行分别讨论,对于情况1,2,3,4进行分离,使多个线程如果都是读的操作时,可以并发执行,提高程序的性能
(2)代码
package ReadWriteLock;
/**
* 对读写操作进行分情况实现,实现多个读操作时的并发执行
*/
public class ReadWriteLock {
//正在读的线程的数量
private int readingReaders = 0;
//等待读的线程的数量(即想读但是读不了在wait set中的线程的数量,即因为有其他线程在写而只能等待的写线程)
private int waitingReaders = 0;
//正在写的线程的数量
private int writingWriters = 0;
//等待写的线程的数量(即想写还不能去写的线程的数量,即因为有其他线程在读或者在写而只能等待的写线程)
private int waitingWriters = 0;
//是否更想写线程被先执行(有写线程在等待的情况下)?
private boolean preferWriter =true;
//默认更希望写线程先被执行
public ReadWriteLock() {
this(true);
}
public ReadWriteLock(boolean preferWriter) {
this.preferWriter = preferWriter;
}
public synchronized void readLock() throws InterruptedException {
try {
//先让要等待进行读操作的线程数量加一,如果执行到没有等待或者等待完,再减回来
this.waitingReaders++;
//如果有线程正在进行写操作,或者,使用者设置了更喜欢写线程并且有写线程在等待时,这时让读线程等待
while(writingWriters > 0 || (preferWriter && waitingWriters > 0)){
this.wait();
}
//只要没有写线程在操作或者等待的话,就可以进行读操作,
// 让正在读的线程数量加1就可以保证写线程只能等待,进入到wait set中
// 而读线程不受正在读线程的数量的限制,此方法执行完之后,其他读线程仍然可以使readingReaders再++,
// 即readLock()后的代码可以并发去读
this.readingReaders++;
}finally {
//线程正常执行了读操作或者抛出异常的时候就应该让等待进行读操作的线程数量减1
this.waitingReaders--;
}
}
public synchronized void readUnlock(){
//当读线程执行完之后,让正在读的线程数量减1,并且直到减到0,写线程才可以继续执行
this.readingReaders--;
/**
* 读线程读的时候,(即readingReaders > 0)
* 写线程只能进入wait set
* 读线程可以读,所以此时wait set中不可能有读线程,并且readingReaders可能大于1
* 所以notifyAll唤醒的都是写线程
* 而当readingReaders>0的时候,唤醒写线程没有意义,
* 将被移动到entry set之后又会因为readingReaders > 0,又调用wait()方法,进入到wait set当中,
* 做了大量无用操作,耗费性能,所以此处我们在readingReaders=0的时候,再去唤醒写线程,
*
* this.notifyAll();
* 比以上每次都唤醒所有效率更好
*/
if(readingReaders==0){
this.notifyAll();
}
}
public synchronized void writeLock() throws InterruptedException {
try {
//先让要等待进行写操作的线程数量加一,如果执行到没有等待或者等待完,再减回来
this.waitingWriters++;
//如果有线程正在进行写操作,或者读操作,此写操作只能等待
while(readingReaders > 0 || writingWriters > 0){
this.wait();
}
//只要没有其他写操作或读操作时,就可以进行写操作,
// 让正在写的线程数量加1就可以保证其他写线程或者读线程只能等待,进入到wait set中
this.writingWriters++;
}finally {
//线程正常执行了写操作或者抛出异常的时候就应该让等待进行写操作的线程数量减1
this.waitingWriters--;
}
}
/**
* 写线程写的时候,(即writingWriters > 0)
* 写线程只能进入wait set
* 读线程也只能进入wait set
* 所以writingWriters最大为1
* 所以notifyAll唤醒的既有读线程,也有写线程,而唤醒之后他们都有可能被执行
*/
public synchronized void writeUnlock(){
this.writingWriters--;
this.notifyAll();
}
}
package ReadWriteLock;
/**
* 共享资源
*/
public class SharedData {
//要进行读写操作的共享变量
private final char[] buffer;
private final ReadWriteLock lock = new ReadWriteLock();
//构造器,在创建共享数据的同时初始化它
public SharedData(int size) {
buffer = new char[size];
for (int i = 0; i < size; i++) {
buffer[i] = '*';
}
}
//读操作
public char[] read() throws InterruptedException {
try {
/**
* readLock两个作用:
* 1.执行完,readingReaders++,这样保证了写线程只能等待
* 2.保证在有写线程执行时,读线程在此方法中执行等待,
* 这样只有在有读线程的时候,代码才会向下执行,
* 即readingReaders充当了一个类似锁的条件判断作用
* (readingReaders>0,只有读线程可以执行;readingReaders=0,写线程才能执行),隔离了读写操作
*/
lock.readLock();
return doRead();
}finally {
lock.readUnlock();
}
}
//写操作
public void write(char c) throws InterruptedException {
try {
/**
* writeLock两个作用:
* 1.执行完,writingWriters++,这样保证了读线程和写线程只能进入循环等待
* 2.保证在有读或写线程执行时,写线程在此方法中执行等待,
* 这样只有在没有其他读线程和写线程的时候,代码才会向下执行,在执行下面的代码时,即便其他线程被调度,
* 由于writingWriters>0,其他线程也只能等待
* 即writingWriters充当了一个类似锁的条件判断作用
* (writingWriters>0,只有一个写线程可以执行;writingWriters=0,其他写或读线程才能执行),隔离了读写操作
*/
lock.writeLock();
doWrite(c);
}finally {
lock.writeUnlock();
}
}
//具体的写的业务
private void doWrite(char c) {
for (int i = 0; i < buffer.length; i++) {
buffer[i] = c;
slowly(10);
}
}
//具体的读的业务
private char[] doRead() {
char[] newBuf = new char[buffer.length];
for (int i = 0; i < buffer.length; i++) {
newBuf[i] = buffer[i];
}
//模拟操作的时间
slowly(50);
//这里不能直接返回buffer,因为它是引用类型,我们这样返回,线程在外面的修改直接修改的就是buffer的内容
return newBuf;
}
//模拟操作的时间
private void slowly(long mills) {
try {
Thread.sleep(mills);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package ReadWriteLock;
import java.util.Random;
/**
* 写线程
*/
public class WriterWorker extends Thread{
private static final Random RANDOM = new Random(System.currentTimeMillis());
private final SharedData sharedData;
private final String filter;
private int index = 0;
public WriterWorker(SharedData sharedData,String filter){
this.sharedData = sharedData;
this.filter = filter;
}
@Override
public void run() {
try {
while(true){
char c =nextChar();
sharedData.write(c);
System.out.println(Thread.currentThread().getName()+" is writing");
Thread.sleep(RANDOM.nextInt(1000));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private char nextChar(){
char c = filter.charAt(index);
index++;
if(index >= filter.length()){
index = 0;
}
return c;
}
}
package ReadWriteLock;
/**
* 读线程
*/
public class ReaderWorker extends Thread{
private final SharedData sharedData;
public ReaderWorker(SharedData sharedData) {
this.sharedData = sharedData;
}
@Override
public void run() {
try {
while(true){
char[] readBuf = sharedData.read();
System.out.println(Thread.currentThread().getName()+" reads "+String.valueOf(readBuf));
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
package ReadWriteLock;
public class ReadWriteLockClient {
public static void main(String[] args) {
SharedData data = new SharedData(10);
new ReaderWorker(data).start();
new ReaderWorker(data).start();
new ReaderWorker(data).start();
new ReaderWorker(data).start();
new ReaderWorker(data).start();
new WriterWorker(data,"aaaaaaaaaaaaaaaa").start();
new WriterWorker(data,"bbbbbbbbbbbbbbbb").start();
new WriterWorker(data,"cccccccccccccccc").start();
new WriterWorker(data,"dddddddddddddddd").start();
new WriterWorker(data,"eeeeeeeeeeeeeeee").start();
}
}