共计 5795 个字符,预计需要花费 15 分钟才能阅读完成。
1. 锁之偏心锁与非偏心锁
2. 可重入锁(递归锁)
3. 自旋锁
4. 读锁写锁
1. 锁之偏心锁与非偏心锁
咱们先来理解一下,最根本的偏心锁和非偏心锁:
偏心锁:指多个线程按申请锁的程序来获取锁,相似排队打饭,先来后到。
非偏心锁:指多个线程获取锁的程序并不是依照申请的程序,有可能后申请的线程优先获取锁,在高并发的状况下,可能会造成优先级反转或者饥饿景象(饥饿景象就是线程永远获取不到锁)。
而对于 JAVA 罕用的 ReentrantLock 和 synchronize 锁而言,是偏心锁还是非偏心锁呢?
咱们先来看一下 ReentrantLock 的构造方法
Lock lock = new ReentrantLock();
/**
* 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) {
// 如果传入为 true,则是偏心锁,正文说会应用先来后到的排序策略
sync = fair ? new FairSync() : new NonfairSync();
}
接下来咱们再看一下 synchronize 关键字是偏心锁还是非偏心锁:
咱们先定义一个 synchronize 办法
public static synchronized void method01() {System.out.println(Thread.currentThread().getName() + "method01");
}
public static synchronized void method01() {System.out.println(Thread.currentThread().getName() + "method01");
}
接下来咱们循环启动多个线程,如果线程的编号是依照程序执行的,则证实 synchronize 是偏心锁,如果线程的编号是乱序执行的,则证实 synchronize 是非偏心锁。
for(int i =1;i<=10;i++){new Thread(()->{method01();},"t"+i).start();}
执行后果如下:
咱们能够得出,synchronized 也是非偏心锁。
2. 可重入锁(递归锁)
可重入锁 递归锁
可重入锁又叫递归锁,指的是同一线程在外层获取锁的时候,在进入内层办法会主动获取锁。是一种不会对其本身进行阻塞的锁,我晓得这么说比拟形象,接下来咱们间隔进行阐明。
咱们先写两个同步办法 A 和 B,其中 A 调用 B,如果锁是不可重入锁,因为线程调用办法 A 时,曾经获取锁,就没有方法获取办法 B 的锁了,然而可重入锁的话,线程调用同步办法 A,办法 A 调用同步办法 B,此时主动获取锁。
public static synchronized void method01() {System.out.println(Thread.currentThread().getName() + "method01");
method02();}
public static synchronized void method02() {System.out.println(Thread.currentThread().getName() + "method02");
}
new Thread(()->{method01();},"t1").start();
此时,线程不会造成死锁,而是顺利执行办法 A 和办法 B
接下来,咱们用 ReentrantLock 来示范一下可重入锁:
// 咱们应用这种模板来应用 ReentrantLock
lock.lock();
try {// 这里是办法体} finally {lock.unlock();
}
咱们新建一个 phone 对象,应用 ReentrantLock,而后再用一个同步办法调用另外一个同步办法
public class Phone implements Runnable {Lock lock = new ReentrantLock();
public void methodA() {lock.lock();
try {System.out.println(Thread.currentThread().getName() + "method01");
methodB();} finally {lock.unlock();
}
}
public void methodB() {lock.lock();
try {System.out.println(Thread.currentThread().getName() + "method02");
} finally {lock.unlock();
}
}
@Override
public void run() {methodA();
}
}
Phone phone = new Phone();
new Thread(phone, "t1").start();
new Thread(phone, "t2").start();
new Thread(phone, "t3").start();
new Thread(phone, "t4").start();
运行后果为:
能够看出,是可重入锁,而且并没有死锁。
然而如果此时,咱们批改一下办法会怎么样?
// 两个 lock
public void methodA() {lock.lock();
lock.lock();
try {System.out.println(Thread.currentThread().getName() + "method01");
methodB();} finally {
// 两个 unlock
lock.unlock();
lock.unlock();}
}
此时也能失常运行,然而如果加锁 lock 和解锁 unlock 的次数不一样,那就没有方法持续运行了,程序会死锁。
3. 自旋锁
自旋锁咱们在之前在介绍 CAS 的时候 JAVA 并发编程——CAS 概念以及 ABA 问题 介绍过,他指的是尝试获取锁的线程不会立刻阻塞,而是采纳循环的形式去获取锁,这样的益处是缩小上下文切换的耗费,毛病是会耗费 CPU。
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;
}
接下来咱们将用代码来自行模仿一把自旋锁:
首先,咱们须要一个原子援用类:
AtomicReference<Thread> atomicReference = new AtomicReference<>();
因为咱们等等要用到 atomicReference 的 compareAndSet 办法,再者对锁进行加锁和解锁的主体是线程,因为是线程获取锁,所以泛型是线程。
接下来定义一个加锁和解锁办法,利用自旋的形式加锁:
public void myLock() {Thread thread = Thread.currentThread();
// 先获取以后线程
//1. 如果援用类没有线程,则替换
// 替换后返回值为 true,因为加了取反,导致为 false,跳出循环,加锁胜利
//2. 如果原子援用类有线程,则返回 false
// 取反后取得 true,就能够有始终循环自旋的成果
while (!atomicReference.compareAndSet(null, thread)) { }
System.out.println(thread.getName() + "\t get lock!");
}
public void unLock() {Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + "\t unlock!");
}
接下来咱们新建两个线程运行一下
public static void main(String[] args) {SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {spinLockDemo.myLock();// 加锁
try {
// 五秒钟之类,第二个线程无奈进入
Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();
}
spinLockDemo.unLock();// 解锁}, "t1").start();
new Thread(() -> {spinLockDemo.myLock();// 加锁
spinLockDemo.unLock();// 解锁}, "t2").start();}
运行后果为:
4. 读锁写锁
此时咱们引入一个新的概念:读写锁。
从后面几种锁的应用和介绍状况来看,咱们每次只容许一个线程通过,其实效率还是挺多的,从实在的开发业务场景进行剖析,其实很多时候,只有保障写操作的排他性,无需保障读操作的排他性。
接下来咱们来模仿一遍读写锁,模仿一个键值对的缓存,应用读写锁
咱们要用到一个及其重要的类:
// 它有一个 lock.writeLock().lock()和 lock.readLock().lock()
// 办法,能够保障读操作的共享性和写操作的排他性。ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
咱们来书写一个 Map,用来当缓存空间,而后读写锁对该缓存进行操作。
// 因为是多线程操作, 记得保障它的可见性,必须采纳 volatile。private volatile Map<String, Object> map = new HashMap<>();
接下来就是最重要的读和写操作:
/**
* 写操作 原子性 独占性
*
* @param key
* @param value
* @throws InterruptedException
*/
public void put(String key, Object value) throws InterruptedException {
// 应用写锁
lock.writeLock().lock();
try {System.out.println(Thread.currentThread().getName() + "\t 正在写入" + key);
Thread.sleep(1000);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入实现");
} finally {lock.writeLock().unlock();}
}
/**
* 共享性,工夫不等
*
* @param key
* @return
* @throws InterruptedException
*/
public Object get(String key) throws InterruptedException {
// 应用读锁
lock.readLock().lock();
try {System.out.println(Thread.currentThread().getName() + "\t 正在读取" + key);
Thread.sleep(1000);
Object object = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取实现" + "对象为" + object.toString());
return object;
} finally {lock.readLock().unlock();}
}
接下来咱们试着运行一下:
public static void main(String[] args) {CacheDemo cacheDemo = new CacheDemo();
// 五个线程写,五个线程读,保障读互斥,写共享
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
try {cacheDemo.put(temp + "", temp +"");
} catch (InterruptedException e) {e.printStackTrace();
}
}, "t1").start();}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
try {cacheDemo.get(temp + "");
} catch (InterruptedException e) {e.printStackTrace();
}
}, "t2").start();}
}
从运行后果发现,写操作时,会一个一个线程进行写入,因为应用了 sleep 办法,会有显著的距离,实现一个再运行下一个,然而读线程因为是共享锁,就会一口气全副执行,依照工夫片轮转法,获取工夫片的线程随便进行读取。
总结:
总 1. 偏心锁:指多个线程按申请锁的程序来获取锁,相似排队打饭,先来后到。
**2. 非偏心锁:指多个线程获取锁的程序并不是依照申请的程序,有可能后申请的线程优先获取锁,在高并发的状况下,可能会造成优先级反转或者饥饿景象(饥饿景象就是线程永远获取不到锁)。
3. 可重入锁:可重入锁又叫递归锁,指的是同一线程在外层获取锁的时候,在进入内层办法会主动获取锁。是一种不会对其本身进行阻塞的锁。
4. 自旋锁:他指的是尝试获取锁的线程不会立刻阻塞,而是采纳循环的形式去获取锁,这样的益处是缩小上下文切换的耗费,毛病是会耗费 CPU。
5. 读写锁:保障写操作的排他性,无需保障读操作的排他性,保证系统吞吐量。