您的位置:首页 > 编程语言 > Java开发

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

2016-03-28 00:39 393 查看

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 都是三个步骤:

从数据库读数据(读amount)保存到零时变量;

利用零时变量进行操作,比如用户存100元,整个操作过程花了5分钟;

将新的 amount 存入数据库。

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

如果线程A(手机端)和B(PC端)现在几乎同时读取了amount ,amount 为1000;

A向账户转账100元,此时tmp = 1100,然后写入了数据库,amount变为 1100;

接着,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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息