前言
参考文章:
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对象。—疯狂Java讲义
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
锁需要程序员手动释放锁,并且要处理异常。