SE高阶(5):多线程—②线程同步、死锁、volatile关键字

本文深入探讨Java中线程同步的重要性和实现方式,包括synchronized关键字、Lock接口及其实现类ReentrantLock等,同时提供了丰富的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程同步的作用

在多线程中,当两个及以上线程并发访问同个资源时,尽管有控制线程的方法,但由于线程调度具有不确定性,所以极容易导致错误,这时就需要线程同步机制来解决此问题。


实现线程同步——同步监视器

Java中,任何对象都能作为同步监视器。同步监视器是为了确保一个线程在对对象操作时,别的线程无权访问,即任何时刻只能有一个线程对同步监视器锁定。所以一般把并发访问的共享资源作为同步监视器。
在Java中,使用关键字synchronized或者Lock类来对同步监视器锁定。
synchronized的用法
  • 同步代码块:synchronized(obj){},传入的obj对象就是同步监视器,在执行同步代码块中的代码之前,必须先获得对同步监视器的锁定。
  • synchronized修饰方法:同步方法的监视器是this,所以调用该方法的对象就是同步监视器,则无需显式指定。
Lock的用法
  • 有ReentrantLock(可重入锁)、ReentrantReadWriteLock(可重入读写锁)、以及Java8新增的StampedLock类。
  • Lock对象相当于同步监视器对象,并发访问时,每次只能允许一个线程对Lock对象加锁,执行完之后必须手动释放锁,释放锁方法一定要放在finally语句中。
  • 另外ReentrantReadWriteLock(提供了三种读写操作模式,但不常用。

线程同步实例

先给上一段多线程实现的取钱代码,没有采用线程同步,看看会出现什么情况。

public class Account {
	private int balance;//余额
	public Account(int balance) {
		this.balance = balance;
		System.out.println("账户当前余额:" + balance);
	}	
	//取钱方法
	public void quqian(int money) {
		if(money <= balance) {
			System.out.println(Thread.currentThread().getName() + ",取出" + money);
			//睡眠线程2s,用于测试另一个线程
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {e.printStackTrace();}							
			//修改余额的代码
			this.balance -= money;
			System.out.println(Thread.currentThread().getName()+",余额:" + this.balance);
		}else {
			System.out.println(Thread.currentThread().getName() + "取钱失败!");
		}		
	}
}	
//执行存钱操作的Runnable线程
class QqPerson implements Runnable{
	private int money;
	private Account ac;
	public QqPerson(Account ac,int money) {
		this.ac = ac;
		this.money = money;
	}
	@Override
	public void run() {
		ac.quqian(money);		
	}
}
public class Test {
	public static void main(String[] args) {
		Account ac = new Account(1000);
		//传入Account对象、取钱金额
		QqPerson q1 = new QqPerson(ac,600);
		QqPerson q2 = new QqPerson(ac,600);
		//线程类传入Runnable对象
		Thread t1 = new Thread(q1,"取钱1号");
		Thread t2 = new Thread(q2,"取钱2号");
		//启动线程
		t1.start();
		t2.start();
	}
}
  • 实例解析:在执行修改余额代码之前,调用sleep(2000)强制让线程睡眠2s,这样能更好地体现多个线程访问共享资源时,由于线程调度的不确定性导致的线程安全问题。事实上,如果不加sleep(),就会出现一会正确一会错误的情况。遇到这种问题,就需要使用线程同步来解决。

使用同步代码块实例
//执行存钱操作的线程
class QqPerson implements Runnable{
	private int money;
	private Account ac;
	public QqPerson(Account ac,int money) {
		this.ac = ac;
		this.money = money;
	}
	@Override
	public void run() {
		//加入同步代码块
		synchronized(ac){
			ac.quqian(money);
		}
	}
}
  • 实例解析:其余的代码都是相同的,只是把并发访问共享资源的执行代码放入同步代码块中,在代码中,依然使用sleep()来睡眠线程,但是会发现别的线程无法执行,这是因为同步锁还未被释放,执行完后释放锁,别的线程才能获取同步锁。
使用同步方法实例
	//取钱方法
	public synchronized void quqian(int money) {
		if(money <= balance) {
			System.out.println(Thread.currentThread().getName() + ",取出" + money);
			//睡眠线程1s,用于测试另一个线程
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {e.printStackTrace();}							
			//修改余额的代码
			this.balance -= money;
			System.out.println(Thread.currentThread().getName()+",余额:" + this.balance);
		}else {
			System.out.println(Thread.currentThread().getName() + "取钱失败!");
		}		
	}
  • 实例解析:因为并发访问共享资源的执行代码是由一个方法来完成,所以可以用synchronized修饰该方法。加锁原理和同步代码块一样,只是无需显式加入同步锁对象。
  • 加锁流程:第一个线程执行取钱方法,先获得同步锁对象,然后执行方法内部代码,执行到sleep()会睡眠当前线程,但因为睡眠线程获得了锁对象,所以别的线程此时无权访问,睡眠时间到了之后,又开始继续执行,直到代码执行结束,然后释放锁。下一个线程开始执行,又继续前面的步骤。
使用Lock加锁实例
public class Account {
	private int balance;//余额
	ReentrantLock lock = new ReentrantLock();
	StampedLock slock = new StampedLock();
	
	public Account(int balance) {
		this.balance = balance;
		System.out.println("账户当前余额:" + balance);
	}
	//取钱方法
	public  void quqian(int money) {
		try {
			lock.lock();//加锁
			if(money <= balance) {
				System.out.println(Thread.currentThread().getName() + ",取出" + money);												
				//修改余额的代码
				this.balance -= money;
				System.out.println(Thread.currentThread().getName()+",余额:" + this.balance);
			}else {
				System.out.println(Thread.currentThread().getName() + "取钱失败!");
			}
		}finally {
			lock.unlock();//手动释放锁
		}
	}
}	
  • 实例解析:上述代码使用了ReentrantLock(可重入锁)来实现线程同步。Lock对象代表了并发访问的同步监视器的领域,以Lock对象作为显式的同步锁对象。使用Lock加锁,一定要手动释放锁。

线程同步注意点:

  • 线程持有同步锁对象期间,使用sleep()不会释放锁。但如果使用wait()会导致释放同步锁。
  • sleep()的调用者是当前线程,而wait()的调用者是对象,所以wait()会导致锁被释放。
  • 同步代码块需要显式传入同步锁对象,同步方法无需同步锁对象,因为隐式传入。
  • Lock对象相当于隐式传入的同步锁对象,所以使用时要保证该同步锁对象是访问共享资源的调用对象。
  • Lock可以嵌套加锁,即在Lock加锁的代码中能执行另一段加锁的代码。

线程死锁

线程同步时,两个及以上线程相互持有同步锁对象,这就导致无法对同步监视器对象进行锁定,造成所有线程都进入阻塞状态,这就是死锁。出现死锁不会出现异常,不会有任何提示。如果通过eclipse的控制台来查看,看起来程序是结束了,但红正方形的出现说明了程序是运行的,只是一直在等待锁的释放。

关键字volatile——用于修饰变量

volatile:易变、不稳定的。
volatile使用场景:
如果只是读写一两个实例变量就使用线程同步,则开销会很大,volatile关键字能为实例变量的同步访问提供了一种免锁机制。用于在多线程中同步公共变量,保证用户在操作一个变量,但不保证多线程的原子性。
原子性解释:
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。
变量使用同步的条件:
如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值