Java多线程之内置锁与显示锁

时间:2023-03-10 03:20:32
Java多线程之内置锁与显示锁

Java中具有通过Synchronized实现的内置锁,和ReentrantLock实现的显示锁,这两种锁各有各的好处,算是互有补充,今天就来做一个总结。

Synchronized

内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁。

synchronized(list){ //获得锁
list.append();
list.count();
}//释放锁

通信

与Synchronized配套使用的通信方法通常有wait(),notify()。

wait()方法会立即释放当前锁,并进入等待状态,等待到相应的notify并重新获得锁过后才能继续执行;notify()不会立刻立刻释放锁,必须要等notify()所在线程执行完synchronized块中的所有代码才会释放。用如下代码来进行验证:

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; } @Override
public void run(){
System.out.println("ReadList begin at "+System.currentTimeMillis());
synchronized (list){
try {
Thread.sleep();
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; } @Override
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();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get out of block at "+System.currentTimeMillis());
}
System.out.println("WriteList end at "+System.currentTimeMillis()); }
}

运行结果

ReadList begin at
WriteList begin at
list.wait() begin at
get lock at
list.notify() at
get out of block at
WriteList end at
list.wait() end at
ReadList end at

可见读线程开始运行,开始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 <= )
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 == )
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 = ;
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 = ;
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=;i<;i++){
list.add(i);
}
RWLockList rwLockList = new RWLockList(list);//初始化数据 Thread writer = new Thread(new Runnable() {
@Override public void run() {
for (int i=;i<;i++){
rwLockList.put(i);
}
}
});
Thread reader1 = new Thread(new Runnable() {
@Override public void run() {
for (int i=;i<;i++){
rwLockList.get(i);
}
}
});
Thread reader2 = new Thread(new Runnable() {
@Override public void run() {
for (int i=;i<;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=;i<;i++){
list.add(i);
}
SyncList syncList = new SyncList(list);//初始化数据
Thread writerS = new Thread(new Runnable() {
@Override public void run() {
for (int i=;i<;i++){
syncList.put(i);
}
}
});
Thread reader1S = new Thread(new Runnable() {
@Override public void run() {
for (int i=;i<;i++){
syncList.get(i);
}
}
});
Thread reader2S = new Thread(new Runnable() {
@Override public void run() {
for (int i=;i<;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");

结果:

RWLockList take 248ms
RWLockList take 255ms
RWLockList take 249ms
RWLockList take 224ms

SyncList take 351ms
SyncList take 367ms
SyncList take 315ms
SyncList take 323ms

可见读写锁的确是优于纯碎的互斥锁

总结

内置锁最大优点是简洁易用,显示锁最大优点是功能丰富,所以能用内置锁就用内置锁,在内置锁功能不能满足之时在考虑显示锁。