synchronized
例子:我们模拟两个线程取钱的操作,代码如下:
class Account {
String accountNo;//账户名
double balance;//账户余额
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public void draw(double drawAmount) {
//如果账户余额>=所取得钱数则取钱成功,否则失败
if (balance >= drawAmount) {
balance -= drawAmount;
System.out.println(Thread.currentThread().getName() + "取钱成功, 余额为:" + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");
}
}
}
class DrawThread extends Thread{
Account account;
public DrawThread(Account account) {
this.account=account;
}
@Override
public void run() {
while (account.balance>=100) {
account.draw(100);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class SynchronizeTest {
public static void main(String[] args) {
Account account=new Account("dandan", 1000);
DrawThread drawThread1= new DrawThread(account);
DrawThread drawThread2= new DrawThread(account);
drawThread1.start();
drawThread2.start();
}
}
运行结果:
然而运行结果,却出现两个800元,即就是线程安全问题,下面我们分析为什么会出现这种状况,可能是下面的这种情况(但是这种情况并不唯一,因为余额的变动并不是原子操作):
那么出现线程安全的原因是什么?
- 存在两个或者两个以上的线程对象,而且线程之间共享着一个资源。
- 有多个语句操作了共享资源。
为了解决这个问题,java的多线程引入synchronized同步代码块和同步方法来解决这个问题,下面我们来看一下这两种方法:
synchronized有两种使用方式:
方式一:同步代码块
格式:
synchronized(锁对象){
需要被同步的代码…
}
注意:多线程操作的锁对象必须是唯一共享的。否则无效。也就是说锁对象是static的/常量,其实最简单的就是使用一个常量作为锁对象
这种方式比较简单,我们就不写代码的例子了。。。
方式二:同步函数
同步函数:同步函数就是使用synchronized修饰一个函数。
注意:
- 如果是一个非静态的同步函数的锁对象是this对象,如果是静态的同步函数的锁对象是当前函数所属的类的字节码文件(class对象)。因此,同步方法只能保证多个线程同时执行同一个对象的同步代码段
- 同步函数的锁对象是固定的,不能自己来指定的。
下面我们用Synchronized修饰这个取钱的方法,代码如下:
class Account {
String accountNo;
double balance;
public Account(String accountNo, double balance) {
// TODO Auto-generated constructor stub
this.accountNo = accountNo;
this.balance = balance;
}
public synchronized void draw(double drawAmount) {
if (balance >= drawAmount) {
balance -= drawAmount;
System.out.println(Thread.currentThread().getName() + "取钱成功, 余额为:" + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");
}
}
}
class DrawThread extends Thread{
Account account;
public DrawThread(Account account) {
// TODO Auto-generated constructor stub
this.account=account;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (account.balance>=100) {
account.draw(100);
}
}
}
public class SynchronizeTest {
public static void main(String[] args) {
Account account=new Account("dandan", 50000);
DrawThread drawThread1= new DrawThread(account);
DrawThread drawThread2= new DrawThread(account);
drawThread1.start();
drawThread2.start();
}
}
执行结果:
那么synchronized的原理是什么?
原来synchronized关键字经过编译之后, 会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数(即我们之前说的锁对象)来指明要锁定和解锁的对象。
在执行monitorenter指令时, 首先要尝试获取对象的锁。 如果这个对象没被锁定, 或者当前线程已经拥有了那个对象的锁, 把锁的计数器加1, 相应的, 在执行monitorexit指令时会将锁计数器减1,当计数器为0时, 锁就被释放。如果获取对象锁失败, 那当前线程就要阻塞等待, 直到对象锁被另外一个线程释放为止。
为什么要用计数器来实现锁对象?
其实为了保证不会出现自己把自己锁死的问题:
对于同步代码块来说,即在synchronized块中,再定义一个synchronized块
对于同步方法来说,为了防止自己再调用自己的时候(递归调用)时,自己把自己锁死