首页 > 编程 > Java > 正文

Java中synchronized和Lock实现并发锁

2019-11-06 06:27:05
字体:
来源:转载
供稿:网友

前言

参考文章: 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锁需要程序员手动释放锁,并且要处理异常。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表