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.读写锁:保障写操作的排他性,无需保障读操作的排他性,保证系统吞吐量。