(五) 同步

本文探讨了多线程环境下共享资源访问的竞争条件问题,并详细介绍了如何使用锁对象(如synchronized关键字和ReentrantLock)来确保数据一致性。此外,还讨论了条件对象的使用方法及其在同步机制中的作用。
摘要由CSDN通过智能技术生成

多数实际的多线程应用中,两个或两个以上的线程需要共享同一数据的存取。
1.竞争条件的一个例子

public class Bank {

	private final double[] accounts;

	public Bank(int n, double initialBalance) {
		accounts = new double[n];
		for (int i = 0; i < accounts.length; i++) {
			accounts[i] = initialBalance;
		}
	}

	public void transfer(int from, int to, double amount) {
		if (accounts[from] < amount) {
			return;
		}
		System.out.println(Thread.currentThread());
		// 这里需要加锁
		accounts[from] -= amount;
		System.out.printf("%10.2f from %d to %d", amount, from, to);
		/**
		 *  这里需要加锁
		 *  (1)将accounts[to]加载到寄存器
		 *  (2)增加amount
		 *  (3)将结果写回accounts[to]
		 */
		accounts[to] += amount;
		System.out.printf("Total Balance:%10.2f%n", getTotalBalance());
	}

	private double getTotalBalance() {
		double sum = 0;
		for (double a : accounts) {
			sum += a;
		}
		return sum;
	}

	public int size() {
		return accounts.length;
	}
}

 

public class TransferRunnable implements Runnable{
	
	private Bank bank;
	private int fromAccount;
	private double maxAmount;
	private int DELAY = 10;
	
	public TransferRunnable(Bank b, int from, double max){
		bank = b;
		fromAccount = from;
		maxAmount = max;
	}
	
	@Override
	public void run(){
		try{
			while(true){
				int toAccount = (int)(bank.size() * Math.random());
				double amount = maxAmount * Math.random();
				bank.transfer(fromAccount, toAccount, amount);
				Thread.sleep((int)(DELAY * Math.random()));
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
}

 

public class UnsynchBankTest {
	
	public static final int NACCOUNTS = 100;
	public static final double INITIAL_BALANCE = 1000;
	
	public static void main(String[] args) {
		Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
		int i;
		for(i=0;i<NACCOUNTS;i++){
			TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
			Thread t = new Thread(r);
			t.start();
		}
	}
}

 

2.详解竞争条件
当两个线程试图同时更新同一个账户的时候,可能会发生两个线程同时执行 accounts[to] += amount;
问题在于这不是原子操作,该指令可能被处理如下:
(1)将accounts[to]加载到寄存器
(2)增加amount
(3)将结果写回accounts[to]
假定第1个线程执行步骤1和2,然后它被剥夺了运行权,假定第2个线程被唤醒并修改了account数组中的同一项。然后,第1个线程被唤醒并完成其第3步。这样一动作擦去了第二个线程所做的更新。如是总金额不在正确。
真正的问题在于transfer方法的执行过程中可能会被中断,如果能确保线程在失去控制之前方法运行完成,那么银行账户对象的状态永远不会讹误。

 

 

 

 

 

 

3.锁对象

synchronized : 自动提供一个锁以及相关的“条件”
Reentrantlock : 保护代码块
java.util.concurrent : 为锁机制提供独立的类

 


Reentrantlock基本结构

myLock.lock();
try{
    critical section
}
finally{
    myLock.unlock();
}

 
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他的线程都无法通过Lock语句。当其他线程调用Lock时,它们被阻塞。直到第一个线程释放锁对象。

 


e.g.通过使用锁来保护Bank类的transfer方法

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockBank {
	
	private final double[] accounts;
	private Lock bankLock = new ReentrantLock();
	
	public LockBank(int n, double initialBalance){
		accounts = new double[n];
		for(int i = 0; i < accounts.length; i++){
			accounts[i] = initialBalance;
		}
	}
	
	public void transfer(int from, int to, double amount){
		bankLock.lock();
		try{
			if(accounts[from] < amount){
				return;
			}
			System.out.println(Thread.currentThread());
			accounts[from] -=amount;
			System.out.printf("%10.2f from %d to %d", amount, from, to);
			accounts[to] += amount;
			System.out.printf("Total Balance:%10.2f%n", getTotalBalance());
		}finally{
			bankLock.unlock();
		}
		
	}

	private double getTotalBalance() {
		bankLock.lock();
		try{
			double sum = 0;
			for(double a:accounts){
				sum += a;
			}
			return sum;
		}finally{
			bankLock.unlock();
		}
		
	}
	
	public int size(){
		return accounts.length;
	}
}

 

注意:
(1)每个Bank对象有自己的ReentrantLock对象。即:如果两个线程访问同一个Bank对象,那么锁以串行的方式提供服务。但是两个线程访问不同的Bank对象,每个线程得到不同的锁对象。
(2)持有计数 : int ReentrantLock.sync.state
    锁是可以重入的,因为线程可以重复的获得已经持有的锁。锁保持一个持有计数器(hold count)来跟踪跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
    e.g. transfer方法调用getTotalBalance方法,这会再次封锁bankLock对象,此时bankLock对象的持有计数为2。当getTotalBalance方法退出时,持有计数变回1。当transfer方法退出时,持有计数变为0。线程锁释放。
(3)可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完成后,另一个线程才能使用相同对象。

 

ReentrantLock()和ReentrantLock(boolean fair) : fair - 如果此锁应该使用公平的排序策略,则该参数为 true
即,当fair==true时创建的是公平锁。一个公平锁偏爱等待时间最长的线程,这一公平保证将大大降低性能,默认情况下,锁没有被强制为公平的。
注意:1.公平锁比使用常规锁要慢很多。2.即使使用公平锁,也无法确保线程调度器是公平的。

 

4.条件对象

由于历史的原因,条件对象经常被称为条件变量(conditional variable)

Condition : 获得一个条件对象 一个锁对象可以有一个或多个相关的条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。
方法 :
void await() : 造成当前线程在接到信号或被中断之前一直处于等待状态。
void signal() : 唤醒一个等待线程。
void signalAll() : 唤醒所有等待线程。

1.等待获得锁的线程和调用await方法的线程存在本质上得不同。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反它处于阻塞状态,直到另一个线程调用到同一个条件的signalAll方法为止。
2.通常对await的调用应放在while语句中,而不是if


final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
while(!(ok to proceed)){
   condition.await();
}
 

3.调用signalAll,从经验上讲,对象的状态有利于等待线程的方向改变时调用signalAll。比如上一个拿到锁的线程在释放锁之前。
4.signalAll不会立即激活一个等待线程,它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
5.signal方法是随机解除等待集中某个线程的阻塞状态,比解除所有线程效率要高,但是如果随机的线程仍不能运行,并再次被阻塞,而且没有其他线程调用signal,那么系统就死锁了。
6.当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await、signal或signalAll方法。
7.使用条件对象会有性能上的牺牲,这是为同步机制中的簿记操作所付出的代价。
8.开始实现自己的条件之前,应该考虑“同步器”中描述的结构。

 

5. synchronized
锁和条件的关键之处
(1)锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
(2)锁可以管理试图进入被保护代码段的线程。
(3)锁可以拥有一个或多个相关的条件对象。
(4)每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

Java中每个对象都有一个内部锁,用synchronized关键字声明方法,那么对象的锁将保护整个方法。

public synchronized void methon(){
    method body
}

 


等价于

public void method(){
    this.intrinsicLock.lock();
    try{
        method body
    }finally{
        this.intrinsicLock.unlock();
    }
}

 


内部锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。

public synchronized void transfer(int from, int to, double amount) throws InterruptedException{
        while(accounts[from] < amount){
            wait();
        }
        System.out.println(Thread.currentThread());
        accounts[from] -=amount;
        System.out.printf("%10.2f from %d to %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf("Total Balance:%10.2f%n", getTotalBalance());
        notifyAll();
}


注意:
1. wait、notify、notifyAll是Object类的final方法。Condition方法必须命名为await、signal、signalAll以便它们不会与那些方法发生冲突。
2. 将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关类的对象的内部锁。因为,没有其他线程可以调用一个类的这个或任何其他的静态方法。

内部锁和条件的局限性
(1)不能中断一个正在试图获得锁的线程
(2)试图获得锁时不能设定超时
(3)每个锁仅有一个单一条件,可能是不够的
结论:最好既不使用Lock/Condition也不使用synchronized关键字。应该使用java.util.concurrent包中的 阻塞队列机制。

对于Lock/Condition机制 :
(1) 如果synchronized关键字适用与程序,则尽量使用它。减少编写代码数,减少出错的几率。
(2) 如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition

6. 同步阻塞
通过进入一个同步阻塞,进程获得object的锁

synchrodized(object){
    critical section
}

 

如果使用如下方式,lock被创建仅仅是用来使用每个JAVA对象持有的锁。

synchronized(lock){
    critical section
}

 
关于客户端锁定(client-side locking)。
客户端锁定是非常脆弱的,通常不推荐使用
例如:考虑Vector类,它的方法(get/set方法)是同步的。现在假定在Vector<Double>中存取银行余额
public void transfer(Vector<Double> account, int from, int to, int amout){        //Error
    accounts.set(from, account.get(from) - amount);
    accounts.set(to, account.get(to) + amount);
}
这是错的,在第一次使用set方法后,线程可能在transfer中被剥夺运行权。

7.监视器的概念
(1) 监视器是只包含私有域的类。
(2) 每个监视器类的对象有一个相关的锁。
(3) 使用该锁对所有方法进行加锁。及调用obj.method()时,obj对象的锁在方法调用开始时自动获得的,并且方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。
(4) 该锁可以有任意多个相关条件。

在Java中的实现 : Java中每个对象有一个内部锁。如果一个方法用synchronized方法声明,表示就是一个监视器方法,通过调用wait/notify/notifyAll来访问条件变量。
事实上Java与监视器的不同点导致线程安全性下降 : 域不要求必须是private;方法不要求必须是synchronized;内部锁对客户是可用的。

8.Volatile域
volatile关键字为实例域的同步访问提供了一种免责机制,如果声明一个域为volatile,那么编译器和虚拟机就知道该线程是可能被另一个线程并发更新的。
e.g. private volatile boolean done;
volatile变量不能提供原子性,例如,方法
public void flipDone(){done = !done}     //不能确保改变域中的值。

 


9.死锁
1.得到锁的线程,在未释放线程时出现异常,没有释放锁。
2.释放锁的线程调用signal或notify,只对一个线程进行解锁,并且该线程不能进行运行(比如调用了wait/await)
Java中没有任何办法可以避免或打破这种死锁。

10.锁测试与超时
因为使用Lock可能导致死锁,所以应该更加谨慎的申请锁,tryLock方法试图申请一个锁,在成功之后返回true,否则立即返回false。
e.g.
if(myLock.tryLock()){
    //now the thread owns the lock
    try{
        ... ...
    }finally{
        myLock.unlock();
    }
}
调用tryLock时,可以使用超时参数
e.g. myLock.tryLock(100, TimeUnit.MILLISECONDS)
TimeUnit是一个枚举类型,取值包括SECONDS(秒)、MILLISECONDS(毫秒)、MICROSECOND(微妙)和NANOSECONDS(纳秒)
lock和tryLock的比较
lock : lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么lock方法就无法终止。
tryLock : 调用带有用超时参数的tryLock,如果线程在等待期间期间被中断,将抛出InterruptedException异常。该特性允许程序打破死锁。

lockInterruptibly方法 : 相当于一个超时设置为无限的tryLock方法。

在等待一个条件时,也可以提供一个超时,如果一个线程被另一个线程通过调用signalAll或者signal激活,或者超时时限到了,或者线程被中断,那么await方法将返回。如果等待的线程被中断,await
e.g. myCondition.await(1000, TimeUnit.MILLISECONDS)

如果等待的线程被中断,await方法将抛出一个InterruptedException异常,如果希望在这种情况下线程继续等待(可能不太合理),可以使用awaitUninterruptibly方法代替await。

11.读/写锁
ReentrantReadWriteLock类,如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,读写锁是十分有用的。在这种情况下,允许对读者线程共享访问是合理的。读者线程与写者线程是互斥的。
使用读/写锁的必要步骤:
(1)构造一个ReentrantReadWriteLock对象
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
(2)抽取读写锁
private Lock readLock = rwLock.readLock();
private Lock writeLock = rwLock.writeLock();
(3)对所有的访问者加读锁
public double getTotalBalance(){
    readLock.lock();
    try{
        ... ...
    }finally{
        readLock.unlock();
    }
}
(4)对所有的修改者加读写锁
public void transfer(... ...){
    writeLock.lock();
    try{
        ... ...
    }finally{
        writeLock.unlock();
    }
}
注意:ReentrantReadWriteLock没有实现Lock接口,而是实现的ReadWriteLock接口

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值