您的位置:首页 > 编程语言 > Java开发

Java中synchronized和Lock实现并发锁

2017-03-06 21:07 597 查看

前言

参考文章:

1. Java 多线程:synchronized 关键字用法(修饰类,方法,静态方法,代码块)

2. Java 多线程:Lock 接口(接口方法分析,ReentrantLock,ReadWriteLock)

3. synchronized 与 Lock 的那点事

4. Java并发编程:Lock

5. ReentrantLock(重入锁)以及公平性

参考书籍:《疯狂Java讲义(第二版)》。

synchronized

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

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对象。
[align=right]—疯狂Java讲义 [/align]

Lock接口

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

总结

  相比于
synchronized
的笨重,
Lock
更加灵活,如果获取
synchronized
锁的线程阻塞了,那么其他等待锁的线程只能等待,十分影响程序效率,而
Lock
可以让线程只等待一段时间或者中断线程的等待,还可以加读锁,同时多个线程都可以进行读,效率十分高。

  但是
Lock
实现锁需要程序员多操点心了,获取
synchronized
锁的线程执行完毕之后就会释放锁,不需要手动释放,而且如果线程发生异常,JVM会让线程自动释放锁,但是
Lock
锁需要程序员手动释放锁,并且要处理异常。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: