Java中synchronized和Lock实现并发锁

前言

参考文章:
1. Java 多线程:synchronized 关键字用法(修饰类,方法,静态方法,代码块)
2. Java 多线程:Lock 接口(接口方法分析,ReentrantLock,ReadWriteLock)
3. synchronized 与 Lock 的那点事
4. Java并发编程:Lock
5. ReentrantLock(重入锁)以及公平性
参考书籍:《疯狂Java讲义(第二版)》。

synchronized

  创建一个账户类Account,有姓名和账户余额属性以及getset方法:

public class Account {

    private String name;
    private double money;

    public Account(String name, double money) {
        super();
        this.name = name;
        this.money = money;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }

}

  线程类是实现Runnable接口的Bank类,代码如下:

public class Bank implements Runnable {

    private Account account;
    private double getMoney; // 本次取钱数量

    public Bank(Account account, double getMoney) {
        super();
        this.account = account;
        this.getMoney = getMoney;
    }

    @Override
    public void run() {
        getMoney();
    }

    private void getMoney() {
        if (account.getMoney() >= getMoney) {
            System.out.println("取钱成功!取出:" + getMoney + " 元!");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.setMoney(account.getMoney() - getMoney);
            System.out.println(account.getName() + "账户余额为:" + account.getMoney() + " 元!");
        } else {
            System.out.println("取钱失败,余额不足!");
        }
    }
}

  main函数如下:

public class Test {

    public static void main(String[] args) {

        Account account = new Account("Rojer", 2000);

        Bank bank = new Bank(account, 1200);
        new Thread(bank).start();
        new Thread(bank).start();
    }

}

  运行结果如下:

取钱成功!取出:1200.0 元!
取钱成功!取出:1200.0 元!
Rojer账户余额为:800.0 元!
Rojer账户余额为:800.0 元!

  从结果中看出,两个线程都取钱成功了,这样银行是会亏死的,现在要实现一个时间段内只能有一个线程执行取钱操作,可以用synchronized关键字来进行加锁。

修饰方法

  注意,synchronized不能修饰接口中的方法,而且父类中的synchronized方法被子类继承以后,是没有继承synchronized的,如果要使用,还需要加上synchronized关键字。
  用synchronized关键字来修饰Bank类的getMoney方法:

    private synchronized void getMoney() {
        if (account.getMoney() >= getMoney) {
            System.out.println("取钱成功!取出:" + getMoney + " 元!");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.setMoney(account.getMoney() - getMoney);
            System.out.println(account.getName() + "账户余额为:" + account.getMoney() + " 元!");
        } else {
            System.out.println("取钱失败,余额不足!");
        }
    }

  运行结果如下:

取钱成功!取出:1200.0 元!
Rojer账户余额为:800.0 元!
取钱失败,余额不足!

  可以看出程序运行结果和我们期待的一样,只有一个线程取钱成功,第二个线程取钱的时候失败了,因为余额不足。
  用synchronized修饰方法时,所有的synchronized修饰的方法都被锁住了,只能供当前拥有锁的线程使用,但是其他的非synchronized修饰的方法没有被锁住,是可以正常调用的。HashTable中就是这样实现线程安全的,可以参考我的另一篇文章-深入JDK源码之Hashtable。所以说,用synchronized来修饰方法是重量级的。

修饰代码块

  修饰代码块就是以下面的方式,用synchronized关键字“包裹”住要同步的代码块:

synchronized (...) {
    ...
}

  括号中可以是一个对象,也可以是一个类的class。如果是一个对象的话,那么只对该对象的线程进行加锁,如果是修饰了一个类的class,如上文的Bank.class,那么无论有多少个Bank对象,并发时只能有一个对象的一个线程访问同步代码块,其他线程都要等待,但是访问没有synchronized锁住的代码块不受影响。

锁住class

  修改getMoney代码如下所示:

    private void getMoney() {
        synchronized (Bank.class) {
            if (account.getMoney() >= getMoney) {
                System.out.println("取钱成功!取出:" + getMoney + " 元!");
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.setMoney(account.getMoney() - getMoney);
                System.out.println(account.getName() + "账户余额为:" + account.getMoney() + " 元!");
            } else {
                System.out.println("取钱失败,余额不足!");
            }
        }
    }

  执行程序结果如下:

取钱成功!取出:1200.0 元!
Rojer账户余额为:800.0 元!
取钱失败,余额不足!

锁住对象

  修改getMoney代码,锁住账户account对象,如下所示:

private void getMoney() {
    synchronized (account) {
        if (account.getMoney() >= getMoney) {
            System.out.println("取钱成功!取出:" + getMoney + " 元!");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.setMoney(account.getMoney() - getMoney);
            System.out.println(account.getName() + "账户余额为:" + account.getMoney() + " 元!");
        } else {
            System.out.println("取钱失败,余额不足!");
        }
    }
}

  运行结果如下:

取钱成功!取出:1200.0 元!
Rojer账户余额为:800.0 元!
取钱失败,余额不足!

  synchronized修饰的是私有的account对象,因此当前对象的其他线程如果要使用account对象,就必须先拿到对象的锁才可以。
  相比于synchronized修饰方法,修饰代码段的同步粒度更小,更加轻量。

修饰静态方法

  静态方法是所有类的对象共享的,因此用synchronized修饰静态方法,其实就是修饰类,即一个线程拥有该静态方法的监视器的时候,其他所有该类的对象,或者直接调用该静态方法的线程,都需要等待当前线程释放监视器。

Lock

  从 Java 5 开始,Java提供了一种更为强大的线程同步机制——通过显示定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
  Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

—疯狂Java讲义

Lock接口

  Lockjava.util.concurrent.locks包中的一个接口,源码如下:

public interface Lock {
    // 用来获取锁,如果锁已被其他线程获取,则进行等待。
    void lock();
    // 如果获取锁成功返回true,否则返回false
    boolean tryLock();
    // 拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。
    // 如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
    void lockInterruptibly() throws InterruptedException;
    // 释放锁
    void unlock();
    Condition newCondition();
}

  ReentrantLock(可重入锁)是Lock接口的实现类,可重入,也就是可以在当前线程上对资源再加一个锁,相应的有一个值在记录加了几重锁,如果释放锁时没有释放完,则会报错。同时,ReentrantLock有个特性就是公平性,这个后面再说。

ReadWriteLock接口

  还有一个接口ReadWriteLock,顾名思义是读写锁:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

  ReentrantReadWriteLock实现了ReadWriteLock接口,如果调用readLock().lock()的话,被当前线程锁住的资源,其他所有线程都可以进行读,但是不能进行写,也不允许其他线程进行writeLock().lock()来对当前资源进行加写锁;如果一个线程调用写锁writeLock().lock(),那么之后当前线程可以进行写操作,其他线程不能同时写。


  上面说到的公平性,如果是非公平的,就是JVM决定唤醒哪个等待线程获得锁,而公平的锁,即唤醒当前在等待的线程中,等待时间最长的那个线程获得锁。默认情况下,ReentrantLockReentrantReadWriteLock都是非公平锁,但是可以设置为公平锁,相对于公平锁,非公平锁有更好的效率,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配,而且对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了。

总结

  相比于synchronized的笨重,Lock更加灵活,如果获取synchronized锁的线程阻塞了,那么其他等待锁的线程只能等待,十分影响程序效率,而Lock可以让线程只等待一段时间或者中断线程的等待,还可以加读锁,同时多个线程都可以进行读,效率十分高。
  但是Lock实现锁需要程序员多操点心了,获取synchronized锁的线程执行完毕之后就会释放锁,不需要手动释放,而且如果线程发生异常,JVM会让线程自动释放锁,但是Lock锁需要程序员手动释放锁,并且要处理异常。

你可能感兴趣的:(Java)