第一次老师讲线程安全的时候,举了一个例子:
从银行里取钱和往银行里存钱,用两个线程操作,最后结果不一致。(包括抢火车票的例子)
当时感觉非常神奇,工作以后常常回忆起来这个例子,但是总忘了怎么实现,现在做一个记录。供以后参考。
先看一个不安全的例子:
/**
* Created by linghui.wlh on 19/12/17.
*/
public class UnsafeBank{
int account = 10;
public static void main(String [] args){
UnsafeBank ub = new UnsafeBank();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 1000; i ++){
ub.deposite();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 1000; i ++){
ub.widthdraw();
}
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(ub.account);
}
//add account by one
public void deposite(){
this.account ++;
}
//minus account by one
public void widthdraw(){
this.account --;
}
}
运行结果:
再次运行:
可以看到虽然每次都是把账户加减相同次数,但是最后结果却不是10。
这里再啰嗦一种写法,也是一样的非线程安全的例子:
package com.wlh.demos.thread;
/**
* Created by linghui.wlh on 19/12/17.
*/
public class UnsafeBank2 {
int account = 10;
public static void main(String [] args){
UnsafeBank2 ub2 = new UnsafeBank2();
//两个线程持有同一个对象ub2
Thread t1 = new ThreadA(ub2);
Thread t2 = new ThreadB(ub2);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ub2.account);
}
public void deposite(){
this.account ++;
}
public void widthdraw(){
this.account --;
}
}
//存钱线程
class ThreadA extends Thread{
private UnsafeBank2 ub;
//构造函数,传入对象引用
public ThreadA(UnsafeBank2 ub){
this.ub = ub;
}
@Override
public void run(){
for(int i = 0; i < 100000; i ++){
ub.deposite();
}
}
}
//取钱线程
class ThreadB extends Thread{
private UnsafeBank2 ub;
//构造函数,传入对象引用
public ThreadB(UnsafeBank2 ub){
this.ub = ub;
}
@Override
public void run(){
for(int i = 0; i < 100000; i ++){
ub.widthdraw();
}
}
}
可以看到出现线程安全问题的原因就是deposite方法和widthdraw方法没有加线程控制。
每次都要写main方法来测试太麻烦,单独写一个Test类,把Bank对象抽出来。这样代码更简明清晰。
下面改造如下:
父类:Bank.java
子类:UnsafeBank.java
子类:SynchronizedBank.java
测试类:BankTester
具体代码:
package com.wlh.demos.thread.safe;
/**
* Created by linghui.wlh on 19/12/17.
* 所有bank类的父类
*/
public class Bank {
public int account = 10;
//add by one
public void deposite(){
this.account ++;
}
//minus by one
public void widthdraw(){
this.account --;
}
}
不安全银行:
package com.wlh.demos.thread.safe;
/**
* Created by linghui.wlh on 19/12/17.
*/
public class UnsafeBank extends Bank{
}
安全银行:
package com.wlh.demos.thread.safe;
/**
* Created by linghui.wlh on 19/12/17.
*/
public class SynchronizedBank extends Bank{
//thread safe add by one
public synchronized void deposite(){
this.account ++;
}
//thread safe minus by one
public synchronized void widthdraw(){
this.account --;
}
}
测试类:
package com.wlh.demos.thread.safe;
import com.wlh.demos.thread.unsafe.*;
/**
* Created by linghui.wlh on 19/12/17.
*/
public class BankTester {
public static void main(String [] args){
Bank bank = new SynchronizedBank();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100000; i ++){
bank.deposite();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100000; i ++){
bank.widthdraw();
}
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bank.account);
}
}
执行结果:
符合预期。
大家都知道synchronized执行效率比较低,那么还有另一种写法,使用atomicInteger:
package com.wlh.demos.thread.safe;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by linghui.wlh on 19/12/17.
*/
public class AtomicBank extends Bank {
//使用AtomicInteger
public AtomicInteger account = new AtomicInteger(10);
//add by one
public void deposite(){
this.account.getAndIncrement();
}
//minus by one
public void widthdraw(){
this.account.getAndDecrement();
}
}
然后修改BankTester
Bank bank = new AtomicBank();
运行结果也是10,符合预期。
还有一种方案,就是使用lock:
package com.wlh.demos.thread.safe;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by linghui.wlh on 19/12/17.
*/
public class LockBank extends Bank {
//add by one
public void deposite(){
Lock lock = new ReentrantLock();
try {
lock.lock();
this.account ++;
lock.unlock();
} finally{
}
}
//minus by one
public void widthdraw(){
Lock lock = new ReentrantLock();
try {
lock.lock();
this.account --;
lock.unlock();
} finally{
}
}
}
修改BankTester:
Bank bank = new LockBank();
运行结果:
还是不正确,有幸请教了青鲤大师,找到了问题所在。
原来这里定义了两个锁,这两个锁分别保证了各自方法执行时的可见性和原子性。相当于多个线程同时操作的有序性仍然无法保证。所以改造方法也很明了,就是用一个锁,而不是两个。
改造如下:
package com.wlh.demos.thread.safe;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by linghui.wlh on 19/12/17.
*/
public class LockBank extends Bank {
Lock lock = new ReentrantLock();
//add by one
public void deposite(){
try {
lock.lock();
this.account ++;
} finally{
lock.unlock();
}
}
//minus by one
public void widthdraw(){
try {
lock.lock();
this.account --;
} finally{
lock.unlock();
}
}
}
最后运行结果:
符合预期。
使用volatile关键字,并不能应对这种场景, 出来的结果仍然是不可预期,因为volatile只是保证了可见性,并不保证操作有序性,所以无法保证。volatile更多的应用在boolean值上面,他保证所有的修改都能够被子线程看到。
完整代码:
https://github.com/danielWLH/superior_demos.git
具体原因看参考文档。
参考文章放最前面:
http://www.importnew.com/18126.html讲volatile非常详细
https://www.cnblogs.com/hapjin/p/5492619.html 讲线程通信
https://www.cnblogs.com/-new/p/7190092.html 讲锁
http://www.cnblogs.com/-new/p/7326820.html synchronized ReentrantLock volatile Atomic 原理分析