java多线程安全问题

当两个进程并发修改同一个文件时就有可能造成线程安全问题,为了解决这个问题java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的同步方法就是同步代码块、同步方法、Lock锁(同步锁)

1、同步代码块

synchronized(obj)
{
	...
	//此处的代码就是同步代码块
}

上面的语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
注意:任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视的锁定。
虽然java程序允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器

public class Account
{
//封装账户编号、账户余额两个属性
private String accountNo;
	private double balance;
	public Account(){}
	//构造器
	public Account(String accountNo,double balance)
	{
	this.accountNo = accountNo;
	this.balance = balance;
	}
	//此处省略accountNo和balance两个属性的set和get方法
	...
	//下面两个方法根据accountNo来计算Account的hashCode和判断equals
	public int hashCode()
	{
	return accountNo.hashCode();
	}
	public Boolean equals (Object obj)
	{
	 if(obj != null && obj.getClass() == Account.class){
	Account target = (Account)obj;
	return target.getAccountNo().equals(accountNo);
	}
	return false;
	}
}


public class DrawThread extends Thread
{
	//模拟用户账户
	private Account account;
	private double drawAmount'
	public DrawThread(String name,Account account,Double drawAmount)
	{
	super(name);
	this.account = account;
	this.drawAmount = drawAmount;
	}
	//当多条线程修改同一共享数据时,讲涉及数据安全问题
	public void run(){
	//使用account作为同步监视器,任何线程进入下面代码块之前,必须先获得对account账户的锁定--其他线程无法获得锁,也就无法修改它
	//这种做法符合:加锁-->修改完成-->释放锁逻辑
	synchronized (account)
	{
	//账户余额大于取钱数目
	if(account.getBalance() >= drawAmount)
	    {
		//吐出钞票
		System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
		try
		{
		Thread.sleep(1);
		}
		catch (InterruptedException ex)
		{
		ex.printStackTrace();
		}
		//修改余额
		account.setBalance(account.getBalance() - drawAmount);
		System.out.prinln("/t余额为:"+ account.getBalance());
	    }
	    else
	    {
	    System.out.prinln(getName() +"取钱失败!余额不足!");
	    }
	}
	//同步代码块结束,该线程释放同步锁
	}
}

public class TestDraw
{
	public static void main(String[] args)
	{
	//创建一个账户
	Account acct = new Account("123456",1000);
	//模拟两个线程对同一账户取钱
	new Thread("甲",acct,800).start();
	new Thread("乙",acct,800).start();
	}
}
上面程序使用synchronized讲run方法里方法体修改成同步代码块,该同步代码块的同步监视器account对象,这样的做法符合“加锁-->修改完成-->释放锁”逻辑,任何线程想修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一条线程可以进入修改共同资源的代码区(也被称为临界区),所以同一时刻最多只有一条线程处于临界区从而保证了线程的安全性。
2、同步方法
同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法对于同步方法而言,无需显示指定同步监视器,同步方法的同步监视器是this,也就是对象本身
可变对象需要额外的方法保证其线程安全,那么程序只要把修改的balance的方法修改成同步方法即可

public class Account
{
	private String accountNo;
	private double balance;
	public Account (){};
	public Account (String accountNo,double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}
	//此处省略了accountNo的set/get方法
	//因此账户余额不允许随便修改,所以取消balance属性的set方法

	public double getBalance()
	{
		return this.balance;
	}
	//提供一个线程安全draw方法来完成取钱操作
	public synchronized void draw(double drawAmount)
	{
		//账户余额大于取钱数目
		if(balance >= drawAmount)
		{
		//吐出钞票
			System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
			try
			{
			Thread.sleep(1);
			}
			catch (InterruptedException ex)
			{
			ex.printStackTrace();
			}
			//修改余额
			account.setBalance(account.getBalance() - drawAmount);
			System.out.prinln("/t余额为:"+ account.getBalance());
		  }
		  else
		  {
		   System.out.prinln(getName() +"取钱失败!余额不足!");
		  }
		}
		//此处省略了hashCode和equals两个重写的方法
	}
}

上面程序中增加了一个代表取钱的draw方法,并使用了synchronized关键字修饰修改该方法,把该方法变成同步方法。同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一条线程获得Account对象的锁定,然后进入draw()方法执行取钱操作--这样也可以保证多条线程取钱的线程安全
备注:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等
因为Account类中已经提供了draw方法,而且取消了setBalance()方法,DrawThread线程类需要改写,该类需要调用Account对象的draw方法来执行取钱操作

public class DrawThread extends Thread
{
	//模拟用户账户
	private Account account;
	//当前取钱线程所希望的钱数
	private double drawAmount;
	public DrawThread(String name,Account account,double drawAmount)
	{
	super(name);
	this.account = account;
	this.drawAmount = drawAmount;
	}
	public void run()
	{
	//直接调用account对象的draw方法来执行取钱
	account.draw(drawAmount);
	}
}
上面的DrawThread类无需自己实现取钱操作,而是直接调用account的draw方法来执行取钱。由于我们已经使用了synchronized关键字保证了draw方法的线程安全性,所以多条线程并发调用draw方法也不会出现问题
释放同步监视器的锁定
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显示释放对同步监视器的锁定,线程会在如下几种情况下释放同步监视器的锁定
-->当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
-->当线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器
-->当线程在同步代码块、同步方法中出现了未处理的Error和Exception,导致了该代码块、该方法异常结束时将会释放同步监视器
-->当线程执行同步代码块、或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器
-->当线程执行同步代码块、或同步方法时,程序调用Thread。sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器
-->当线程执行同步代码块,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。当然,我们应该避免使用suspend和resume方法来控制线程。
同步锁(Lock)
从JDK1.5之后,Java提供了另外一种线程同步机制:它通过显示定义同步锁对象来实现同步没在这种机制下同步锁应该使用Lock对象充当
通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且可以支持多个相关的Condition对象
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁,通常使用Lock对象的代码格式如下

class L
{
	//定义锁对象
	private final ReentrantLock lock = new ReentrantLock();
	//定义需要保证线程安全的方法
	public void m()
	{
		//加锁
		lock.lock();
		try
		{
			//需要保证线程安全的代码
			//...method body
		}
		//使用finally快来保证释放锁
		finally
		{
			lock.unlock();
		}
	}
}

使用Lock对象来进行同步时,锁定和释放锁出线在不同作用范围时,通常建议使用finally快来确保在必要时释放锁。通过使用Lock对象我们可以把Account类改为如下形式,它依然是线程安全的

public class Account
{
	//定义锁对象
	private final ReetrantLock lock = new ReentrantLock();
	private String accountNo;
	private double balance;
	public Account(){}
	public Account(String accountNo,double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}
	//此处省略了accountNo的get/set方法
	//因此账户余额不允许随便修改,所以取消balance属性的set方法
	public getBalance()
	{
		return this.balance;
	}
	//提供一个线程安全draw方法来完成取钱操作
	public void draw(double drawAmount)
	{
		//对同步锁进行锁定
		lock.lock();
		try
		{
		//账户余额大于取钱数目
			if(balance >= drawAmount)
			{
			//吐出钞票
				System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
				try
				{
				Thread.sleep(1);
				}
				catch (InterruptedException ex)
				{
				ex.printStackTrace();
				}
				//修改余额
				account.setBalance(account.getBalance() - drawAmount);
				System.out.prinln("/t余额为:"+ account.getBalance());
			  }
			  else
			  {
			   System.out.prinln(getName() +"取钱失败!余额不足!");
			  }
		}
		//使用finally块来保证释放锁
		finally
		{
			lock.unlock();
		}
	}
	//此处省略了hashCode和equals两个重写的方法
}
程序中实现draw方法时,进入方法开始后立即请求lock对象进行加锁,接着程序完全实现draw方法的draw方法的取钱逻辑之后,程序使用finally块来确保释放锁
使用Lock与使用同步方法有点相似,只是使用Lock时显示使用Lock对象作为同步锁,而使用同步方式时系统隐式使用当前对象作为同步监视器,同样符合“加锁->访问->释放锁”的操作模式,而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一条线程能进入临界区。
同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在同一块结构中,而且当获取了多个琐时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java中可以使用synchronized关键字来解决多线程安全问题。synchronized可以用在方法上或者代码块上,表示当前线程独占这段代码,其他线程在这段代码执行期间不能访问。另外Java还提供了ReentrantLock类来实现同步,这个类比synchronized更灵活,支持更多的功能。 ### 回答2: Java中提供了多种方式来解决多线程安全问题。 1. 使用synchronized关键字:通过在关键代码块或者方法前加上synchronized关键字,确保同一时间只有一个线程可以执行该代码块或者方法。这样可以有效地解决竞态条件和数据不一致的问题。 2. 使用ReentrantLock类:ReentrantLock是一种可重入的互斥锁,通过使用lock()和unlock()方法来实现线程间的互斥访问。与synchronized关键字相比,ReentrantLock类提供了更大的灵活性,例如可中断的锁、超时获取锁等。 3. 使用volatile关键字:当多个线程访问共享变量时,通过使用volatile关键字可以保证变量的可见性,即一个线程对变量的修改对其他线程是可见的。但是volatile关键字无法保证变量的操作的原子性。 4. 使用Atomic类:Java提供了一系列的原子类,例如AtomicInteger、AtomicLong等,这些原子类提供了比volatile关键字更强的原子性保证。通过使用这些原子类,可以避免使用synchronized关键字,提高多线程程序的性能。 5. 使用线程安全的数据结构:Java提供了一些线程安全的数据结构,例如ConcurrentHashMap、ConcurrentLinkedQueue等,对于多线程并发访问的场景,可以使用这些线程安全的数据结构来保证数据的一致性和线程安全性。 综上所述,Java提供了多种方式来解决多线程安全问题,开发者可以根据具体的需求和场景来选择合适的方式。 ### 回答3: Java提供了多种机制来解决多线程安全问题。 1. 同步方法:在方法声明前加上synchronized关键字,可以确保在同一时间只有一个线程可以进入方法。这样可以保证共享资源的状态一致性。 2. 同步代码块:使用synchronized关键字来标记代码块,只有获取到锁的线程才能执行该代码块中的代码。这样可以对特定的代码块进行同步控制,避免多线程同时对共享资源进行访问。 3. volatile关键字:使用volatile关键字修饰的变量,会保证对它的读写操作都是原子性的。对volatile变量的写操作会立即刷新到主内存,对volatile变量的读操作会从主内存中获取最新的值,避免了线程之间的可见性问题。 4. 使用锁:Java提供了多种锁机制,如ReentrantLock、ReadWriteLock等,它们可以用来实现更细粒度的同步控制。使用锁可以提供更高级的功能,如可重入、可中断、公平/非公平、读写分离等。 5. 使用并发容器:Java提供了一些并发安全的容器,如ConcurrentHashMap、ConcurrentLinkedQueue等,这些容器内部实现了线程安全的同步机制,可以在多线程环境下安全地访问和修改容器内的数据。 总的来说,Java通过synchronized关键字、volatile关键字、锁和并发容器等机制,提供了丰富的工具来解决多线程安全问题。开发人员可以根据具体需求选择适合的机制来保证多线程环境下的数据一致性和线程安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值