您的位置:首页 > 编程语言 > Java开发

java并发编程(1)火车票售票问题

2017-08-02 13:38 148 查看
Java的关于线程同步和互斥方面的策略有很多,比如synchronized、信号量、线程的wait和notify方法等等。讲解之前我们有必要区分对象、类和线程的所有权关系。首先声明一点:一个对象可以有多个线程共享,比如我们在一个类A中写十个内部类,这十个内部类都继承Thread类,然后类A中有十个属性,分别是十个内部线程类的对象,这种情况就属于多个线程共享一个对象;我们还必须清楚的认识到,一个类也可以有多个线程共享,比如我们有个类B继承了Thread类,我们在另外一个类中用循环new十个B的对象并启动线程,那么这十个线程就共享B类的所有静态属性。

下面一个一个来讲解:

我们先来看看原始的代码是怎样的,如下所示,这是一个售票的程序,共有十个售票员线程,想共同把150张票售完,代码如下所示:

_______________________________________________________________________________

public
class
SynchorizedTest implements Runnable{
    private
static int
num=150;    
//表示总共的票数 150,所有对象共享
    private
int
seller;            
//表示售票员的编号
    SynchorizedTest(int seller){   
//构造方法 
可以指定对象是哪个销售员
       this.seller=seller;
    }
    private
void
sell(){        //当前售票员的售票方法
    
4000
   if(num>0){
           System.out.println("seller"+this.seller+":ticket"+num+"
isselled!");
           num--;
       }
    }
    public
static void
main(String[] args) {
       for(int i=1;i<=10;i++){
           SynchorizedTest st=new SynchorizedTest(i);
           new Thread(st).start();
       }
    }
    @Override
    public
void
run() {         //每个售票员线程都执行相同的售票操作
       while(true){
           sell();
           if(num<=0)break;
       }
      
    }
}
_______________________________________________________________________________

seller2:ticket 150 is selled!
seller1:ticket 150 is selled!
seller2:ticket 149 is selled!
seller10:ticket 150 is selled!
seller9:ticket 150 is selled!
seller7:ticket 150 is selled!
seller8:ticket 150 is selled!
seller6:ticket 150 is selled!
seller6:ticket 142 is selled!
seller6:ticket 141 is selled!
seller6:ticket 140 is selled!
seller3:ticket 150 is selled!
seller5:ticket 150 is selled! _______________________________________________________________________________
原本代码的执行结果如上所示,我们会发现第150号票被多次重复售出,这很明显是不合理的。

究其原因,是因为num-- 这个操作是CPU来做的,系统先把内存中num加载到CPU的寄存器中,然后执行加法操作,再从寄存器写到内存原来num的位置。如果是一个线程,这个过程没什么问题,但是这里是多线程的,很可能一个线程把num刚加载到CPU,还未来得及写回内存,这个线程的时间片用完了,第二个线程又开始把num加载到CPU执行运算,这样就是导致重复售票的问题。如果想要解决这个问题,关键是在一个线程开始把num加载到CPU,直到运算完写回CPU的过程中不能中断(这里指的是不能被其他9个线程横叉一笔,并不是说这个期间要一直占用CPU,因为还有其他程序的线程)。

接下来我们试试java提供的各种同步互斥的策略能不能解决这个问题。

1.   synchronized关键字

synchronized关键字是java提供的一种比较简答的解决同步和互斥的策略。Synchronized可以锁定对象,代码块,静态方法和普通方法,下面我们分别讨论各种锁定的功能在这道题上的效果。

(1)  对象/代码块的锁定

Synchronized关键字锁定对象和代码块的格式为:

synchronized(对象)

{

               互斥执行的代码块

}

Synchronized对对象进行锁定的时候,不管当前有多少个线程共用这个对象,在执行到我们锁定的代码块的时候当前对象只能被一个线程拥有,这是因为每个对象都有一个monitor,线程在使用对象的时候必须先获得对象monitor的所有权。如果我们想互斥的访问某个对象的某些属性,可以在对这些属性进行修改的地方把当前对象锁定。

对这道题来说,锁定对象似乎并没有什么作用,因为我们在main方法中是new了十个对象,这十个对象各是一个线程,这十个线程共享的是一个类,而不是一个对象,因此锁定代码块没有什么作用,不信吗?我们来试试!

下面的代码是对sell方法中当前对象进行锁定(其他代码不变):

_______________________________________________________________________________

private
void
sell(){          //当前售票员的售票方法
  synchronized(this){
     if(num>0){
         System.out.println("seller"+this.seller+":ticket
"+num+" isselled!");
         num--;
     }
  }
}

_______________________________________________________________________________

执行的部分结果如下所示:

_______________________________________________________________________________

seller1:ticket 150 is selled!
seller2:ticket 150 is selled!
seller2:ticket 148 is selled!
seller2:ticket 147 is selled!
seller5:ticket 147 is selled!
seller5:ticket 145 is selled!
seller5:ticket 144 is selled!
seller5:ticket 143 is selled!
seller5:ticket 142 is selled!
_______________________________________________________________________________
        经过多次执行测试,可以发现依旧存在重复售票的问题。

(2)  普通方法的锁定

如果一个非静态的方法声明中使用synchronized,这个方法的在执行期间当前对象只能被一个线程拥有。换句话说:synchronized void f() { /* body */ } 和voidf() { synchronized(this) { /* body */ } }是完全等价的。所以,和上面一样,锁定方法也不适用于售票问题。

(3)  静态方法的锁定

静态方法的锁定本质上是对类的锁定而不是对象的锁定,对于这道题来说,如果我们把sell方法声明为静态的,然后用synchronized锁定,可不可以解决重复售票的问题呢?我们拭目以待!

___________________________________________________________________________________

private
static synchronized
void
sell(){         //当前售票员的售票方法
       if(num>0){
           System.out.println("ticket"+num+" isselled!");
           num--;
       }
    }

_____________________________________________________________________

ticket 150 is selled!
ticket 149 is selled!
ticket 148 is selled!
ticket 147 is selled!
ticket 146 is selled!
ticket 145 is selled!
ticket 144 is selled!
ticket 143 is selled!
ticket 142 is selled!

……

ticket 5 is selled!
ticket 4 is selled!
ticket 3 is selled!
ticket 2 is selled!
ticket 1 is selled!

_____________________________________________________________________

    因为静态方法中不能访问seller这个非静态的属性,所以不得不把销售员去掉了,但是大家可以发现重复售票的问题确实被解决了,但是不知道那个售票员卖出的票心里总觉得不放心,那么我们在线程的run方法中把这个seller输出:

_____________________________________________________________________

@Override
    public
void
run() {         //每个售票员线程都执行相同的售票操作
       while(true){
           sell();
           System.out.println(seller+"售票成功!");
           if(num<=0)break;
       }
      
    }

_____________________________________________________________________

ticket 150 is selled!
ticket 149 is selled!
10售票成功!
1售票成功!
ticket 148 is selled!
5售票成功!
ticket 147 is selled!
5售票成功!
ticket 146 is selled!
5售票成功!
ticket 145 is selled!
5售票成功!
ticket 144 is selled!

_____________________________________________________________________

    多次执行结果表明,每个售票员确实是并发售票的,而且完美的解决了重复售票的问题!只是静态方法不能售票的同事显示售票员,所以看起来不是很美观。

(4)  对象属性的锁定

如果我们在num声明的时候直接指定num为互斥变量,结果会怎么样呢?很可惜,java里不能直接使用synchronized声明和修饰一个普通类型的变量(如果一个对象作为一个类的属性,是可以被synchronized修饰的,注意这里是修饰,不是声明),而是使用synchronized去修饰一个代码块或一个方法。

下面我们测试一下,如果把类的一个属性(前提是这个属性是某类的对象),那么类会不会也会被锁定呢?

测试代码如下:我们的目的是让加法的线程和减法的线程互斥执行,也就是先把num加大最大,然后减到最小,中间不要有加减法交叉。

_______________________________________________________________________________

 
public
class
SynchorizedTest {
   
    private
int
num;
    private String
flag;
    SynchorizedTest(){
       this.num=0;
       this.flag="Test";
    }
   
    private
void
addNum(){
       synchronized (this.flag){
           this.num++;
           System.out.print(this.num+"
");
       };
    }
    private
void
absNum(){  
       synchronized (this.flag){
           this.num--;
           System.out.print(this.num+"
");
       };
    }
    public
void
launch(){
       new ThreadAdd().start();
       new ThreadAbs().start();
    }
    public
static void
main(String[] args) {
       SynchorizedTest test=new SynchorizedTest();
       test.launch();
    }
    private
class
ThreadAdd extends Thread{
 
       @Override
       public
void
run() {
           while(true){
              addNum();
              if(num>10)break;
           }
          
       }
      
    }
    private
class
ThreadAbs extends Thread{
 
       @Override
       public
void
run() {
           while(true){
              absNum();
              if(num<-10)break;
           }
          
       }
      
    }
}
 

_______________________________________________________________________________

代码执行的结果如下所示:

_______________________________________________________________________________

1 2 3 4 5 6 7 8 9 10 11 10 9 8 7 6 5 4 3 2 1 0 -1 -2 -3 -4 -5 -6 -7 -8 -9-10 -11

_______________________________________________________________________________

我们会发现结果相当好,多次执行结果表明两个线程看起来是成功互斥执行。但是为什么要重新定义一个String类型的属性,为什么不能直接用synchronized修饰num呢?用synchronized修饰num我也想啊,可是java规定synchronized不能修饰基本数据类型,只能修饰对象,方法或者代码块,因此我们只能定义一个String类型的对象,但是锁定对象的某个属性和锁定对象一样吗?为什么能通过锁定属性实现互斥对象呢?再一次证明了

答案是我们不能把锁定对象的属性和锁定对象混为一谈,如果我们把加法的上限加到10000,把减法的下限减到-10000,会发现两个线程依旧存在交叉执行的情况,因此可以发现,num和flag都是对象的属性,锁定flag属性并不能保证num属性也处于锁定状态。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: