Thread详解5:synchronized的使用(一)

1 为什么要使用synchronized

我们首先编写一个非常简单的多线程的程序,是模拟银行中的多个线程同时对同一个储蓄账户进行存款、取款操作的。
在程序中我们使用了一个简化版本的Account类,代表了一个银行账户的信息。在主程序中我们首先生成了1000个线程,然后启动它们,每一个线程都对John的账户进行存100元,然后马上又取出100元。这样,对于John的账户来说,最终账户的余额应该是还是1000元才对。然而运行的结果却超出我们的想像,首先来看看我们的演示代码:

Account.java

package medium;

class Account {
    String name;
    float amount;

    public Account(String name, float amount) {
        this.name = name;
        this.amount = amount;
    }

    /**
     * 存钱
     * 之所以要把对amount的运算使用一个临时变量首先存储,sleep一段时间,然后,再赋值给amount,
     * 是为了模拟真实运行时的情况。因为在真实系统中,账户信息肯定是存储在持久媒介中,
     * 比如RDBMS中,此处的睡眠的时间相当于比较耗时的数据库操作,
     * 最后把临时变量tmp的值赋值给amount相当于把amount的改动写入数据库中。
     * */
    public void deposit(float amt) {
        float tmp = amount;
        tmp += amt;

        try {
            Thread.sleep(100);// 模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        amount = tmp;
    }

    /**
     * 取钱
     * */
    public void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(100);// 模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        amount = tmp;
    }

    /**
     * 查询账户余额
     * */
    public float getBalance() {
        return amount;
    }
}

AccountTest.java

package medium;

public class AccountTest {

    // 线程的数量
    private static int NUM_OF_THREAD = 8888;
    static Thread[] threads = new Thread[NUM_OF_THREAD];

    public static void main(String[] args) {
        // 账户名为 John,里面有1000元
        final Account acc = new Account("John", 1000.0f);
        // 用for循环新建 NUM_OF_THREAD 个线程,每个线程都是一新建就立刻启动,然后run都是存100再取100
        for (int i = 0; i < NUM_OF_THREAD; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                    acc.deposit(100.0f);
                    acc.withdraw(100.0f);
                }
            });
            threads[i].start();
        }

        // 主线程等 NUM_OF_THREAD 个线程全部运行完才去查询余额,我们期待的是1000
        for (int i = 0; i < NUM_OF_THREAD; i++) {
            try {
                threads[i].join(); // 等待所有线程运行结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Finally, John's balance is:" + acc.getBalance());
    }

}

运行结果:每次都不一样

Finally, John's balance is:1500.0

Finally, John's balance is:1600.0

Finally, John's balance is:2100.0

……

为什么会出现这样的问题?这就是多线程中的同步的问题。在我们的程序中,Account中的amount会同时被多个线程所访问,这就是一个竞争资源,通常称作竞态条件。对于这样的多个线程共享的资源我们必须进行同步,以避免一个线程的改动被另一个线程所覆盖。



2 给对象加锁

再详细一点解释。上面的 deposit 和 withdraw 都是三个步骤:

  1. 从数据库读数据(读amount)保存到零时变量;
  2. 利用零时变量进行操作,比如用户存100元,整个操作过程花了5分钟;
  3. 将新的 amount 存入数据库。

实际上,上面三个步骤都是要花一定时间的。举个形象一点的例子:

  1. 如果线程A(手机端)和B(PC端)现在几乎同时读取了amount ,amount 为1000;
  2. A向账户转账100元,此时tmp = 1100,然后写入了数据库,amount变为 1100;
  3. 接着,B向账户转账200元,此时tmp = 1200,然后写入数据库,amount变为 1200。但是实际上,我们都知道amount应该变为1300才正确。

那么我们对deposit 和 withdraw 进行同步,也就是加synchronized原语,也就是让一个线程在调用这个Account对象时,其他所有的线程都不能调用该对象, 它将所有线程串行化。所以,【synchronized加锁的是对象,而不是代码】。我们来测试一下:

        ……

//   public void deposit(float amt) {
    synchronized public void deposit(float amt) {
        float tmp = amount;
        tmp += amt;

        try {
            Thread.sleep(100);// 模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        amount = tmp;
    }

    /**
     * 取钱
     * */
//   public void withdraw(float amt) {
    synchronized public void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(100);// 模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        amount = tmp;
    }

        ……

输出:

Finally, John's balance is:1000.0

但是我们有一个明显的感受,就是程序的执行变得很慢,而且一开始还有内存的溢出的错误,我不得不将线程数量从我喜欢的 8888 改成了 100 !它还运行了好一会儿。(博主的本本是Macbook Pro 8G/256G 2.7 GHz Intel Core i5 )。所以,解决办法肯定不是这么简单的,后面会讲到。



3 给类加锁

如果两个线程,分别访问两个不同的对象实例,则它们之间没法同步的,所以这里介绍一下给类加同步的方法: 对公共成员加锁。例如给一个类添加一个静态成员,两个实例都可以同步这个对象而达到线程安全。下面举一个对比的例子。

非类同步的代码,新建两个线程,则两个线程并发执行:

public class Test1 extends Thread{
    private int val;
    private static Object object = new Object();

    public Test1(int val) {
        this.val = val;
    }

    public void printVal() {
            // 非 类同步的方法
        for(int i = 0; i<20; i++){
            System.out.println(val + "");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        super.run();
        printVal();
    }


    public static void main(String[] args) {
        Test1 t1 = new Test1(6);
        Test1 t2 = new Test1(8);
        t1.start();
        t2.start();
    }

}

输出

6886866868866868686886866868688686686868

类同步的代码,所以两个线程是串行执行的:


public class Test1 extends Thread{
    private int val;
    private static Object object = new Object();

    public Test1(int val) {
        this.val = val;
    }

    public void printVal() {
        // 类同步的方法
        synchronized (object) {
            // 非 类同步的方法
            for(int i = 0; i<20; i++){
                System.out.print(val + "");
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void run() {
        super.run();
        printVal();
    }


    public static void main(String[] args) {
        Test1 t1 = new Test1(6);
        Test1 t2 = new Test1(8);
        t1.start();
        t2.start();
    }

}

输出

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值