多线程内置锁与互斥锁
Java中具有通过Synchronized实现的内置锁,和ReentrantLock实现互斥锁,各有好处,互有补充。
Synchronized
内置锁获得锁和释放锁是隐式的,进入Synchronized修饰的代码就获得锁,走出相应的代码就释放锁。
synchronized(list){ // 获得锁
list.appent();
list.count();
} // 释放锁
通信
与Synchronized配套使用的通信方法常有wait(), notify().
- wait()方法会立即释放当前锁,并进入等待状态,等待到相应的notify()并重新获得锁过后才能继续执行;
- notify()不会立刻释放锁,必须要等notify()所在线程执行完Synchronized块中的所有代码才会释放
import java.util.LinkedList;
import java.util.List;
/**
* SynchronizedTest
* PACKAGE_NAME
*
* @author xiaoyy
* 内置锁demo
* @Date 2017-05-09 下午1:37
* The word 'impossible' is not in my dictionary.
*/
public class SynchronizedTest {
public static void main(String[] args) {
List list = new LinkedList();
Thread r = new Thread(new ReadList(list));
Thread w = new Thread(new WriteList(list));
r.start();
w.start();
}
}
class ReadList implements Runnable{
private List list;
public ReadList(List list){ this.list = list; }
public void run(){
System.out.println("ReadList begin at "+System.currentTimeMillis());
// 开启内置锁
synchronized (list){
try {
Thread.sleep(1000);
System.out.println("list.wait() begin at "+System.currentTimeMillis());
// 立即释放当前锁
list.wait();
System.out.println("list.wait() end at "+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("ReadList end at "+System.currentTimeMillis());
}
}
class WriteList implements Runnable{
private List list;
public WriteList(List list){ this.list = list; }
public void run(){
System.out.println("WriteList begin at "+System.currentTimeMillis());
// 开启内置锁
synchronized (list){
System.out.println("get lock at "+System.currentTimeMillis());
// 等待内置锁内方法执行完再释放当前锁
list.notify();
System.out.println("list.notify() at "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get out of block at "+System.currentTimeMillis());
}
System.out.println("WriteList end at "+System.currentTimeMillis());
}
}
运行结果
可见读线程开始运行,开始wait()过后,写线程才获得锁;写线程走出同步块而不是notify()过后,读线程才wait()结束,亦即获得锁。 所以notify()不会释放锁,wait会释放锁。 注:notifyall()会通知等待队列终端所有线程。
编码
编码模式比较简单,单一,不必显示的获得锁,释放锁,能降低因粗心忘记释放锁的错误。使用模式如下:
synchronized(object){
}
灵活性
- 内置锁在进入同步块时,采用的是无限等待的策略,一旦开始等待,就即不能中断也不能取消,容易产生饥饿与死锁的问题
- 在线程调用notify()方法时,会随机选择相应对象的等待队列的一个线程将其唤醒,而不是按照FIFO的方式,如果有强烈的公平性要求,比如FIFO就无法满足
性能
Synchronized在JDK1.5及之前性能(主要指吞吐率)比较差,扩展性也不如ReentrantLock。但是JDK1.6以后,修改了管理内置锁的算法, 使得Synchronized和标准的ReentrantLock性能差别不大。
ReentrantLock
ReentrantLock是互斥锁,需要显示进行 lock 以及 unlock 操作。
通信
与ReentrantLock搭配的通行方式是Condition,如下:
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
condition.await();//this.wait();
condition.signal();//this.notify();
condition.signalAll();//this.notifyAll();
Condition是被绑定到Lock上的,必须使用lock.newCondition()才能创建一个Condition。从上面的代码可以看出,Synchronized能实现的通信方式, Condition都可以实现,功能类似的代码写在同一行中。而Condition的优秀之处在于它可以为多个线程间建立不同的Condition,比如对象的读/写Condition, 队列的空/满Condition,在JDK源码中的ArrayBlockingQueue中就使用了这个特性:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
编码
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}
相比于Synchronized要复杂一些,而且一定要记得在finally中释放锁而不是其他地方,这样才能保证即使出了异常也能释放锁。
灵活性
- lock.lockInterruptibly() 可以使得线程在等待锁是支持响应中断;lock.tryLock() 可以使得线程在等待一段时间过后如果还未获得锁就停止等待而非一直等待。 有了这两种机制就可以更好的制定获得锁的重试机制,而非盲目一直等待,可以更好的避免饥饿和死锁问题
- ReentrantLock可以成为公平锁(非默认的),所谓公平锁就是锁的等待队列的FIFO,不过公平锁会带来性能消耗,如果不是必须的不建议使用。 这和CPU对指令进行重排序的理由是相似的,如果强行的按照代码的书写顺序来执行指令,就会浪费许多时钟周期,达不到最大利用率
性能
虽然Synchronized和标准的ReentrantLock性能差别不大,但是ReentrantLock还提供了一种非互斥的读写锁, 也就是不强制每次最多只有一个线程能持有锁,它会避免“读/写”冲突,“写/写”冲突,但是不会排除“读/读”冲突, 因为“读/读”并不影响数据的完整性,所以可以多个读线程同时持有锁,这样在读写比较高的情况下,性能会有很大的提升。
下面用两种锁分别实现的线程安全的linkedlist:
class RWLockList {//读写锁
private List list;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public RWLockList(List list){this.list = list;}
public int get(int k) {
readLock.lock();
try {
return (int)list.get(k);
} finally {
readLock.unlock();
}
}
public void put(int value) {
writeLock.lock();
try {
list.add(value);
} finally {
writeLock.unlock();
}
}
}
class SyncList {
private List list;
public SyncList(List list){this.list = list;}
public synchronized int get(int k){
return (int)list.get(k);
}
public synchronized void put(int value){
list.add(value);
}
}
- 读写锁测试代码:
List list = new LinkedList();
for (int i=0;i<10000;i++){
list.add(i);
}
RWLockList rwLockList = new RWLockList(list);//初始化数据
Thread writer = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<10000;i++){
rwLockList.put(i);
}
}
});
Thread reader1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<10000;i++){
rwLockList.get(i);
}
}
});
Thread reader2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<10000;i++){
rwLockList.get(i);
}
}
});
long begin = System.currentTimeMillis();
writer.start();reader1.start();reader2.start();
try {
writer.join();
reader1.join();
reader2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("RWLockList take "+(System.currentTimeMillis()-begin) + "ms");
- 同步锁测试代码:
List list = new LinkedList();
for (int i=0;i<10000;i++){
list.add(i);
}
SyncList syncList = new SyncList(list);//初始化数据
Thread writerS = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<10000;i++){
syncList.put(i);
}
}
});
Thread reader1S = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<10000;i++){
syncList.get(i);
}
}
});
Thread reader2S = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<10000;i++){
syncList.get(i);
}
}
});
long begin1 = System.currentTimeMillis();
writerS.start();reader1S.start();reader2S.start();
try {
writerS.join();
reader1S.join();
reader2S.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("SyncList take "+(System.currentTimeMillis()-begin1) + "ms");
- 结果: ```text RWLockList take 248ms RWLockList take 255ms RWLockList take 249ms RWLockList take 224ms
SyncList take 351ms SyncList take 367ms SyncList take 315ms SyncList take 323ms
可见读写锁的确是优于纯碎的互斥锁
互斥锁demo
```java
public class ReentrantLockTest {
private static class Counter {
private ReentrantLock reentrantLock = new ReentrantLock();
public void count() {
reentrantLock.lock();
try {
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + ", i = " + i);
}
} finally {
// 必须在finally释放锁
reentrantLock.unlock();
}
}
}
private static class MyThread extends Thread {
private Counter mCounter;
private String mName;
public MyThread(Counter counter, String name) {
mCounter = counter;
mName = name;
}
@Override
public void run() {
System.out.println("start thread " + mName);
super.run();
mCounter.count();
System.out.println("end thread " + mName);
}
}
public static void main(String[] args) {
Counter counter = new Counter();
// myThread1和myThread2是调用同一个对象counter
MyThread thread1 = new MyThread(counter, "thread1");
MyThread thread2 = new MyThread(counter, "thread2");
thread1.run();
thread2.run();
}
}
ReentrantLock or synchronized
内置锁最大优点是简洁易用,互斥锁最大优点是功能丰富,所以能用内置锁就用内置锁,在内置锁功能不能满足之时在考虑互斥锁。
ReentrantLock 在性能上 比 synchronized 更胜一筹。
ReentrantLock 需格外小心,因为需要显式释放锁,lock() 后记得 unlock(),而且必须在 finally 里面,否则容易造成死锁。
synchronized 隐式自动释放锁,使用方便。
ReentrantLock 扩展性好,可中断锁,定时锁,自由控制。
synchronized 一但进入阻塞等待,则无法中断等待。
采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用; 而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象。
Lock
通过查看Lock的源码可知,Lock 是一个接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException; // 可以响应中断
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 可以响应中断
void unlock();
Condition newCondition();
}
lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。
Lock的使用:
public class LockTest {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); // 注意这个地方:lock被声明为成员变量
public static void main(String[] args) {
final LockTest test = new LockTest();
new Thread("A") {
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread("B") {
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
// Lock lock = new ReentrantLock(); // 注意这个地方:lock被声明为局部变量
lock.lock();
try {
System.out.println("线程" + thread.getName() + "得到了锁...");
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
} finally {
System.out.println("线程" + thread.getName() + "释放了锁...");
lock.unlock();
}
}
}
需注意Lock定义为局部变量与成员变量时线程获得、释放锁的顺序。
锁的概念
可重入性 锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,
可中断锁 可中断锁就是可以响应中断的锁。在Java中,Synchronized就不是可中断锁,而Lock是可中断锁。
公平锁 公平锁即尽量以请求锁的顺序来获取锁。 synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
读写锁 读写锁将对临界资源的访问分成了两个锁,一个读锁和一个写锁。 正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。 ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。 可以通过readLock()获取读锁,通过writeLock()获取写锁。