多线程4 线程同步
并发:一个对象被多个线程所使用。
1 线程的不安全
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如两个线程存取同一个对象,并且每个线程分别调用了一个修改该对象状态的方法,会发生什么呢?可以想见,这两个线程会相互覆盖。取决于线程访问数据的次序,可能会导致对象被破坏。
经典的不安全案例有:
1、买票
2、银行取钱
3、线程内不安全集合
都是多个线程调用同一个对象造成的,这里就不赘述了。
2 同步
核心:队列和锁!
有两种机制可以防止并发访问代码块。
2.1 synchronized关键字
synchronized关键字会自动提供一个锁相关的“条件”,对于大多数显示锁的情况这种机制功能很强大,也很便利。(简单)
(1)、修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
就以买票举个例子:
//利用synchronized关键字,对对象加实例锁
private synchronized void buy() throws InterruptedException {
//方法体
}
(2)、修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
复制代码
synchronized void staic method() {
//业务代码
}
(3)、修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
复制代码
synchronized(this) {
//业务代码
}
简单总结一下:
synchronized
关键字加到 static
静态方法和 synchronized(class)
代码块上都是是给 Class 类上锁。
synchronized
关键字加到实例方法上是给对象实例上锁。
在用synchronized修饰方法时要注意以下几点:
synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
在子类方法中加上synchronized关键字
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { }
}
在子类方法中调用父类的同步方法
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
在定义接口方法时不能使用synchronized关键字。
构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
2.1.1 wait()和notify
Void wait():导致线程进入等待状态,直到它得到通知
Void notifyAll():解除对象中wait()方法对那些线程的阻塞状态.
例如:
public synchronized void transfer(int from, int to, double amount)
throws InterruptedException
{
while (accounts[from] < amount)
wait();
System.out.print(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();
}
2.1.1 以银行存取款为例子
/**
* A bank with a number of bank accounts that uses synchronization primitives.
*/
public class Bank
{
private final double[] accounts;
/**
* Constructs the bank.
* @param n the number of accounts
* @param initialBalance the initial balance for each account
*/
public Bank(int n, double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}
/**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public synchronized void transfer(int from, int to, double amount)
throws InterruptedException
{
while (accounts[from] < amount)
wait();
System.out.print(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();
}
/**
* Gets the sum of all account balances.
* @return the total balance
*/
public synchronized double getTotalBalance()
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
/**
* Gets the number of accounts in the bank.
* @return the number of accounts
*/
public int size()
{
return accounts.length;
}
}
2.2 ReentrantLock类
ReentrantLock类的锁称为重入锁。但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。
常用的格式如下:
public class LockTest {
private final Lock lock = new ReentrantLock();
public void add(int n) {
lock.lock();
try {
//循环块
} finally {
lock.unlock();
}
}
}
unlock操作必须在finnally语句里
2.2.1 条件对象
通常,线程进入临界区后却发现只有满足了某个条件之后它才能执行。可以使用一个件对象来管理那些已经获得了一个锁却不能做有用工作的线程。会介java库中条件对象的实现(由于历史原因,条件对象经常被称为条件变量(conditional variable))。
一般可写成:Condition sufficientFunds
,
sufficientFunds = bankLock.newCondition();
这样就只有当条件不满足的时候会调用
sufficientFund.await();
当前线程现在暂停,并放弃锁。这就允许另一个线程执行,我们希望它能增加账户余额。等待获得锁的线程和已经调用了avait方法的线程存在本质上的不同。一旦一个线程调
用了await方法,它就进人这个条件的等待集(wait set)。当锁可用时,该线程并不会变为可运行状态。实际上,它仍保持非活动状态,直到另一个线程在同一条件上调用signalAl1方法。
当另一个线程完成转账时,它应该调用
sufficientFunds.signalAll();
这个调用会重新激活等待这个条件的所有线程。当这些线程从等待集中移出时,它们再次成为可运行的线程,调度器最终将再次将它们激活。同时,它们会尝试重新进入该对象。一旦锁可用,它们中的某个线程将从await调用返回,得到这个锁,并从之前暂停的地方继续执行。
此时,线程应当再次测试条件。不能保证现在一定满足条件——signalAll方法仅仅是通知等待的线程:可能满足条件,应检查。
2.2.2 以银行存取款为例子
核心语句:
private Condition sufficientFunds;
bankLock = new ReentrantLock();
//返回与这个锁相关的条件对象
sufficientFunds = bankLock.newCondition();
bankLock.lock();
try
{
while (accounts[from] < amount)
//该线程放在这个条件的等待集中
sufficientFunds.await();
System.out.print(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());
//解除等待集中所有阻塞
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
全部代码:
import java.util.*;
import java.util.concurrent.locks.*;
/**
* A bank with a number of bank accounts that uses locks for serializing access.
*/
public class Bank
{
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
/**
* Constructs the bank.
* @param n the number of accounts
* @param initialBalance the initial balance for each account
*/
public Bank(int n, double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
/**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try
{
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(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());
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
/**
* Gets the sum of all account balances.
* @return the total balance
*/
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
/**
* Gets the number of accounts in the bank.
* @return the number of accounts
*/
public int size()
{
return accounts.length;
}
}
sufficientFunds.await();与synchronized关键字方法的wait();等价。
sufficientFunds.signalAll();synchronized关键字方法的notifyAll();等价。
3 锁和条件的总结
- 锁用来保护代码片段,一次只能有一个线程执行被保护的代码。
- 可以管理试图进入被保护代码段的线程。
- 一个锁可以有一个或多个相关联的条件对象。
- 每个条件对象管理那些已经进入被保护代码段但还不能运行的线程。