同步之synchronized关键字

    快过年了,相信很多朋友都准备在网上买票然后舒舒服服的回家过年吧,然而抢票难却让大伙都很郁闷。前几天跟一个在广州工作的朋友聊天的时候,他还说因为买不到高铁票而要骑自行车回家呢,虽然也不远,才400多公里,对吧。但不管怎样,中国人的传统,回家团圆。希望大家都能顺顺利利的回家跟家人团聚。 

    接下来进入正题,谈谈java的同步机制synchronized。在讲之前,让我们先来模拟一下买票情景:假设某车站有三个窗口同时在卖18张票,为了更真实的模拟现实情景,每卖出一张票我们就休眠10毫秒,因为网络有可能不是实时传输的。 请看下面的例子:


package com.gk.thread.mutex.synchronization;

public class SellTicket implements Runnable {

	private int tickets = 18;
	
	@Override
	public void run() {

		while(true) {
			
			if(tickets > 0) {
				System.out.println(Thread.currentThread().getName() + "正在出票...   " + tickets--);
				
				try {
					Thread.sleep(1 * 10);	
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}else {
				break;
			}
		}

	}

}


    测试代码


package com.gk.thread.mutex.synchronization;

public class Test {
	
	public static void main(String[] args) {
		
		// 多态方式创建资源对象
		Runnable r = new SellTicket();
		
		new Thread(r, "窗口1").start();		// 匿名对象
		new Thread(r, "窗口2").start();
		new Thread(r, "窗口3").start();

	}
	
}


    上面程序代码看似没什么问题,但是运行结果却让我们很郁闷。不仅出现了不同的窗口在卖同一张票的情况,还出现了卖-1张票的情况。

    想不通,为什么会出现这种情况呢?我们再想想,既然线程的执行是随机不确定的,是不是有可能上面三个线程恰好同时进入了run方法中的if语句然后又恰好同时执行了“Thread.currentThread().getName() + "正在出票...   " + tickets--”。由于这三个线程同时执行了这条语句,也就是说tickets还没来得及自减一,其他线程又输出了这个tickets,所以也就不难理解上面结果中出现了不同的窗口在卖同一张票的情况了。同理,出现0-1也是这样,当tickets等于1的时候,三个线程都判断tickets0,所以三个线程都进入了if语句。然后窗口1这个线程先输出tickets=1,然后tickets自减一等于0,然后窗口3输出了tickets=0,然后tickets自减一等于-1,最后窗口2执行输出语句,所以就输出了-1。经过上面那样分析,感觉也是蛮有道理的,对吧。其实,这就是多线程安全问题了。

    既然知道了是多线程安全导致的,那么我们要怎样解决呢?我们中国有句古话叫做对症下药,要彻底的解决这个问题就先要知道产生这个问题的具体原因是什么。一般认为多线程安全是由下面几个原因引起的:1、多线程环境;2、有共享数据;3、对共享数据的操作不是原子性的。

    分析了原因之后,我们再来看看要怎样使上面的代码避免多线程安全问题。既然是多个窗口同时在卖票,所以多线程环境是肯定避免不了的,而且多个窗口卖的是同一种票,所以这同一种票也就是共享数据,这也是避免不了的。既然出现多线程安全的前两个原因都是我们避免不了的,那么我们就只能从第3个原因下手了。在上面的操作中,我们对共享数据tickets做了非原子性的操作(判断(tickets> 0)、输出(tickets)、自减(tickets--)),现在要解决这个问题就是要设法使对tickets的操作是原子性的,也就是说将对tickets的操作包成一个整体。那么有什么方法可以实现呢?别担心,聪明的java创造者们早就想到了这个问题,他们提供了同步机制synchronized可以帮助我们解决这个问题

        在使用synchronized关键字时,就相当于给代码加上了锁,被加上锁的代码在同一时刻最多只有一个线程可以执行。也就是说被synchronized修饰后的代码就变成了一个整体,这时任何线程对它的操作就不再是非原子性的了。

        synchronized关键字可以修饰方法或代码块。修饰方法时,锁对象是this(静态方法是当前类的Class文件对象),直接在方法声明上加上synchronized即可,一般加在权限修饰符后面及方法返回值前面(可参考StringBuffer类的源码);修饰代码块时,格式是这样的:synchronized(锁对象){需要同步的代码;},其中锁对象可以是任意的(Object或是其它),但不同线程必须使用同一个锁对象,这个锁对象才是真正的起同步作用。

        下面修改SellTicket类,给if语句加上synchronized


package com.gk.thread.mutex.synchronization;

public class SellTicket implements Runnable {

	private int tickets = 18;
	private Object obj = new Object();
	
	
	@Override
	public void run() {

		while(true) {
			
			/*
			 *  synchronized修饰代码块的时候锁对象可以是任意的,这里使用object
			 */
			synchronized(obj) {	
				if(tickets > 0) {
					System.out.println(Thread.currentThread().getName() + "正在出票...   " + tickets--);
					
					try {
						Thread.sleep(1 * 10);	// 为了跟真实的模拟现实情况,每卖出一张票就停止10毫秒
					} catch (InterruptedException e) {
						throw new RuntimeException(e);
					}
				}else {
					break;
				}
			}
		}

	}

}


    测试代码


package com.gk.thread.mutex.synchronization;

public class Test {
	
	public static void main(String[] args) {
		
		// 多态方式创建资源对象
		Runnable r = new SellTicket();
		
		new Thread(r, "窗口1").start();		// 匿名对象
		new Thread(r, "窗口2").start();
		new Thread(r, "窗口3").start();
		
	}
	
}


    对共享数据tickets的非原子性操作加上synchronized同步锁之后就不会再出现上面的线程安全问题了。但是这种获得的安全也是有代价的,每个线程执行的时候都要去判断同步上的锁,这是很耗系统资源的,无形之中就会降低程序的运行效率。不过由此获得了安全,我想这种代价是值得的。



猜你喜欢

转载自blog.csdn.net/leexichang/article/details/79327341
今日推荐