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时,
当执行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修饰的方法或代码块,是永远可以得到锁的。
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线程执行结束释放掉锁之后才能尝试获取 锁。
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的加解锁都要成对出现。
自旋锁(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(() -> { data.writeData(temp, temp); }, temp ).start(); } /* 线程操作资源类 创建4个读线程 */ for (int i = 0; i < 4; i++) { // lambda表达式内部必须是final final String temp = String.valueOf(i); new Thread(() -> { data.readData(temp); }, temp).start(); } } }
可以看到,写线程还没写完,就被其他线程打断。其他线程就开始 读/写,这就造成了数据的不一致。
由于上述方案是没有加锁的,所以在写线程进行写的时候会被其他线程打断,从而不具备 原子性,这时候就需要使用读写锁来解决了。
创建读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
获得写锁
Lock writeLock = readWriteLock.writeLock();
获取读锁
Lock readLock = readWriteLock.readLock();
ReadWriteLock 的读锁readLock是共享锁,允许多个线程同时读。但是其写锁writeLock是独占锁,写的时候只能一个线程进入写。
package LockTypeDemo; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class RWLockDemo { public static void main(String[] args) { Data data = new Data(); /* 线程操作资源类 创建4个写线程 */ for(int i = 0; i < 4; i ++){ // lambda表达式内部必须是final final int temp = i; new Thread(()->{ data.write(String.valueOf(temp), String.valueOf(temp) ); }, i + "").start(); } /* 线程操作资源类 创建4个读线程 */ for(int i = 0; i < 4; i ++){ // lambda表达式内部必须是final final int temp = i; new Thread(()->{ data.read(String.valueOf(temp)); }, i + "").start(); } } } //资源类 class Data{ //读写锁 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //注意,此处需要volatile修饰 ,使得map被其他线程可见 private volatile Map<String,String> map = new HashMap<>(); /** * 加入写锁之后使得写操作具有原子性 */ public void write(String k, String v){ //获取写锁 Lock writeLock = readWriteLock.writeLock(); writeLock.lock(); try { System.out.println("【线程"+Thread.currentThread().getName()+"】 正在写"); //模拟网络延时 休眠0.1秒 TimeUnit.MICROSECONDS.sleep(100); map.put(k, v); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("【线程"+Thread.currentThread().getName()+"】 写完毕"); writeLock.unlock(); } } public void read(String k){ //获取读锁 Lock readLock = readWriteLock.readLock(); readLock.lock(); try { System.out.println("【线程"+Thread.currentThread().getName()+"】 正在读"); //模拟网络延时 休眠0.1秒 TimeUnit.MICROSECONDS.sleep(100); System.out.println("【线程"+Thread.currentThread().getName()+"】 读"+"\t" +map.get(k)); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); } } }
执行结果:
写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是4个线程同时进入,然后并发读取的。