介绍这几种锁之前,先了解一下jdk1.8的并发相关的包,今天我们了解l这几种锁都在looks包下
可以看到locks包下主要有三个接口,可重入锁介绍的Lock接口的主要实现类ReentrantLock
lock与synchronized的区别
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
1,可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。可重入锁的意义之一在于防止死锁(各自持有对方的锁)。
//可重入锁
public class ReentrantLockDemo{
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"a").start();
new Thread(()->{
phone.sms();
},"b").start();
}
}
class Phone{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName()+"----sms");
call();//这个方法也有锁
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"---call");
}
}
class Phone2{
Lock lock = new ReentrantLock();
public void sms(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"----sms");
call();//这个方法也有锁
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"---call");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
代码解释:按道理来说A线程输出sms调call方法前就释放sms方法的锁,然后在B线程参与锁的竞争,可能会产生的的输出结果是 Asms Bsms Acall Bcall,但是输出的结果永远都是A先sms,call Bsms,call就是因为A线程并不是在调用call方法前释放了sms的锁而是在call方法执行完才释放了sms的锁,很多人就会问他不是还得获取call方法的锁,这就是可重入锁,他并不用获取call方法的锁,相当于获得的大门了钥匙,大门里面的小门钥匙就不用获取
2,读写锁,也称共享锁与独占锁(synchronized),共享锁见名知意,共享嘛,可以被多个线程去读,独占锁,写的时候只有一个线程去写
ReadWriteLock接口的主要实现ReentrantReadWriteLock ,
没加锁之前运行下面这段代码可以看到运行结果
public class ReadWriteLockDemo {
private volatile Map<String, Object> map = new HashMap<>();
//写入,我只希望同时有只有一个线程写
public void put (String key,Object value){
System.out.println(Thread.currentThread().getName()+"写入---》"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入ok---》"+value);
}
//读 我希望有多个线程可以读
public void get(String key){
System.out.println(Thread.currentThread().getName()+"读取---》"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功---》"+o);
}
public static void main(String[] args) {
ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
//写入
for (int i = 1; i <=5 ; i++) {
final int temp = i;
new Thread(()->{
readWriteLockDemo.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <=5 ; i++) {
final int temp = i;
new Thread(()->{
readWriteLockDemo.get(temp+"");
},String.valueOf(i)).start();
}
}
可以清晰的在多线程的情况下问题很大,1还没有写入成功就要读取成功了,而且1在写入的时候被其他线程插队,下面我们用读写锁改进
public class ReadWriteLockDemo {
private volatile Map<String, Object> map = new HashMap<>();
//读写锁,更加细粒的控制线程
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//写入,我只希望同时有只有一个线程写 原子性操作
public void put (String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入---》"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入ok---》"+value);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//读 我希望有多个线程可以读 非原子性操作 可以插队
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取---》"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功---》"+o);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
public static void main(String[] args) {
ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
//写入
for (int i = 1; i <=5 ; i++) {
final int temp = i;
new Thread(()->{
readWriteLockDemo.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <=5 ; i++) {
final int temp = i;
new Thread(()->{
readWriteLockDemo.get(temp+"");
},String.valueOf(i)).start();
}
}
加了读写锁之后,发现写入他是一个原子性操作,而读取不是,他允许插队
3,公平锁FairSync与非公平锁NonfairSync
非公平锁,可以线程插队执行,假设一个线程先执行,要3小时才执行完,一个只需要3秒的线程,如果是公平锁那就必须等3个小时线程执行完才会执行3秒的,可能会导致性能倒置问题,有的时候我们希望3秒的先执行就使用非公平锁。点进ReentrantLock源码我们不难发现,它其实有两个构造方法一个有参一个无参的构造方法,无参的构造方法默认是new了一个非公平锁,有参的需要自己传一个boolean值来设置true为公平锁false为非公平锁,在进入方法仔细观察他们的区别
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
可以看到,公平锁的tryAcquire实现和非公平锁的tryAcquire实现的区别在于:公平锁多加了一个判断条件:hasQueuedPredecessors,如果发现有线程在等待获取锁了,那么就直接返回false,否则在继承尝试获取锁,这样就保证了线程是按照排队时间来有限获取锁的。而非公平锁的实现则不考虑是否有节点在排队,会直接去竞争锁,如果获取成功就返回true,否则返回false。
当然,这些分支执行的条件是state为0,也就是说当前没有线程独占着锁,或者获取锁的线程就是当前独占着锁的线程,如果是前者,就按照上面分析的流程进行获取锁,如果是后者,则更新state的值,如果不是上述的两种情况,那么直接返回false说明尝试获取锁失败。
公平锁的lock使用了AQS的acquire,而acquire会将参与锁竞争的线程加入到等待队列中去按顺序获得锁,队列头部的节点代表着当前获得锁的节点,头结点释放锁之后会唤醒其后继节点,然后让后继节点来竞争获取锁,这样就可以保证锁的获取是按照一定的优先级来的。而非公平锁的实现则会首先尝试去竞争锁,如果不成功,再走AQS提供的acquire方法,非公平锁的tryAcquire方法使用了父类的nonfairTryAcquire方法来实现。
3,自旋锁(乐观锁)
所谓自旋锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。
public class OptimisticLockDemo {
//Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void mylock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==>mylock");
//自旋锁
while(!atomicReference.compareAndSet(null, thread)){
}
}
//解锁
public void myUnlock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==>myUnlock");
atomicReference.compareAndSet(thread, null);
}
public static void main(String[] args) throws InterruptedException {
OptimisticLockDemo optimisticLockDemo = new OptimisticLockDemo();
new Thread(()->{
optimisticLockDemo.mylock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
optimisticLockDemo.myUnlock();
}
},"T1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
optimisticLockDemo.mylock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
optimisticLockDemo.myUnlock();
}
},"T2").start();
}
}
代码中T1线程先获取锁,把null改为thread 然后睡眠5秒在解锁把thread改为null,T2线程在T1获取锁的未释放锁的时候,进入加锁的方法时会一直自旋,直到T1解锁完才能获取锁,他是一种乐观的形态,一直去尝试去获取锁,
而与synchronized和WriteLock不同的是synchronized与WriteLock他只要有线程获取锁,其他线程必须等待他解锁完才能去竞争锁,是一种悲观行为