Java高并发学习—— Java锁
Posted Johnny*
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高并发学习—— Java锁相关的知识,希望对你有一定的参考价值。
Java锁
https://github.com/yangjinwh/interview 第二季脑图
公平锁与非公平锁
理论
公平锁: 就是按照多线程请求锁的顺序来获取锁。FIFO。与生活中的食堂排队打饭一样,遵循先来后到原则。
非公平锁: 是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能先申请的线程后得到锁,即允许插队(线程优先级翻转)或者线程饥饿(某个线程一直得不到锁)。
如何创建
通过ReentrantLock类的构造方法指定boolean类型参数的值,true表示公平锁,false表示非公平锁。默认是非公平锁。
区别
公平锁就是很公平。在 并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个。就占有锁。否则就会加入到等待队列中,以后按照FIFO的规则由JVM调度。
非公平锁比较粗鲁,上来直接就尝试占用锁,如果尝试失败,在采用类似公平锁的那种方式。
另外ReentrantLock之所以默认是非公平锁,是因为非公平锁比公平锁的吞吐量要大。
可重入锁
理论
可重入锁也叫递归锁。指的是同一线程在外层方法获得锁之后,在进入内层方法是会自动获取锁。也就是说,线程可以进入任何一个已经拥有的锁所同步着的代码块,不会因为之前已经获取过还没有释放而阻塞。可以用下面例子来理解这句话:
public synchronized void method1() { method2();}
public synchronized void method2() {}
线程获取了method1的锁之后,由于method2也是该锁所同步的代码块,所以可以直接进入,自动获得该锁。实际上是对该锁的引用计数器加1.
可重入锁的种类
可重入锁可以分为隐式锁和显式锁。隐式锁是 指的synchronized这类由JVM调控加锁和解锁的锁,默认是可重入锁。显式锁是ReentrantLock由调用者自己加解锁的这样一类锁。隐式锁可以理解为汽车的自动挡,是汽车控制好的了。显式锁可以理解为汽车的手动挡,需要自己把控。
原理
package LockTypeDemo;
/**
* @author Johnny Lin
* @date 2021/6/13 12:38
*/
public class Lock_SyncDemo {
private final Object objectLock = new Object();
public void m1(){
synchronized (objectLock){
System.out.println("---------synchronized code block -----------");
}
}
public static void main(String[] args) {
}
}
进入Lock_SyncDemo.class所在目录下后,使用javap -c Lock_SyncDemo.class反编译。
每一个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,
- 如果目标锁对象的计数器为0,说明该锁对象没有被其他线程所持有。Java虚拟机会将该锁对象的持有线程置为当前线程,并且将其计数器加1.
- 如果目标对象的计数器不为0,如果锁对象的持有线程是当前线程,那么Java虚拟机会将其计数器加1。否则需要等待,直至持有线程释放该锁。这也是同一线程可重入同一对象锁所管控的代码块/方法的原因。
当执行monitorexit是,Java虚拟机则需要将锁对象的计数器减1,当计数器为0时,代表锁已经被释放了。
对应到Java代码实现,如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
/*可重入的体现
如果当前队列中线程是当前线程
*/
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
作用
可重入锁的作用是避免死锁,比如说上面例子。method1调用method2,如果不是可重入锁,那么method2会一直等待着method1释放掉它所占用的锁,而method1却又一直等着method2执行,就会进入死锁。
可重入验证
验证synchronized
在一个synchronized修饰的方法或代码块内部,调用本类的其他synchronized修饰的方法或代码块,是永远可以得到锁的。
package LockTypeDemo;
/**
* @author Johnny Lin
* @date 2021/6/12 22:56
*/
public class RenentrantLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.senEmail();
}, "A").start();
new Thread(() -> {
phone.senEmail();
}, "B").start();
}
}
//资源类
class Phone{
public synchronized void senEmail(){
System.out.println(Thread.currentThread().getName() + "\\t invoked sendEmail()");
//调用另一个同步方法
sendMsg();
}
public synchronized void sendMsg(){
System.out.println(Thread.currentThread().getName()+"\\t invoked sendMsg()");
}
}
执行结果
这就说明A线程在进入sendEmail()方法时获取了锁,之后调用同一把锁控制的代码块是会自动获得该锁。而B线程只有等待A线程执行结束释放掉锁之后才能尝试获取 锁。
验证Lock
Phone2资源类实现Runnable接口,run方法中调用sendEmail(),在sendEmail()中调用sendMsg()同步方法。
package LockTypeDemo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Johnny Lin
* @date 2021/6/12 23:12
*/
public class ReentrantLockDemo2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
Thread t1 = new Thread(phone,"t1");
Thread t2 = new Thread(phone,"t2");
t1.start();
t2.start();
}
}
class Phone2 implements Runnable{
Lock lock = new ReentrantLock();
public void sendEmail(){
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\\t invoked sendEmail()");
sendMsg();
} finally {
lock.unlock();
lock.unlock();
}
}
public void sendMsg(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\\t invoked sendMsg()");
} finally {
lock.unlock();
}
}
@Override
public void run() {
sendEmail();
}
}
执行结果:
如果sendEmail()方法中加锁和解锁都出现 两次会 怎么样?
public void sendEmail(){
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\\t invoked sendEmail()");
sendMsg();
} finally {
lock.unlock();
lock.unlock();
}
}
执行结果,正常结束。
如果sendEmail()方法中加锁两次而解锁只出现一次会 怎么样?
public void sendEmail(){
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\\t invoked sendEmail()");
sendMsg();
} finally {
lock.unlock();
}
}
执行结果:
这很好理解,因为t1线程一直占用着锁没有释放,所以另一个线程t2因为得不到锁自然就会阻塞。与下面sendMsg()加锁两次只解锁一次是相同的。
public void sendMsg(){
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\\t invoked sendMsg()");
} finally {
lock.unlock();
}
}
如果加锁一次而解锁了两次会怎么样?
public void sendEmail(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\\t invoked sendEmail()");
sendMsg();
} finally {
lock.unlock();
lock.unlock();
}
}
执行结果:
t1 invoked sendEmail()
t1 invoked sendMsg()
t2 invoked sendEmail()
t2 invoked sendMsg()
Exception in thread "t1" Exception in thread "t2" java.lang.IllegalMonitorStateException
【总结】
所以Lock的加解锁都要成对出现。
自旋锁(spinlcok)
理论
自旋锁(spinlock)是指尝试获取锁的线程不会立即阻塞而是采用循环的方式去获取锁。
好处是减少线程上下文切换的消耗,并且保证了并发量。
缺点 当不断自选的线程越来越多时,循环等待会不断消耗CPU资源。
在多线程环境下,CPU采取的策略是为每个线程分配时间片并轮转的形式。
上下文切换就是当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己状态,以便下次再切换回这个任务时,可以在 加载这个任务过程就是一次上下文切换。
自己实现自旋锁
package LockTypeDemo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Johnny Lin
* @date 2021/6/13 0:01
*/
public class SpinLockDemo {
//对象线程的原子引用
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock(){
System.out.println(Thread.currentThread().getName() + "\\t come in myLock");
Thread current = Thread.currentThread();
// 如果当前已经有线程占有了锁 则自旋
while( !atomicReference.compareAndSet(null, current)){}
System.out.println(Thread.currentThread().getName() + " get lock");
}
public void myUnLock(){
System.out.println(Thread.currentThread().getName() + "\\t come in myUnLock### ");
Thread current = Thread.currentThread();
while( !atomicReference.compareAndSet(current, null)){}
System.out.println(Thread.currentThread().getName() + " release lock");
}
public static void main(String[] args) {
SpinLockDemo sl = new SpinLockDemo();
new Thread(() -> {
sl.myLock();
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
sl.myUnLock();
}, "A").start();
//main线程休眠1秒钟 保证A线程优于B线程启动
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
sl.myLock();
sl.myUnLock();
}, "B").start();
}
}
执行结果:
共享锁和排他锁
理论
共享锁: 也叫读锁。是指该锁允许被多个线程持有。读锁就是一种共享锁。
排他锁: 也叫独占锁。 是指该锁一次只能被一个线程所持有。Synchronized和Lock都是排他锁。写锁也是一种独占锁。
对于 读写锁ReentrantReadWriteLock来说。其读锁readLock就是共享锁,允许多个人同时读。但是其写锁writeLock是独占锁,写的时候只能一个人写。
【读写锁的应用场景】
以前我们使用ReentrantLock创建锁的时候,是独占锁,也就是说一次 只能有一个线程持有该锁。但是存在一种读写分离的场景。也就是读取共享资源时向同时进行,而想去写共享资源时,不希望再有其它线程可以对该资源进行读或写。如果还是使用以前独占锁的方案的话,读的并发性会很差。
因此上述场景可以使用读写锁解决。读写锁用于:
读-读 : 能共存使,用共享锁
写-写 :不能共存,使用排他锁
读-写 : 不能共存,使用排他锁
代码
不加锁
package LockTypeDemo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Johnny Lin
* @date 2021/6/13 10:19
*
* 内部类如果不为static 则成员变量也不能有static
* Inner classes cannot have static declarations
*/
public class RWNoLock {
//资源类
static class Data{
Map<String,String> map = new HashMap<>();
public void writeData(String k, String v){
try { TimeUnit.MICROSECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("【线程"+Thread.currentThread().getName() +"】"+ "\\t 正在写");
map.put(k, v);
System.out.println("【线程"+Thread.currentThread().getName() +"】"+"\\t 写完");
}
public void readData(String k){
try { TimeUnit.MICROSECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("【线程"+Thread.currentThread().getName() +"】" + " 读 \\t "+ map.get(k));
}
}
public static void main(String[] args) {
Data data = new Data();
/*
线程操作资源类
创建4个写线程
*/
for (int i = 0; i < 4; i++) {
// lambda表达式内部必须是final
final String temp = String.valueOf(i);
new Thread((以上是关于Java高并发学习—— Java锁的主要内容,如果未能解决你的问题,请参考以下文章
乐观锁和悲观锁的使用场景及应用——Java高并发系列学习笔记