多线程操作一个变量示例

第一次老师讲线程安全的时候,举了一个例子:

从银行里取钱和往银行里存钱,用两个线程操作,最后结果不一致。(包括抢火车票的例子)

当时感觉非常神奇,工作以后常常回忆起来这个例子,但是总忘了怎么实现,现在做一个记录。供以后参考。

先看一个不安全的例子: 

/**
 * 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 --;
    }
}

运行结果:

163438_XAEk_3755458.png

再次运行:

163458_raP1_3755458.png

可以看到虽然每次都是把账户加减相同次数,但是最后结果却不是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);
    }
}

执行结果:

170459_NvRe_3755458.png

符合预期。

 

大家都知道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();

运行结果:

095947_JlYO_3755458.png

还是不正确,有幸请教了青鲤大师,找到了问题所在。

原来这里定义了两个锁,这两个锁分别保证了各自方法执行时的可见性和原子性。相当于多个线程同时操作的有序性仍然无法保证。所以改造方法也很明了,就是用一个锁,而不是两个。

改造如下:

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();
        }
    }
}

最后运行结果:

100700_Wcv4_3755458.png

符合预期。

 

使用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 原理分析

转载于:https://my.oschina.net/u/3755458/blog/1592223

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值