同步是什么?
当两个线程同时对一个变量进行修改时,不同的访问顺序会造成不一样的结果,这时候就需要同步保证结果的唯一性。
未同步时
新建Bank类,transfer()用于在两个账户之间转账金额
class Bank {
private double[] accounts;
public Bank(int accountNum, double initialMoney) {
accounts = new double[accountNum];
Arrays.fill(accounts, initialMoney);
}
public void transfer(int from, int to, double money) {
if (accounts[from] < money) {
return;
}
System.out.print(Thread.currentThread());
accounts[from] -= money;
System.out.printf("%10.2f from %d to %d", money, from, to);
accounts[to] += money;
System.out.printf(" Total %10.2f%n", getTotal());
}
public double getTotal() {
double total = 0.0d;
for (double temp : accounts) {
total += temp;
}
return total;
}
public int size() {
return accounts.length;
}
}
假设银行有1000个开户人,每个人账户有1000元,新建1000个线程进行随机转账,无论怎么转账,总金额都应该为1000000,但实际的钱却越来越少(这个例子不太好,原因是浮点数加减可能误差)
int NACCOUNTS = 1000;
double INITIAL_BALANCE = 1000;
double MAX_AMOUNT = 1000;
int DELAY = 10;
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double money = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, money);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
原因分析:
- 当两个线程同时执行到accounts[to] += money;
- 线程1取出accounts[to](如900)放到寄存器,+money(如100),正准备写回accounts[to](变成了1000)时
- 线程2抢占到权限执行accounts[to] += money 将accounts[to]修改为了10002
- 线程1再写就将10002覆盖成了1000
ReentrantLock
修改Bank,对其transfer方法加锁,注意lock应该为成员变量,即每个Bank实例都只有一把锁,不同对象之间的锁不会互相影响,需要在finally释放锁
class Bank {
private double[] accounts;
private ReentrantLock lock = new ReentrantLock();
public Bank(int accountNum, double initialMoney) {
accounts = new double[accountNum];
Arrays.fill(accounts, initialMoney);
}
public void transfer(int from, int to, double money) {
if (accounts[from] < money) {
return;
}
lock.lock();
try {
System.out.print(Thread.currentThread());
accounts[from] -= money;
System.out.printf("%10.2f from %d to %d", money, from, to);
accounts[to] += money;
System.out.printf(" Total %10.2f%n", getTotal());
} finally {
lock.unlock();
}
}
public double getTotal() {
double total = 0.0d;
for (double temp : accounts) {
total += temp;
}
return total;
}
public int size() {
return accounts.length;
}
}
Condition
在转账时,应该避免选择没有足够资金的账号转出
if(bank.getMoney(fromAccount) >= money){
bank.transfer(fromAccount, toAccount, money);
}
但在多线程情况下,可能两个线程同时进入if,一个线程转账后导致另外一个线程金额不够转账,故我们要确保在检查之后加锁禁止别的线程先行一步
private ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
while(account[from] < money){
}
}finally{
lock.unlock();
}
但当账户没有钱的时候,转出线程会一直等待其他线程转入资金,而其他线程因为无法拿到锁而无法转入,这就造成了死锁,这时候就需要条件对象,当金额不足时阻塞线程放弃锁的持有
private ReentrantLock lock = new ReentrantLock();
private Condition moneyEnough;
lock.lock();
try {
moneyEnough = lock.newCondition();
while(account[from] < money){
moneyEnough.await();
}
}finally{
lock.unlock();
}
当其他线程转账后,应该调用signalAll()唤起所有await()中的线程,当其中的某个线程被调度并再次获取锁后,会再进入try子句检测金额是否足够
moneyEnough.signalAll();
Tips:
- 还有一个signal()方法随机唤起一个等待线程
- 当所有线程的金额都小于转账金额,调用await(),所有线程都会阻塞,此时会再次死锁
synchronized
Java中每一个对象都有一个内部锁,如果一个方法用synchronized声明,线程调用该方法时需要获得其内部锁,即
public synchronized void method(){
}
等价于
private ReentrantLock innerLock = new ReentrantLock();
public void method(){
innerLock.lock();
try{
}finally{
innerLock.unlock();
}
}
内部锁只有一个条件 ,对其的阻塞和唤醒调用wait()、notifyAll()/notify(),它们是Object中的final方法,相当对ReentrantLock调用
innerLock.await();
innerLock.signalAll();
将静态方法声明为synchronized,调用该方法时会锁住对应的类,此时其他线程无法调用该类的其他同步静态方法
Tips:
- 内部锁不能中断一个正在试图获得锁的线程
- 锁时不能设置超时
- 只有一个条件
该使用哪种锁机制?
使用synchronized可减少代码的编写,减少出错的几率
使用ReentrantLock+Condition可以自行控制锁的过程,实现多个条件
同步阻塞
使用其他对象的锁来完成原子操作
public class bank{
private Object lock = new Object();
public void transfer(int from, int to, double money) {
synchronized(lock){
}
}
}
监视器
volatile
若如果只有一两个域可能发生多线程的误写,可对该域声明为volatile,虚拟机和编译器就知道该域是可能被另一个线程并发更新的,但其不能保证原子性
boolean flag;
public void Not(){
flag = !flag;
}
如上,不能保证其再读取、翻转和写入时不被中断
final和锁
当把域声明为final时,其他线程对其的读取只能是构造成功后的值,而不会是null
fial Map<String, Double> accounts = new HashMap<>();
原子性
死锁
线程局部变量
当多个线程都要调用Random中的方法生成随机数时,由于Random是加锁的,其他线程就得等待,此时可用TheadLocal辅助类为各个线程提供各自的Random实例
ThreadLocal<Random> threadLocal = ThreadLocal.withInitial(() -> new Random());
threadLocal.get().nextInt();
此外,专门创建多线程随机数的ThreadLocalRandom,其current()方法会返回当前线程的Random类实例
ThreadLocalRandom.current().nextInt();
锁超时
当线程调用tryLock()方法去申请另一个线程的锁时,很有可能发生阻塞,故可在申请时设置时长
private ReentrantLock lock = new ReentrantLock();
try {
lock.tryLock(100, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
读写锁
读写锁可从ReentrantReadWriteLock取出,为所有获取方法加上读锁,为所有修改方法加上写锁
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
Lock readLock = reentrantReadWriteLock.readLock();
Lock writeLock = reentrantReadWriteLock.writeLock();
为什么弃用stop()和suspend()
stop()方法用来终止线程,并立即释放线程所获得的锁,这会导致对象状态不一致(如钱已被转出,但在转入前stop,会导致数据丢失)
故无法确定什么时候调用stop()是安全的,在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止
suspend()方法用来阻塞线程,直至另一个线程调用resume(),当用suspend()挂起一个持有一个锁的线程,则该锁在resume()之前是不可用的。
若此时再用suspend()方法的线程获取该锁,则会死锁,被挂起的锁等待resume()释放,而要resume()则要获取被挂起的锁