前言
这是线程真正开始变得有意思的部分,各位学习前面的概念头昏脑胀了没?是时候来玩一玩了。线程同步是什么呢?
相当于一个很有意思的游戏,玩的好的随手编辑几句,性能杠杠的,众人惊为天人,纷纷膜拜(心中OS:哈哈哈,哥就是有实力!这么简单,随便玩一玩就OK);
玩不好的呢,看到就头疼:啊啥玩意,又死锁了 T T,为啥会死锁,我不知道啊,看我无辜的小眼神(●'◡'●)(怂.png,要不,我们重启?)
大神还是FIVE就看这部分学的好不好了,你准备好了么?
14.5 同步
线程的特点之一是内存共享。所以就会造成一个问题:同时修改怎么办?谁先谁后,这就是竞争条件。
14.5.1 竞争条件的一个例子
一个银行有若干账户。随机生成多笔账户间相互转账的交易,每笔交易中,都会从一个账户随机转移一定的金额到另一个账户。
A->B的转账100块操作:从A用户扣除100,B用户增加100
Bank.java
功能:两个账户从来源账户转指定金额到目标账户,并验证账户金额
🍹 建立指定数目的账户
🍹 完成从一个账户转账到另一个账户
🍹 计算所有账户总金额
🍹 获取指定的账户数目
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
public class Bank {
private final double[] accounts;
public Bank(int n, double initialBalance){
accounts = new double[n];
for(int i = 0; i < n; i++){
accounts[i] = initialBalance;
}
}
public void transfer(int from, int to, double amount){
if(accounts[from] < amount) return;
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());
}
private double getTotalBalance(){
double sum = 0;
for(double a: accounts){
sum += a;
}
return sum;
}
public int size(){
return accounts.length;
}
}
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
TransferRunnable.java
功能:当前账户到随机账户转账
🍹 确定来源账户和最大转账金额
🍹 完成一次从来源账户到随机账户的转账
🍹 睡眠随机时间,继续从来源账户到重新随机的账户转账
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
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;
}
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){}
}
}
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
UnsynchBankTest.java
功能:当前账户到随机账户转账
🍹 建立一个银行和100个账户
🍹 开100个线程代表不同的来源账户
🍹 不同的账户进行转账
🍹 10秒后关闭所有线程(因为除了主线程所有线程都开了守护,只要主线程结束都可以结束)
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args) throws InterruptedException {
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.setDaemon(true);
t.start();
}
Thread.sleep(10000);
}
}
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
运行结果:
部分计算正确,部分计算错误,中间有一些计算错误的总金额
🚗🚓🚕🛺🚙🚌🚐🚎🚑🚒🚚🚛🚜🚘🚔🚖🚍🦽🦼🛹🚲🛴🛵🏍
🚗🚓🚕🛺🚙🚌🚐🚎🚑🚒🚚🚛🚜🚘🚔🚖🚍🦽🦼🛹🚲🛴🛵🏍
14.5.2 详解竞争条件
为什么金额有时候是错误的呢?
1)accounts[to] += amount; 不是原子操作(对应于 JVM 指令不止一条),实际上这个加法操作对于计算机 JVM 指令来说,可没有那么简单,分为三步:
🍹 accounts[to] 放到寄存器
🍹 增加 amount
🍹 取出赋值给 accounts[to]
那就可能存在,某个线程执行了前面的 1 到 2 步,另一个线程开始执行,完成了三步,那么再回到第一个线程进行赋值的时候,金额就不正确了
2)第一个线程执行完减法操作,被剥夺运行权,第二个线程执行完减法操作,总金额变少,进行输出,因为前面加法的操作没有执行完,导致总金额减少
这个问题有多严重
1)银行的钱可能变多,用户的钱变少,用户骂银行扣钱,放弃使用银行,导致银行倒闭
2)银行的钱变少,用户的钱变多,因为银行的钱变少,钱总是不够用,银行倒闭
怎么解决线程竞争问题?
保证在无法控制的部分,强制此部分代码必须执行完成,才能切换下一个线程,这就需要用到线程的究极大招:锁
14.5.3 锁对象
为了保证线程无法控制的部分,能够顺利独立、连续的完成,就要用到锁。ReentrantLock 是一种常见的锁,它的用法特别简单:
锁定:myLock.lock();
解锁:myLock.unlock();
在我们的银行加个锁:
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
public class Bank {
private final double[] accounts;
private Lock bankLock = new ReentrantLock();
public Bank(int n, double initialBalance){
accounts = new double[n];
for(int i = 0; i < n; i++){
accounts[i] = initialBalance;
}
}
public void transfer(int from, int to, double amount){
bankLock.lock();
if(accounts[from] < amount) return;
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());
bankLock.unlock();
}
private double getTotalBalance(){
double sum = 0;
for(double a: accounts){
sum += a;
}
return sum;
}
public int size(){
return accounts.length;
}
}
❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤❤🧡💛💚💙💜🤎🖤
运行结果:
🍹 跑的速度变慢了
🍹 跑的结果一直正确
🚗🚓🚕🛺🚙🚌🚐🚎🚑🚒🚚🚛🚜🚘🚔🚖🚍🦽🦼🛹🚲🛴🛵🏍
🚗🚓🚕🛺🚙🚌🚐🚎🚑🚒🚚🚛🚜🚘🚔🚖🚍🦽🦼🛹🚲🛴🛵🏍
这段lock()/unlock()之间的代码和公厕是一样的。一个人上大号进去以后把门锁了,后续的人要进同一个厕所就需要等待前面的人出来才能进厕所。
可重入锁:一个线程可以重复获得多次锁。相当于一个门装了10个锁,那就要10把不同的钥匙把这些锁都打开,才可以解锁。
所以刚刚这段代码锁两次解两次也是可以的
后记
其实大概看了下,感觉这部分无论是概念方面还是生命周期方面,还是例子都不是特别全,但是没看完不确定缺了哪些(其实基础篇好像很少有特别全的,编程思想的比较全,但是难懂)。等看完了,我再看一遍线程的知识点给大家补充,不要着急。
相关内容:选择 《Java核心技术 卷1》查找相关笔记
评论🌹点赞👍收藏✨关注👀,是送给作者最好的礼物,愿我们共同学习,一起进步
公众号 钰娘娘知识汇总