synchronized(monitor)、ReentantLock(AQS)、AtomicLong(CAS)、LongAdder(XADD)
针对代码块需要同步的锁
synchronized---锁竞争不激烈的场景
竞争不激烈的时候,各种锁优化机制会发挥作用(如轻量级锁、偏向锁、自旋锁、锁消除等),如果竞争激烈,会使用重量级锁,性能下降。
ReentantLock---锁竞争激烈的场景
通过维护state,同步队列和等待对列,性能均衡,竞争激烈也不会有问题
针对某个变量需要同步的场景
AtomicLong(CAS)---锁竞争不激烈的场景
锁竞争激烈的场景,每次CAS都只有一个线程能成功,竞争失败的线程会非常多。失败次数越多,循环次数就越多,很多线程的CAS操作越来越接近 自旋锁(spin lock),导致严重的性能问题
LongAdder(Cells+baseValue)---锁竞争激烈的场景
通过维护baseValue和Cells数组,允许多核场景下多个线程同时在cells中修改变量,极大减少了锁争抢和阻塞导致的性能问题
使用方式上:reentrantLock是显式锁,而synchronized是隐式锁,异常情形下,synchronized自动释放锁,reentrantLock则必须在finally中手动释放。
是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
功能上:reentrantlock 功能更丰富,支持公平锁、锁中断、尝试获取锁等功能,所以更强大
1reentrantlock 的lockInterruptibly方法支持等待可中断
对于synchronized,如果一个线程正在等待锁,那么结果只有两种情况,要么获得这把锁继续执行 ,要么就保持等待。而使用ReentrantLock,如果一个线程正在等待锁,那么它依然可以收到通知,被告知无需再等待,可以停止工作了。
2reentrantlock支持公平锁-ReentrantLock(boolean fair)
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而非公平锁不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁,synchronized中的锁是非公平的,ReenrantLock默认情况下也是非公平的,但是可以在构造函数中设置为公平锁 。
3ReentrantLock.tryLock(time,timeUnit)
性能上:Synchronized因为各种锁优化机制更适用于锁竞争不激烈,ReetrantLock适用激烈情形
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:
class syncData {
private int data;// 共享数据
public synchronized void set(int data) {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
}
public synchronized void get() {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
}
}
然后写个测试类来用多个线程分别读写这个共享数据:
public static void main(String[] args) {
// final Data data = new Data();
final syncData data = new syncData();
// final RwLockData data = new RwLockData();
//写入
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
});
t.setName("Thread-W" + i);
t.start();
}
//读取
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
});
t.setName("Thread-R" + i);
t.start();
}
}
运行结果:
Thread-W0准备写入数据
Thread-W0写入0
Thread-W0准备写入数据
Thread-W0写入1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
........
现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??
对!读取线程不应该互斥!
那有一种错误的方案就是直接去掉get方法的synchronized,此方案虽然get的时候不再互斥了,但是同时会引入一个问题,我set的时候竟然也可以get,这就导致读取和写入会相互干扰。正确的方案是用读写锁ReadWriteLock实现:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
[java] view plain copy
class Data {
private int data;// 共享数据
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}
测试结果:
[plain] view plain copy
Thread-W1准备写入数据
Thread-W1写入9
Thread-W1准备写入数据
Thread-W1写入24
Thread-W1准备写入数据
Thread-W1写入12
Thread-W0准备写入数据
Thread-W0写入22
Thread-W0准备写入数据
Thread-W0写入15
Thread-W0准备写入数据
Thread-W0写入6
Thread-W0准备写入数据
Thread-W0写入13
Thread-W0准备写入数据
Thread-W0写入0
Thread-W2准备写入数据
Thread-W2写入23
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入17
Thread-W2准备写入数据
Thread-W2写入11
Thread-R2准备读取数据
Thread-R1准备读取数据
Thread-R0准备读取数据
Thread-R0读取11
Thread-R1读取11
Thread-R2读取11
Thread-W1准备写入数据
Thread-W1写入18
与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。
StampedLock是ReadWriteLock的一个改进。StampedLock与ReadWriteLock的区别在于,StampedLock认为读不应阻塞写。StampedLock认为读写互斥应该是指——不让读重复读同一数据,而不是不让写线程写。StampedLock是一种偏向于写线程的改进,解决了读多写少时,使用ReadWriteLock会产生写线程饥饿现象。
下面将锁对比下
synchronized同步锁--锁竞争不激烈,可以充分利用jvm的锁优化机制,如锁消除、偏向锁、轻量级锁、自旋锁等
ReentrantLock可重入锁(Lock接口)--应用于锁竞争激烈
ReentrantReadWriteLock可重入读写锁(ReadWriteLock接口)--应用于锁竞争激烈,读多并且读写锁互斥的场景
StampedLock戳锁 --应用于锁竞争激烈,读多并且读写锁不互斥的场景