锁是多线程并发执行下,通过保证单位时间内只有单个线程对共享资源进行修改,从而保证程序能够正确执行。在Java语言中目前有这样三种锁。
- synchronized
- ReadWriteLock
- StampedLock(java8)
synchronized
实现
- Synchronized的语义底层是通过一个monitor的对象来完成,指令monitorenter和monitorexit标记线程获得和释放monitor(锁)。
优点
- 写法简单, 通过synchronized 修饰方法或者代码块,Collections中通过synchronized实现了很多线程安全的集合类。
缺点
- 重量级锁,线程锁切换需要消耗大量资源(cpu周期),javase 1.6有所优化。
ReadWriteLock
实现
- 基于AbstractQueuedSynchronizer来实现,lock方法是基于比较并交换(CAS)。
优点
- 读写分离,读锁共享,读读并行,读多写少场景性能高,并发包下打量工具类使用。
缺点
- 可能会导致线程饥饿,取决于公平锁or非公平锁,公平锁不会饥饿,但是性能下降。
StampedLock
实现
- 它使用了一个票据(stamp)的概念,这是一个long值,在加锁和解锁操作时,它被用作一张门票,解锁一个操作你需要传递相应的的门票。如果传递错误的门票,那么可能会抛出一个异常,或者其他意想不到的错误。StampedLock内部基于CLH锁,CLH是一种自旋锁,他能保证没有线程饥饿发生,并且保证FIFO顺序服务。
优点
- 乐观读模式,乐观锁类似无锁操作,不会阻塞线程。
缺点
- 使用较为复杂,类似cas操作重试使用的Unsafe.park()函数,park()函数遇到线程中断可能导致疯狂占用CPU情况。
测试性能
- Synchronized、ReadWriteLock锁、StampedLock的读写锁以及读写乐观锁。读线程将读取一个计数器的值,写线程会将它从0增加到10000000。
Synchronized
public class Synchronized implements Counter
{
private Object lock = new Object();
private int counter;
public long getCounter()
{
synchronized (lock)
{
return counter;
}
}
public void increment()
{
synchronized (lock)
{
++counter;
}
}
}
ReadWriteLock
public class RWLock implements Counter
{
private ReadWriteLock rwlock = new ReentrantReadWriteLock();
private Lock rlock = rwlock.readLock();
private Lock wlock = rwlock.writeLock();
private long counter;
public long getCounter()
{
try
{
rlock.lock();
return counter;
}
finally
{
rlock.unlock();
}
}
public void increment()
{
try
{
wlock.lock();
++counter;
}
finally
{
wlock.unlock();
}
}
}
StampedLock
public class Stamped implements Counter {
private StampedLock rwlock = new StampedLock();
private long counter;
public long s, t;
public long getCounter()
{
long stamp = rwlock.tryOptimisticRead();
try
{
long result = counter;
if (rwlock.validate(stamp))
{
return result;
}
stamp = rwlock.readLock();
result = counter;
rwlock.unlockRead(stamp);
return counter;
}
finally
{
}
}
public void increment()
{
long stamp = rwlock.writeLock();
try
{
++counter;
}
finally
{
rwlock.unlockWrite(stamp);
}
}
}
结果
5个读线程和5个写线程
Using SYNCHRONIZED. threads: 10. rounds: 5. Target: 10000000
775
714
843
1027
1139
Using RWLOCK. threads: 10. rounds: 5. Target: 10000000
4769
8308
14998
11754
3735
Using STAMPED. threads: 10. rounds: 5. Target: 10000000
814
926
1210
1205
1265
10个读线程和10个写线程
Using SYNCHRONIZED. threads: 20. rounds: 5. Target: 10000000
959
776
919
1010
1187
Using RWLOCK. threads: 20. rounds: 5. Target: 10000000
5609
1193
5289
10819
16415
Using STAMPED. threads: 20. rounds: 5. Target: 10000000
856
808
842
946
1122
16个读线程和4个写线程
Using SYNCHRONIZED. threads: 20. rounds: 5. Target: 10000000
1839
3384
1909
2577
1943
Using RWLOCK. threads: 20. rounds: 5. Target: 10000000
55780
62211
68641
这里能说明读写锁这种状态效果不好
Using STAMPED. threads: 20. rounds: 5. Target: 10000000
6241
9457
6253
4048
12298
总结
总体看来, 整体性能表现最好的仍然是内置的同步锁。但是,这里并不是说内置的同步锁会在所有的情况下都执行得最好。这里主要想表达的是在你将你的代码投入生产之前,应该基于预期的竞争级别和读写线程之间的分配进行测试,再选择适当一个适当的锁。否则你会面临线上故障的风险。