synchronized and Lock

49 阅读2分钟

下面是一段多线程取钱引发线程安全问题的代码

public class DrawTest {
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(1000000);
        new MyThread(account,"小明").start();
        new MyThread(account,"lihua").start();
    }
}
class Account {
    private double money;
    public Account(double money) {
        this.money = money;
    }

    public void drawMoney(double money){
        String name = Thread.currentThread().getName();
        if (this.money >= money){
            System.out.println(name +"取钱"+money);
            this.money -= money;
            System.out.println(name + "余额:" + this.money);
        }else {
            System.out.println("余额不足");
        }
    }
    public double getMoney() {
        return money;
    }
}
class MyThread extends Thread{

    private Account account;
    public MyThread(Account account, String name) {
        super(name);
        this.account = account;
    }
    @Override
    public void run() {
        // 取钱
        account.drawMoney(1000000);
    }
}

输出结果:
lihua取钱1000000.0
小明取钱1000000.0
lihua余额:0.0
小明余额:-1000000.0

可以看到出现了超取现象

为了保证线程安全可以使用synchronized关键字

// 同步方法
public synchronized void drawMoney(double money){
    String name = Thread.currentThread().getName();
    if (this.money >= money){
        System.out.println(name +"取钱"+money);
        this.money -= money;
        System.out.println(name + "余额:" + this.money);
    }else {
        System.out.println("余额不足");
    }
}
// 同步代码块
public void drawMoney(double money){
    String name = Thread.currentThread().getName();
    synchronized(this){
        if (this.money >= money){
            System.out.println(name +"取钱"+money);
            this.money -= money;
            System.out.println(name + "余额:" + this.money);
        }else {
            System.out.println("余额不足");
        }
    }
}

那么同步代码块与同步方法哪个更好?

  • 同步代码块:同步代码块锁的范围更小,同步方法锁的范围更大。
  • 可读性:同步方法锁可读性更好

锁对象的选择

对于锁对象的选择需要选择唯一对象,比如双引号创建的字符串(Java 中⽤双引号括起来的字符串,例如:"abc"、"def",都是直接存储在“⽅法区”的“字符串常量池”当中的,只会存一份)。但选择字符串常量作为锁对象那么对于同一个类的不同实例需要不同锁对象的场景就不适用,因此一般对于实例方法选择this关键字作为锁对象,这样每个实例都能有自己的锁对象,对于静态方法则选择类对象(即类名.class)作为锁对象,同步方法锁的

Lock锁

Lock是接口,不能直接实例化,可以采用他的实现类ReentrantLock来构建锁对象。

// 创建锁对象
private Lock lock = new ReentrantLock();
    lock.lock();//开锁
    //同步代码块
    lock.unlock();//释放锁

创建锁对象要考虑位置 比如这里的取钱就应该作为Accout类的实例属性,这样对于不同实例就有不同的锁对象。 为了保证锁对象的唯一性,加上final关键字,即:

// 创建锁对象
private final Lock lock = new ReentrantLock();
    lock.lock();//开锁
    //同步代码块
    lock.unlock();//释放锁

此时Lock锁的使用还存在问题,如果同步代码块出现异常将会导致锁无法释放,所以想到try...catch...finally,将释放锁放到finally代码块中即:

// 创建锁对象
private final Lock lock = new ReentrantLock();
    lock.lock();//开锁
    try{
        //同步代码块
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        lock.unlock();//释放锁
    }