Java高并发学习—— Java锁

Posted Johnny*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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时,

  1. 如果目标锁对象的计数器为0,说明该锁对象没有被其他线程所持有。Java虚拟机会将该锁对象的持有线程置为当前线程,并且将其计数器加1.
  2. 如果目标对象的计数器不为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高并发,如何解决,啥方式解决,高并发

乐观锁和悲观锁的使用场景及应用——Java高并发系列学习笔记

乐观锁和悲观锁的使用场景及应用——Java高并发系列学习笔记

java高并发锁的3种实现

Java实战分布式锁实战之重现高并发场景一

synchronized学习