为什么需要同步
JVM有一个main memory,而每个线程都有自己的working memory,一个线程对一个变量进行操作时,都要在自己的
working memory里面建立一个copy,操作完之后再写入main memory.当多个线程同时操作一个变量时,就可能产生不可预知的结果,这就是线程安全问题.
当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用。达到此目的的过程叫做同步(synchronization),
解决线程安全问题的方法就是利用同步机制.看一段简单的代码,银行账户取钱的例子:
package code;
public class UseBank {
public static void main(String[] args) {
// TODO Auto-generated method stub
Account account = new Account();
Thread user_1 = new User(account);
Thread user_2 = new User(account);
user_1.start();
user_2.start();
}
}
class User extends Thread{
private Account account;
public User(Account account){
this.account = account;
}
@Override
public void run(){
System.out.println("I get:" + this.account.Withdraw(800));
}
}
class Account{
private int money = 1000;
public Account(){
}
//取钱
public int Withdraw(int number){
if(number < 0){
return -1;
}else if(number > money){
return -2;
}else if(money < 0){
return -3;
}else{
money -= number;
try{
Thread.sleep(2000);
}
catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("Withdraw:" + number + " " + "Remains: " + money);
return number;
}
}
}
代码中定义了一个银行账户有1000元,两个线程代表两个用户去共享这个账户去取钱,每个取800,正常来说一个用户取完800之后另一个用户是无法再取出800的,但是代码执行的结果会出现以下情况:
情况一:
Withdraw:800 Remains: -600
I get:800
Withdraw:800 Remains: -600
I get:800
情况二:
Withdraw:800 Remains: 200
I get:800
Withdraw:800 Remains: -600
I get:800
也就是说两个用户都成功取出了800.这是因为,在执行钱数减少之前,线程休眠了一个简短的时间,在一个用户进入else语句后休眠过程中,还未取走钱,所以另一个用户也能进入,于是就都能取走800.
同时也能看到,余额输出的顺序可以是-600 -600或者200 -600.这是因为在输出钱另一个账户可能没把钱取走,也可能把钱去走了.
解决办法
在Withdraw方法前加上关键字synchronized.
package code;
public class UseBank {
public static void main(String[] args) {
// TODO Auto-generated method stub
Account account = new Account();
Thread user_1 = new User(account);
Thread user_2 = new User(account);
user_1.start();
user_2.start();
}
}
class User extends Thread{
private Account account;
public User(Account account){
this.account = account;
}
@Override
public void run(){
System.out.println("I get:" + this.account.Withdraw(800));
}
}
class Account{
private int money = 1000;
public Account(){
}
//取钱
public synchronized int Withdraw(int number){
if(number < 0){
return -1;
}else if(number > money){
return -2;
}else if(money < 0){
return -3;
}else{
try{
Thread.sleep(2000);
}
catch (InterruptedException e){
e.printStackTrace();
}
money -= number;
System.out.println("Withdraw:" + number + " " + "Remains: " + money);
return number;
}
}
}
之后再执行代码就只有一个用户能够取出800了.
同步的实现方法
同步方法
被
synchronized修饰的方法叫做同步方法.
当
synchronized关键字修饰一个方法时,该方法叫做同步方法.在JAVA中每个对象都有一个锁,当一个进程访问某个对象的
synchronized方法时,将该对象上锁,其他任何进程都无法再访问对象的synchronized方法,直到之前那个对象执行完毕或是抛出异常,才将锁释放,其他进程才能够访问该对象的
synchronized方法.在这里需要注意的是被上锁的是对象.如果被
synchronized修饰的方法是静态方法,将会锁住整个类.
上面例子的解决方法使用的就是同步方法.
同步代码块
可能一个方法中只有某段代码访问共享资源,而同步是一种高开销的操作,因此应该减少同步的内容,同步代码块就是用synchronized修饰代码块,被修饰的代码块会自动加上内置锁,实现同步.
通常同步代码块是需要锁定的对象,一般是需要并发访问的共享资源,任何线程在修改指定资源之前都首先对该资源加锁,在加锁期间其它线程无法修改该资源。从而保证了线程的安全性,而且线程在调用sleep也不会让出资源锁。
例如上例可以这样修改,在用户线程代码中对Account对象进行加锁,把调用Withdraw代码块变成同步块:
package code;
public class UseBank {
public static void main(String[] args) {
// TODO Auto-generated method stub
Account account = new Account();
Thread user_1 = new User(account);
Thread user_2 = new User(account);
user_1.start();
user_2.start();
}
}
class User extends Thread{
private Account account;
public User(Account account){
this.account = account;
}
@Override
public void run(){
synchronized(account){
System.out.println("I get:" + this.account.Withdraw(800));
}
}
}
class Account{
private int money = 1000;
public Account(){
}
//取钱
public synchronized int Withdraw(int number){
if(number < 0){
return -1;
}
else if(number > money){
return -2;
}else if(money < 0){
return -3;
}else{
try{
Thread.sleep(2000);
}
catch (InterruptedException e){
e.printStackTrace();
}
money -= number;
System.out.println("Withdraw:" + number + " " + "Remains: " + money);
return number;
同步锁
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
上述例子使用同步锁:
package code;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UseBank {
public static void main(String[] args) {
// TODO Auto-generated method stub
Account account = new Account();
Thread user_1 = new User(account);
Thread user_2 = new User(account);
user_1.start();
user_2.start();
}
}
class User extends Thread{
private Account account;
public User(Account account){
this.account = account;
}
@Override
public void run(){
System.out.println("I get:" + this.account.Withdraw(800));
}
}
class Account{
private int money = 1000;
Lock lock = new ReentrantLock();
public Account(){
}
//取钱
public int Withdraw(int number){
lock.lock();
if(number < 0){
lock.unlock();
return -1;
}
else if(number > money){
lock.unlock();
return -2;
}else if(money < 0){
lock.unlock();
return -3;
}else{
try{
Thread.sleep(2000);
}
catch (InterruptedException e){
e.printStackTrace();
}
money -= number;
System.out.println("Withdraw:" + number + " " + "Remains: " + money);
lock.unlock();
return number;
}
}
}
在对共享资源进行操作的前加锁,操作完毕释放.