1、公平锁和非公平锁
公平锁是指 多个线程按照申请锁的顺序来获取锁,根据先来后到的规则进行排队等候 。
非公平锁是指 多个线程获取锁的顺序并不是按照申请锁的先后顺序,有可能后申请锁的线程比先申请锁的线程优先获得锁 ,在高并发环境下,有可能造成优先级反转或者饥饿现象。
饥饿现象:长时间未获取到锁
举例:ReentrantLock 可以指定构造函数参数来创建公平锁或者非公平锁,默认是非公平锁。
//非公平锁
ReentrantLock lock = new ReentrantLock();
//公平锁
ReentrantLock lock = new ReentrantLock(true);
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
两者区别:
- 公平锁 在并发环境中,每个线程在获取锁得锁时,会先查看此锁维护的等待队列,如果为空,或者当前线程就是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
- 非公平锁 不同的是,会一开始就尝试占有锁,如果尝试失败,再采用类似公平锁那种方法进行等待。synchronized 也是一种非公平锁。
非公平锁的优点是可以具有更大的吞吐量 。
2、可重入锁(递归锁)
可重入锁(递归锁)指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码;
在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁;
也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块 。
例如在两个加锁的方法中,methodA方法在获得锁以后调用methodB会直接获得methodB的锁。
![image-20210208162336434](https://gitee.com//xizhongren8980/BlogImages/raw/master/imgs/20210208162350.png)
ReentrantLock/synchronized 就是典型的可重入锁
可重入锁的最大作用是避免死锁。
验证可重入锁:
public class DemoClass {
public synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + "线程 执行了methodA方法");
methodB();
}
public synchronized void methodB() {
System.out.println(Thread.currentThread().getName() + "线程执行了methodB方法");
}
}
public class LockDemo2 {
public static void main(String[] args) {
DemoClass demo = new DemoClass();
new Thread(()->{
demo.methodA();
},"thread-A").start();
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("**************");
new Thread(()->{
demo.methodA();
},"thread-B").start();
}
}
运行结果:
thread-A线程 执行了methodA方法
thread-A线程执行了methodB方法
**************
thread-B线程 执行了methodA方法
thread-B线程执行了methodB方法
从结果可以得出,一个同步方法可以进入自己代码块中的另外一个同步方法,像这样的代码也就是可重入锁(递归锁)。
3、自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样做的好处是减少线程上下文切换的消耗,缺点是长时间未获取到锁导致的循环比较消耗CPU资源。
//Unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
在Unsafe.getAndAddInt 方法就采用了自旋锁,通过 do...while()
循环来不断获取内存中的真实值var5。
3.1 手写一个自旋锁
/**
* @ClassName: SpinLock
* @Auther: 戏中人
* @Description: 手写自旋锁
*/
public class SpinLock {
//原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//上锁
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程尝试获取锁");
while (!atomicReference.compareAndSet(null, thread)) {
}
System.out.println(Thread.currentThread().getName() + "线程成功获得锁");
}
//解锁
public void unLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "线程解锁了");
}
}
/**
* @ClassName: SpinLockDemo
* @Auther: 戏中人
* @Description: 手写自旋锁演示示例
*/
public class SpinLockDemo {
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
new Thread(()->{
spinLock.lock();
System.out.println("thread-A线程处理业务开始...");
//暂停5秒线程,模拟线程A处理业务
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("thread-A线程处理业务结束,花费5秒");
spinLock.unLock();
},"thread-A").start();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
spinLock.lock();
System.out.println("thread-B线程处理业务结束");
spinLock.unLock();
},"thread-B").start();
}
}
运行结果:
![image-20210209193442736](https://gitee.com//xizhongren8980/BlogImages/raw/master/imgs/20210209193447.png)
从结果分析可以得出,线程A在获得锁之后,线程B是无法再获得锁的, 因此线程B只能不断通过while循环尝试获得锁,直到线程A解锁后才跳出循环。在这个过程中,线程B并没有阻塞,而是不断循环尝试获得锁,像这样的设计就是自旋锁的体现。
4、独占锁、共享锁、互斥锁
独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁。
共享锁:指该锁可被多个线程所持有。
ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁
互斥锁:读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的
下面将通过一个仿缓存功能对ReentrantReadWriteLock 的功能进行演示:
/**
* @ClassName: MyCache
* @Auther: 戏中人
* @Description: 手写一个仿缓存类
*/
public class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
//模拟向缓存写入数据(未加锁)
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName() + "线程正在写入...");
//暂停线程1秒,模拟写入
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入完成!!!");
}
//模拟读取缓存中的数据(未加锁)
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "线程正在读取数据...");
//模拟读取数据
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "线程已读取到数据-" + value);
}
//模拟清除缓存
public void clear() {
map.clear();
}
}
/**
* @ClassName: ReadWriteLockDemo
* @Auther: 戏中人
* @Description: 读写锁示例
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 0; i < 5; i++) {
String temp = i + "";
new Thread(()->{
cache.put(Thread.currentThread().getName() + temp,Thread.currentThread().getName());
},"thread-"+i).start();
}
for (int i = 0; i < 5; i++) {
String temp = i + "";
new Thread(()->{
cache.get(Thread.currentThread().getName() + temp);
},"thread-" + i).start();
}
}
}
第一次运行未加锁的缓存,运行结果如下:
thread-0线程正在写入...
thread-1线程正在写入...
thread-2线程正在写入...
thread-3线程正在写入...
thread-4线程正在写入...
thread-0线程正在读取数据...
thread-1线程正在读取数据...
thread-2线程正在读取数据...
thread-3线程正在读取数据...
thread-4线程正在读取数据...
thread-1线程写入完成!!!
thread-0线程写入完成!!!
thread-4线程写入完成!!!
thread-3线程写入完成!!!
thread-2线程写入完成!!!
thread-1线程已读取到数据-thread-1
thread-4线程已读取到数据-thread-4
thread-3线程已读取到数据-null
thread-0线程已读取到数据-thread-0
thread-2线程已读取到数据-thread-2
从结果中可以看出,往缓存类中写入数据的线程被其他线程加塞了,出现了多个线程同时写,并且在未写入完成之前被其他线程打断进行读取等情况。出现这种情况显然是不符合我们的要求 ,即要求向缓存写入数据时只能同时有一个线程进行写入,并且在写入过程中不能被其他线程所中断。
因此现在对 MyCache
类的 put 和 get 方法进行分别添加ReentrantReadWriteLock中的writeLock()写锁
和readLock()读锁
,改进后的代码如下:
public class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//模拟向缓存写入数据
public void put(String key,Object value) {
//加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "线程正在写入...");
//暂停线程1秒,模拟写入
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入完成!!!");
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
//模拟读取缓存中的数据
public void get(String key) {
//加读锁
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "线程正在读取数据...");
//模拟读取数据
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "线程已读取到数据-" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
//模拟清除缓存
public void clear() {
map.clear();
}
}
再次运行查看结果:
thread-0线程正在写入...
thread-0线程写入完成!!!
thread-1线程正在写入...
thread-1线程写入完成!!!
thread-2线程正在写入...
thread-2线程写入完成!!!
thread-3线程正在写入...
thread-3线程写入完成!!!
thread-4线程正在写入...
thread-4线程写入完成!!!
thread-0线程正在读取数据...
thread-1线程正在读取数据...
thread-2线程正在读取数据...
thread-4线程正在读取数据...
thread-3线程正在读取数据...
thread-3线程已读取到数据-thread-3
thread-0线程已读取到数据-thread-0
thread-2线程已读取到数据-thread-2
thread-1线程已读取到数据-thread-1
thread-4线程已读取到数据-thread-4
这次的结果就是我们想要实现的,当一个线程在写入数据时,不能被其他线程所打断或者加塞,也就是只能同时有一个线程进行写入;当没有线程进行写入数据时,允许多个线程并发读取数据。