在java中,很多时候大家都是用窗口售票这个实例来理解同步锁synchronized的,而关于这个实例,作为一个初学者,我最开始接触的时候对里面的一些用法感到十分难以理解,比如,在用两个方法(继承Thread类和实现Runnable接口)时,为什么定义票数一个用静态一个可以不用,下面就我遇到的问题进行说明,希望对初学者理解同步锁有所帮助。

这里先用两种方法实现窗口售票实例,一种是继承Thread类,一种是实现Runnable接口

问题一:定义票数时,为什么一个用静态修饰,一个不用

首先方法一,继承Thread类:

代码如下:

package org.westos_07:
 
public class SellTicket extends Thread { 
//这100张票应该被三个线程共用,所以用static修饰
private static int tickets = 100 ;
@Override
public void run() {
//st1,st2,st3都要执行run()方法中代码
//模拟该电影院售票:电影院一直有票
while(true){
if(tickets>0){
System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}

主线程:

public class SellTicketDemo {
public static void main(String[] args) {
//创建线程类对象
SellTicket st1 = new SellTicket() ;
SellTicket st2 = new SellTicket() ;
SellTicket st3 = new SellTicket() ;
 
//分别命名三个窗口
st1.setName("窗口1") ;
st2.setName("窗口2") ;
st3.setName("窗口3") ;
//启动线程
st1.start() ;
st2.start() ;
st3.start() ;
}
}

方法二,实现Runnable接口

代码如下:

package org.westos_08;
 
public class SellTicket implements Runnable {
 
private int  tickets = 100 ;
@Override
public void run() {
// 模拟电影院一直有票
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}

主线程:

public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
SellTicket st = new SellTicket() ;
//创建线程类的对象
Thread t1 = new Thread(st, "窗口1") ;
Thread t2 = new Thread(st, "窗口2") ;
Thread t3 = new Thread(st, "窗口3") ;
//启动线程
t1.start() ;
t2.start() ;
t3.start() ;
}

在给出的代码里面,我们可以看出,对于票数的定义,一个加入了静态的修饰,一个没有,这是因为什么呢?最开始我将两个都加了静态,这里都没有出现问题,而在两个都去掉之后,Thread继承这个里面就会出现三个窗口都是从100开始,到1结束的情况,一共有300张票,而理想情况应该是三个窗口共同售卖100张票,一共只有100张票。

这是因为,在使用Thread继承时,我们可以在主线程中看出,一共定义了三个自定义对象,而在接口中就只定义了一个自定义对象,然后用Thread类的对象来启动线程的,那么问题就在这里,如果是三个对象,那么他们各自都是会有自己的成员变量,三个各自定义了100张票,那么启动线程时,就会各自售卖自己的,一个的票数改变了是不会影响另外一个的,那么这样要怎么解决呢?那就需要三个对象共同使用一份资源,加一个静态修饰就行了,这样三个对象就是使用了一份资源。而在用接口实现的时候,因为创建了一个自定义对象,它们本身就是一份资源,三个Thread类共同使用,就不会出现各自定义的情况。

 

当然,我们执行这两个代码都是没有问题的,三个窗口同时售卖100张,但是,如果我们加入延时呢?比如下面这种情况(下面有些问题只用接口举例,而且因为主线程不会有变化,所以只给出自定义类的代码)

问题二:为什么要加延时以及为什么要把延时加入if里面

 

代码如下:

package org.westos_08;
 
public class SellTicket implements Runnable {
 
private int  tickets = 100 ;
@Override
public void run() {
// 模拟电影院一直有票
while (true) {
if (tickets > 0) {
try {
Thread.sleep(100) ;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}

加延时的原因就是因为,cpu的一点点时间片可以让线程运行多次,在加入延时后,比如,当窗口一(也就是线程一)拿到的是票数1,然后延时,那么紧接着窗口二,就会拿到票数0,窗口三拿到票数-1,这样售卖的票就会出现负票,就会有问题。这样就可以引进同步锁synchronized了,让初学者能够理解同步锁的作用以及运用时机。

而这里有为什么要将延时加入if里面呢?这里就会有人奇怪为什么要这样问,那是因为当我把延时加在if外面,while循环里面的时候,并不会有什么问题,而且当时我还想过,如果这样的话,根本不可能出现负票,因为,0和-1根本就不会通过if的判断条件,更加不可能执行里面的语句了,这样就意味着不加同步锁也是可以的,那么同步锁的意义又在哪里?后来我在网上找到了一个博主写的博客的解释,如下:

“这里的原因是因为,IF语句是判断语句,只有已经通过判断之后休眠才会出现负票的情况,比如

例:我们可以假设,当只有一张票的时候,乙,甲、丙和丁四个线程依次加入就绪队列,乙线程先获得 CPU 执行权,通过 if 语句,然后在 sleep(10)下休眠(注意,就是这个地方),也就是在这短时间,加、丙和丁线程也依次通过了 if 语句。10 ms 后,乙线程 执行下面的打印票信息,即 1 号票,然后,另外三个线程也依次苏醒,输出 0 号票,-1 号票,-2 号票。

如果在休眠之后再判断,但乙出票之后再减减,就不会通过判断语句,当然也不会执行里面的语句了。”

这样就能很好的解释延时为什么会出现负票,以及为什么要加在if里面了。这里我们就可以引进同步锁synchronized了。代码如下:

public class SellTicket implements Runnable {
 
//100张票被共用,不让外界修改数据
private  static int tickets = 100 ;
//同步锁对象
private Object obj = new Object() ;
@Override
public void run() {
// 模拟电影院一直有票
while (true) {
//设置线程睡眠0.1m
//ts1,ts2,ts3
//相当于ts1,ts2,ts3创建自己的锁定(不是同一把锁)
synchronized(new Object()){
//每一个线程ts1,ts2,ts3都使用的一个锁
//ts1进来,其他线程进不来
if (tickets > 0) {
try {
Thread.sleep(100) ;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}//ts1出来了
}
}
}

同步锁的意思是排队,正好和他的字面意思相反,加了同步锁之后,一个线程只有等抢到cpu执行权的线程执行完毕后,释放锁,然后再执行,这样就不会出现负票的情况了。

问题三:用Thread类怎么实现同步锁

在用接口实现同步锁之后,我也想用Thread继承来实现同步锁的作用,可是,直接沿用接口的方法,可以发现并不能解决问题,还是会有负票出现,这又是为什么呢?下面先给出我解决问题后的代码,然后进行解释:

代码:

public class MyThread extends Thread{
private static int k=100;
private static Object obj = new Object() ;
@Override
public void run() {
while(true){
synchronized (obj) {
if(k>0){
try {
Thread.sleep(100) ;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"正在出售第"+(k--)+"张票");
}
}
}
}
}

方法就是这行代码private static Object obj = new Object() ;,其实原因和问题一差不多,因为定义了同步锁,而锁是定义了一个Object对象,如果使用Thread类不加静态修饰的话,就相当于各自个它们加了一把锁,意思就是在线程一出票的时候,线程二同样能进去,同步锁是起不到作用的,所以给Object对象加了静态修饰,让他们共同使用同一把锁,这样就和接口一样了,接口只是创建了一个对象,所以不加静态也是使用的一把锁。

这里对于售票这个实例我的理解就是这么多了,希望对和我一样的出入java的人有所帮组。