java中的许多类都非常相似,他们之间的区别就是在多线程时使用是否线程安全比如StringBuffer 和 StringBuilder。
这个问题一直没有深入探索,所以经常搞不清他们之间的关系。要想深入的理解他们,首先要搞明白什么是线程安全。
1.多线程的内存分配
在jvm中,有一个主内存,主内存是多线程共享的。
每个线程有一个自己的工作线程,多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。
当new一个对象的时候,其被分配在主内存中,某个线程想要操作它,不能获取,而是从主内存获取该对象的副本
下面是某个线程操作对象时的执行流程:
1. 从主存复制变量到当前工作内存
2. 执行代码,改变共享变量值
3. 用工作内存数据刷新主存相关内容
2.引起线程不安全的原因
下面举个简单的例子:
for(int i=0;i<10;i++) x++;
当单个线程a执行以上代码,流程如下:
1.从主内存中读取x的副本到工作内存
2.x+1
3.将x+1后的值写回主存
若另一个线程b同时执行另一段代码:
for(int i=0;i<10;i++) x--;
假设x现在为10,可能会有以下情况发生:
1:线程a从主存读取x副本到工作内存,工作内存中x值为10
2:线程b从主存读取x副本到工作内存,工作内存中x值为10
3:线程a将工作内存中x加1,工作内存中x值为11
4:线程a将x提交主存中,主存中x为11
5:线程b将工作内存中x值减1,工作内存中x值为9
6:线程b将x提交到中主存中,主存中x为9
如果x是银行账户,线程a是存款操作,线程b是扣款操作,显然会出现严重问题,看下面的代码:
public class TestThread {
@Test
public void testThread() throws InterruptedException {
Account account = new Account(10000);
Thread a = new Thread(){
public void run(){
for (int i = 0; i < 10000; i++){
account.add(1);
}
}
};
Thread b = new Thread(){
public void run(){
for (int i = 0; i < 10000; i++){
account.withdraw(1);
}
}
};
a.start();
b.start();
a.join();
b.join();
System.out.println(account.getBalance());
}
}
class Account{
private int balance;
public Account(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void add(int num) {
balance = balance + num;
}
public void withdraw(int num) {
balance = balance - num;
}
}
注:join()方法保证了 main函数输出 balance 在 a,b两线程执行完成后执行
上面代码执行多次的结果都不一样。这样的代码如果放在实际项目中,就会出现很大的问题。
3.如何保证线程安全
java用synchronized关键字做为多线程并发环境的执行有序性的保证手段之一。
当synchronized标记一个方法时,方法中调用的共享对象相当于加了一把线程锁。(synchronized只对共享对象操作才会显得有意义)
一个线程执行加了synchronized标记的代码过程如下:
1 获得同步锁
2 清空工作内存
3 从主存拷贝变量副本到工作内存
4 对这些变量计算
5 将变量从工作内存写回到主存
6 释放锁
synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
当上面代码中add方法withdraw方法加了synchronized标记后,最后执行结果每次都是准确的10000。