Java多线程同步问题

        何谓同步问题呢?就是比如银行开了一个账户,那么如果我在ATM机上执行两个操作,分别是存入了200元,然后取出100元,原来账户中假设有1000元,那么此时操作完上述两步骤之后账户中总共有1100元。在银行账户上操作只有等当前步骤操作完才能进行下一个步骤,这就是同步。如果不同步呢,那么可能存入钱的时候同时取了钱,可能取完钱账户数据更新为900元,那么这就是因为不同步产生的脏数据。接下来我们通过举例的方式进行讲解同步问题以及相应的解决方法。首先我们本文采用的账户类Student类,该类中分别有存钱取钱两个操作。代码:

package person;

public class Student {
	public int k=0;
	public int total;
	public int perMonth;
	public String name;
	public void saveMoney(Student s) {
		// TODO Auto-generated method stub
		
		try {
			Thread.sleep(0);;
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		
		s.total=s.total+2*(s.perMonth);
		System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
		
	}
	public void getMoney(Student s)
	{
		try {
			Thread.sleep(0);;
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		
		s.total=s.total-s.perMonth;
		System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
		
	}

}
1.脏数据的产生

        假如有多个存钱的线程以及多个取钱的线程,那么账户的余额便是线程间的共享数据,如果线程间没有同步,而是乱序进行,可能出现几个线程同时对数据进行改变,那么就会产生脏数据。具体代码如下:

package waytobuildthread;

import person.Student;

public class RunnableTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		final Student s=new Student();
		s.total=1000;
		s.perMonth=200;
		s.name="Tom";
		System.out.println("Tom最开始拥有"+s.total+"元");
		Thread[] save=new Thread[10];
		Thread[] get=new Thread[10];
		for(int i=0;i<10;i++)
		{
			Thread t1= new Thread(){
	            public void run(){
	            	
	            	
	            	s.saveMoney(s);
	        		try {
	        			Thread.sleep(1000);;
	        		}
	        		catch(InterruptedException e) {
	        			e.printStackTrace();
	        		}
	            	
	            }
	        };
	        t1.start();
	        save[i]=t1;
		}
		
		for(int i=0;i<10;i++)
		{
			Thread t2= new Thread(){
	            public void run(){
	            	
	            	s.getMoney(s);
	            	
	        		try {
	        			Thread.sleep(1000);;
	        		}
	        		catch(InterruptedException e) {
	        			e.printStackTrace();
	        		}
	            	
	            }
	        };
	        t2.start();
	        get[i]=t2;
		}
		//等待线程结束
		for(Thread t:save)
		{
			try
			{
				t.join();
				
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}

		}
		//等待线程结束
		for(Thread t:get)
		{
			try
			{
				t.join();
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}

		}
		
		System.out.println("最后Tom总共拥有"+s.total+"元");
		
	}

}

运行程序,结果如下:

Tom最开始拥有1000元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有4200元
Tom存了400当前总共拥有3800元
Tom取了200当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom存了400当前总共拥有4600元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
Tom取了200当前总共拥有2800元
Tom取了200当前总共拥有2600元
最后Tom总共拥有2600元

        根据上述结果可以知道本来应该余额是3000元,结果只剩2600元,出现了脏数据。从第一组数据就是1800元,可能出现的情况就是一个存400的线程正在进行,另一个存400的线程紧接其后,还没来得及显示余额,第二笔款便存进去了。

        很显然,脏数据的存在是不可取的,因此应该使用相应的解决方法将线程间设置为同步问题,接下来就讲述几个解决同步问题的方法。

2.synchronized代码块

        使用同步代码块即可保证代码块内的程序是同步的,即当前有线程在执行代码块中的程序,其它程序只能处于等待状态。这样就不会出现因多个线程同时修改数据而产生脏数据。以下是代码:

package waytobuildthread;

import person.Student;

public class RunnableTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		final Object obj=new Object();
		final Student s=new Student();
		final Student jack=new Student();
		s.total=1000;
		s.perMonth=200;
		s.name="Tom";
		System.out.println("Tom最开始拥有"+s.total+"元");
		Thread[] save=new Thread[10];
		Thread[] get=new Thread[10];
		for(int i=0;i<10;i++)
		{
			Thread t1= new Thread(){
	            public void run(){
	            	
	            	synchronized(obj)
	            	{
	            		s.saveMoney(s);
	            	}
	        		try {
	        			Thread.sleep(1000);;
	        		}
	        		catch(InterruptedException e) {
	        			e.printStackTrace();
	        		}
	            	
	            }
	        };
	        t1.start();
	        save[i]=t1;
		}
		
		for(int i=0;i<10;i++)
		{
			Thread t2= new Thread(){
	            public void run(){
	            	synchronized(obj)
	            	{
	            		s.getMoney(s);
	            	}
	            	
	        		try {
	        			Thread.sleep(1000);;
	        		}
	        		catch(InterruptedException e) {
	        			e.printStackTrace();
	        		}
	            	
	            }
	        };
	        t2.start();
	        get[i]=t2;
		}
		//等待线程结束
		for(Thread t:save)
		{
			try
			{
				t.join();
				
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}

		}
		//等待线程结束
		for(Thread t:get)
		{
			try
			{
				t.join();
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}

		}
		
		System.out.println("最后Tom总共拥有"+s.total+"元");
		
	}

}

        以上代码是先创建Object对象,注意标明对象不可变,然后该对象作为修饰代码块的关键字synchronized括号中的参数。运行上述程序,得到如下结果:

Tom最开始拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有3800元
Tom存了400当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom存了400当前总共拥有4000元
Tom存了400当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
最后Tom总共拥有3000元

根据上述结果可以看出,每个线程都是有条理进行的,不会出现几个线程抢着修改数据的情况,因此最终结果也是与预计的相同,达到了同步的效果。

3.synchronized方法

        还可以使用关键字synchronized修饰方法,这样当有线程正在使用该方法时,会使其它线程进入等待状态,从而达到同步,修改Student类如下:

package person;

public class Student {
	public int k=0;
	public int total;
	public int perMonth;
	public String name;
	public synchronized void saveMoney(Student s) {
		// TODO Auto-generated method stub
		
		try {
			Thread.sleep(0);;
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		
		s.total=s.total+2*(s.perMonth);
		System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
		
	}
	public synchronized void getMoney(Student s)
	{
		try {
			Thread.sleep(0);;
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		
		s.total=s.total-s.perMonth;
		System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
		
	}

}

测试程序如同普通线程测试程序一样:

package waytobuildthread;

import person.Student;

public class RunnableTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		final Object obj=new Object();
		final Student s=new Student();
		final Student jack=new Student();
		s.total=1000;
		s.perMonth=200;
		s.name="Tom";
		System.out.println("Tom最开始拥有"+s.total+"元");
		Thread[] save=new Thread[10];
		Thread[] get=new Thread[10];
		for(int i=0;i<10;i++)
		{
			Thread t1= new Thread(){
	            public void run(){
	            	
	            	
	            	s.saveMoney(s);
	            	
	        		try {
	        			Thread.sleep(1000);;
	        		}
	        		catch(InterruptedException e) {
	        			e.printStackTrace();
	        		}
	            	
	            }
	        };
	        t1.start();
	        save[i]=t1;
		}
		
		for(int i=0;i<10;i++)
		{
			Thread t2= new Thread(){
	            public void run(){
	            	
	            	s.getMoney(s);
	            	
	            	
	        		try {
	        			Thread.sleep(1000);;
	        		}
	        		catch(InterruptedException e) {
	        			e.printStackTrace();
	        		}
	            	
	            }
	        };
	        t2.start();
	        get[i]=t2;
		}
		//等待线程结束
		for(Thread t:save)
		{
			try
			{
				t.join();
				
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}

		}
		//等待线程结束
		for(Thread t:get)
		{
			try
			{
				t.join();
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}

		}
		
		System.out.println("最后Tom总共拥有"+s.total+"元");
		
	}

}

运行结果如下:

Tom最开始拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有3800元
Tom存了400当前总共拥有4200元
Tom存了400当前总共拥有4600元
Tom存了400当前总共拥有5000元
Tom取了200当前总共拥有4800元
Tom取了200当前总共拥有4600元
Tom取了200当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
最后Tom总共拥有3000元
4.Lock锁

        JDK5新增加了一个Lock接口以及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程的同步。下面便将该例子通过Lock来实现同步,将Student类修改如下,测试程序不变动:

package person;
import java.util.concurrent.locks.*;
public class Student {
	public int k=0;
	public int total;
	public int perMonth;
	public String name;
	Lock lock=new ReentrantLock();//先建立一个lock对象
	public void saveMoney(Student s) {
		// TODO Auto-generated method stub
		
		try {
			Thread.sleep(0);;
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		try
		{
			
			lock.lock();
			s.total=s.total+2*(s.perMonth);
			System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
		}
		finally
		{
			lock.unlock();
		}
		
	}
	public void getMoney(Student s)
	{
		try {
			Thread.sleep(0);;
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		try
		{
			lock.lock();
			s.total=s.total-s.perMonth;
			System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
		}
		finally
		{
			lock.unlock();//释放锁
		}
		
		
	}

}

        运行测试程序结果如下:

Tom最开始拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有3800元
Tom存了400当前总共拥有4200元
Tom存了400当前总共拥有4600元
Tom取了200当前总共拥有4400元
Tom存了400当前总共拥有4800元
Tom取了200当前总共拥有4600元
Tom取了200当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
最后Tom总共拥有3000元
5.比较synchronized和Lock

1.用法不一样:在需要同步的对象上加入synchronized控制,synchronized既可以加在方法上,也可以加在特定代码块中。而Lock需要显示地指定起始位置和终止位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现的。

2.性能不一样:synchronized会随着竞争越来越激烈的情况下性能下降很快,而Lock性能基本保持不变。

3.锁机制不同:synchronized获得锁和释放的方式都是在块结构中,当获取多个锁的时候,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常导致锁没有释放从而引发死锁现象。而Lock需要手动释放,并且必须在finally块中释放,否则会产生死锁的发生。Lock还提供了更强大的功能,它的tryLock()方法可以采用非阻塞的方式去获取锁。

注意:两种锁不要一起使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值