Quantcast
Channel: CSDN博客推荐文章
Viewing all articles
Browse latest Browse all 35570

java 线程同步浅析

$
0
0

一、为什么需要同步?


简单地说,之所以需要同步,是为了防止一个线程在执行一段代码块的这段时间内被中断。打个比方,你准备要吃包子,吃包子这个过程包括你把包子拿起来,和吃下去这两个原子动作,当你拿起包子的时候你当然不希望别人从你的手中把包子抢走,所以你希望在你把包子拿起来和吃下去这个过程中不会有别人来抢。 这里你和抢你包子的人好比两个线程,如果要实现吃包子的过程不被中断,就需要线程同步。

从程序角度来说,当有多个线程对同一个数据(即所谓的临界资源)进行写操作的时候,就需要同步,如果不同步则有可能产生不正确的结果。如果多个线程对临界资源仅仅是读,则不需要同步。考虑这样一个场景:铁道部销售火车票时,火车票的数量是固定的,每当有购票请求时,售票系统会启动一个线程执行出票、打印这两个操作,用程序模拟如下:


package com.litl.java.test;

public class TicketSale {

	
	private static int[] tickets=new int[10];
	private int mIndex;
	
	private void saleTicket(){
		if(mIndex==tickets.length){
			//System.out.println(Thread.currentThread().getName()+" 票已经卖完了!!!");
			return;
		}
		int ticket=tickets[mIndex]; //第一步,从系统中读取可用的票务信息
		System.out.println(Thread.currentThread().getName()+" 购票成功,出票编号为:"+ticket); //第二步,打印票给客户
		tickets[mIndex]=-1;//第三步,修改票信息数据库
		mIndex++; //第四步,可用票指针前移
	}
	
	class BuyThread extends Thread{
		
		public BuyThread(String name) {
			super(name);
		}
		
		@Override
		public void run() {
			saleTicket();
		}
	}
	
	private void startSale() {
		// 100个人去抢10张票
		for (int i = 0; i < 100; i++) {
			BuyThread buyThread = new BuyThread("thread-" + i);
			buyThread.start();
		}
	}
	
	public static void main(String[] args) {
		
		TicketSale ts=new TicketSale();
		
		//初始化票务信息
		for(int i=0;i<tickets.length;i++){
			tickets[i]=i;
		}
		
		ts.startSale();
	}
}

首先看一个正确的结果:

thread-1 购票成功,出票编号为:0
thread-3 购票成功,出票编号为:1
thread-5 购票成功,出票编号为:2
thread-7 购票成功,出票编号为:3
thread-9 购票成功,出票编号为:4
thread-11 购票成功,出票编号为:5
thread-13 购票成功,出票编号为:6
thread-15 购票成功,出票编号为:7
thread-17 购票成功,出票编号为:8
thread-19 购票成功,出票编号为:9

多运行几次(这也是多线程问题有时比较难以查找的原因,有时候概率甚至是非常低的,运行几十万次才出现一次),则出现在不想看到的结果:

thread-0 购票成功,出票编号为:0
thread-1 购票成功,出票编号为:1
thread-2 购票成功,出票编号为:1
thread-4 购票成功,出票编号为:3
thread-6 购票成功,出票编号为:3
thread-8 购票成功,出票编号为:3
thread-10 购票成功,出票编号为:3
thread-12 购票成功,出票编号为:3
thread-14 购票成功,出票编号为:3
thread-16 购票成功,出票编号为:3
thread-18 购票成功,出票编号为:3
thread-20 购票成功,出票编号为:3
thread-22 购票成功,出票编号为:3
thread-24 购票成功,出票编号为:3
thread-3 购票成功,出票编号为:3
thread-26 购票成功,出票编号为:3


thread-78 购票成功,出票编号为:3
thread-80 购票成功,出票编号为:3
thread-82 购票成功,出票编号为:3
thread-84 购票成功,出票编号为:3
thread-86 购票成功,出票编号为:3
thread-88 购票成功,出票编号为:3
thread-90 购票成功,出票编号为:3
thread-92 购票成功,出票编号为:3
thread-94 购票成功,出票编号为:3
thread-96 购票成功,出票编号为:3
thread-98 购票成功,出票编号为:3
Exception in thread "thread-89" java.lang.ArrayIndexOutOfBoundsException: 10
at com.litl.java.test.TicketSale.saleTicket(TicketSale.java:16)
at com.litl.java.test.TicketSale.access$0(TicketSale.java:9)
at com.litl.java.test.TicketSale$BuyThread.run(TicketSale.java:28)
Exception in thread "thread-91" java.lang.ArrayIndexOutOfBoundsException: 10
at com.litl.java.test.TicketSale.saleTicket(TicketSale.java:16)
at com.litl.java.test.TicketSale.access$0(TicketSale.java:9)
at com.litl.java.test.TicketSale$BuyThread.run(TicketSale.java:28)
Exception in thread "thread-93" java.lang.ArrayIndexOutOfBoundsException: 10
at com.litl.java.test.TicketSale.saleTicket(TicketSale.java:16)
at com.litl.java.test.TicketSale.access$0(TicketSale.java:9)
at com.litl.java.test.TicketSale$BuyThread.run(TicketSale.java:28)

上面的打印结果中,多个人买到了同一张票,可是一张票只能坐一个人!

为什么会出现上面这种错误的结果呢?从代码上看,原因很简单,所有人买到的票都是通过

int ticket=tickets[mIndex];
这条代码实现的,多个线程得到的ticket相同,也就是多个线程在买票时,所读取到的mIndex值是一样的,也就是说,当卖完第一张票之后,系统还没来得及更新mIndex,又处理了第二个买票请求,而这时的mIndex还未改变,于是两个人买到了同一张票!产生这个现象的本质原因,是由于两个线程交替执行,第一个线程还没有完成它应该完成的一系列任务时,系统把cpu时间片交给了另一个线程,而两个线程都对同一个资源进行了操作,于是产生了错误的结果。


那么,如何解决这种资源访问的冲突呢?很简单,只要让这段代码“不可中断”,即当一个线程进入这段代码时,只要还没有执行完,其他想要执行这段代码的线程就乖乖地在那里等着,即可。

int ticket=tickets[mIndex]; //第一步,从系统中读取可用的票务信息
System.out.println(Thread.currentThread().getName()+" 购票成功,出票编号为:"+ticket); //第二步,打印票给客户
tickets[mIndex]=-1;//第三步,修改票信息数据库
mIndex++; //第四步,可用票指针前移


二、java中线程同步的实现

java中是通过锁机制来实现线程之间的同步的。何谓锁呢?打个通俗的比方,在没有加锁的情况下,包子是放在食堂的桌子上,而所有想吃包子的人都在食堂里,谁都可以去抢,但是现在加锁之后,每次只能一个人进食堂里去吃包子(这个比喻好像不太恰当~~),因为食堂门被第一个进入食堂的人反锁了,其他的人都乱哄哄地堵在食堂门口,只有等在食堂里吃包子的那个人吃完出来解除反锁之后,才可以进去。

java中有两种实现锁的方法,一是显式的Lock,而是通过synchronized关键字来实现对资源(也就是代码块)加锁。这里我们修改上面的程序,用synchronized对买票方法进行加锁,即可避免上面出现的错误情况:

private synchronized void saleTicket(){
		if(mIndex==tickets.length){
			//System.out.println(Thread.currentThread().getName()+" 票已经卖完了!!!");
			return;
		}
	
		int ticket=tickets[mIndex]; //第一步,从系统中读取可用的票务信息
		System.out.println(Thread.currentThread().getName()+" 购票成功,出票编号为:"+ticket); //第二步,打印票给客户
		tickets[mIndex]=-1;//第三步,修改票信息数据库
		mIndex++; //第四步,可用票指针前移
	
	}
分析:

1,通过对saleTicket()方法添加synchronized关键字,当一个线程在执行它的过程中,不会被其他线程中断,从而保证了整个买票过程的完整性,避免两个人买了同一张票的问题。

2,这里的 synchronized所获取的锁是当前类实例对象的锁,即TicketSale当前实例对象的锁。


有关synchronized关键字的详细分析,将在下篇中介绍。









作者:wensefu 发表于2013-12-29 2:00:41 原文链接
阅读:125 评论:0 查看评论

Viewing all articles
Browse latest Browse all 35570

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>