并发系列--Java中的各种锁

Java中的各种锁

Lock接口

Lock简介,地位,作用

锁是一种工具,用于控制对共享资源的访问

Lock和Synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同

Lock并不是用来代替synchronized的,而是当使用synchronized不合适或不满足要求的时候,来提供高级功能的。

Lock接口最常见的实现类是ReentraintLock

通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现也可允许并发访问,比如ReadWriteLock里面的ReadLock。

为什么Synchronized不够用?

效率低:锁的释放情况少、试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程。

不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能时不够的。

Lock中四个方法

lock(),tryLock(), tryLock(long time, TimeUnit unit)和lockInterruotibly()

第一个

lock()就是最普通的获取锁。如果锁已被其他线程获取,则进行等待
lock不会像synchronized一样在异常时自动释放锁
因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放

private static Lock lock02 = new ReentrantLock();

//lock不会像synchronized一样在异常时自动释放锁
//因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
@Test
public void test02(){
    lock02.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "开始执行任务");
    }finally {
        lock02.unlock();
    }
}

lock()方法不能被中断,这会带来很大的隐患,一旦陷入死锁,lock()就会陷入永久等待

第二个

tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败

相比于lock,这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为

该方法会立即返回,即便在拿不到锁时不会一直在那等

第三个

tryLock(long time, TimeUnit unit) :超时就放弃

避免死锁案例

public class ReentrantLockDemo {

    private Runnable01 runnable01 = new Runnable01(1);
    private Runnable01 runnable02 = new Runnable01(0);

    @Test
    public void test01() {
        Thread thread01 = new Thread(runnable01);
        Thread thread02 = new Thread(runnable02);
        thread01.start();
        thread02.start();
        while (thread01.isAlive() || thread02.isAlive()) {

        }
    }

}

class Runnable01 implements Runnable {

    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    int flag;

    public Runnable01(int flag){
        this.flag = flag;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程1获取到了锁1");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程1获取到了锁2");
                                    System.out.println("线程1成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock2.unlock();
                                }
                            } else {
                                System.out.println("线程1获取锁2失败,已重试");
                            }
                        } finally {
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程2获取到了锁2");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程2获取到了锁1");
                                    System.out.println("线程2成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock1.unlock();
                                }
                            } else {
                                System.out.println("线程2获取锁1失败,已重试");
                            }
                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

分析ReentrantLock的实现原理

第四个

lockInterruotibly(),相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断

    private static Lock lock03 = new ReentrantLock();
    private Runnable runnable03 = new Runnable() {
        @Override
        public void run() {
            try {
                lock03.lockInterruptibly();
                try{
                    System.out.println(Thread.currentThread().getName() + "获取到了锁");
                    Thread.sleep(5000);
                }catch (InterruptedException e){
                    System.out.println(Thread.currentThread().getName() + "睡眠期间被中断");
                }finally {
                    lock03.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了锁");
                }
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "等锁期间被中断");
            }

        }
    };

    @Test
    public void test03(){

        Thread thread = new Thread(runnable03);
        Thread thread02 = new Thread(runnable03);
        thread.start();
        thread02.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread02.interrupt();

        while (thread.isAlive() || thread02.isAlive()){

        }
    }

可见性问题

happens-before原则

Lock的加解锁和Synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

在这里插入图片描述
在这里插入图片描述

lock拥有可见性保证

锁的分类

这些分类,是从各种角度出发去看的

这些分类并不是互斥的,也就是多个类型可以并存;有可能一个锁,同时属于两种类型

比如ReentrantLock既是互斥锁,也是可以重入锁

好比一个人可以同时是男人也可以是军人

悲观锁/乐观锁

线程要不要锁住同步资源

定义

乐观锁,非互斥同步锁 不需要挂起线程
悲观锁,互斥同步锁

劣势

1.阻塞和唤醒带来的性能劣势
2.永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行
3.优先级反转,持有者的线程优先级比较低,阻塞的线程优先级比较高,会造成优先级反转

是否锁住资源角度分类

悲观锁

如果我不锁柱这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容的万无一失。

悲观锁流程

在这里插入图片描述
在这里插入图片描述

乐观锁

认为自己在处理操作的时候不会有其它线程来干扰,所以并不会锁住被操作对象

在更新的时候,去对比在我修改的期间有没有被其它人改变过。如果没有被改变过,就说明真的只有我自己在操作,那我就正常去修改数据

如果数据和我一开始拿到的不一样了,说明其它人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃,报错,重试等策略。

乐观锁的实现一般都利用CAS算法实现

乐观锁流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用场景

java中悲观锁的实现就是synchronizedLock相关类

乐观锁的典型例子就是原子类并发容器

int a;

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger();
    atomicInteger.incrementAndGet();
}

public synchronized void testMethod() {
    a++;
}

Git 乐观锁

Git就是乐观锁的典型例子,当我们往远端仓库push的时候,git就会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其它人修改了远端代码了,我们这次提交就失败;如果远端和本地版本号一致,我们就可以顺利提交版本到远端仓库。

Git不适用悲观锁

数据库 select for update 就是悲观锁

开销对比

悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响

相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

适用建议

悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
1、临界区有IO操作
2、临界区代码复杂或者循环量大

乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高

共享锁/排它锁

多线程能否共享同一把锁

什么是共享锁和排它锁

排它锁,又称为独占锁,独享锁

共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其它线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据

共享锁和排它锁的典型就是读写锁ReentraintReadWriteLock,其中读锁是共享锁,写锁是独享锁

读写锁作用

在没有读写锁之前,我们假设使用ReentraintLock,那么虽然我们保证了线程安全,但是也浪费了一定资源;多个读操作同时进行,并没有线程安全问题

在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率

读写锁规则

a)多个线程只申请读锁,都可以申请到
b)如果有一个线程已经占用了读锁,则此时其它线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
c)如果有一个线程已经占用了写锁,则此时其它线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
d)一句话总结:要么是一个或者多个线程同时又读锁,要么是一个线程有写锁,但是两则不会同时出现(要么多读,要么一写)

换一句话更容易理解:读写锁只是一把锁,可以通过两种方式锁定读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定

这里是把“获取写锁”理解为“把读写锁进行写锁定”,相当于是换了一种思路,不过原则是不变的,就是要么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不会同时出现

ReentrantReadWriteLock读写锁例子
public class CinemaReadWrite {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read(){
        readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write(){
        writeLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    @Test
    public void test01(){

        new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                write();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                write();
            }
        }).start();

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

选择规则

读线程插队

非公平:假设线程2和线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程5不在队列里,现在过来想要读取

此时有两种策略

1.读可以插队,效率高,写锁容易造成饥饿
2.避免饥饿

ReentrantReadWriteLock选择了策略2

公平锁:不允许插队
非公平锁:写锁可以随时插队,读锁仅在等待队列头节点不是想获得写锁的线程的时候可以插队

证明:读锁仅在等待队列头节点不是想获得写锁的线程的时候可以插队

public class CinemaReadWriteQueue {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->write(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->read(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
        new Thread(()->read(),"Thread5").start();
    }
}

升降级

为什么需要升降级

写锁的时候支持获取读锁,但是写锁不会释放

支持锁的降级,不支持升级:代码演示

public class Upgrading {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void readUpgrading() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
            System.out.println("升级会带来阻塞");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取到了写锁,升级成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void writeDowngrading() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
            readLock.lock();
            System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
//        System.out.println("先演示降级是可以的");
//        Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
//        thread1.start();
//        thread1.join();
//        System.out.println("------------------");
//        System.out.println("演示升级是不行的");
        Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
        thread2.start();
    }
}

**
降级提高效率,升级会造成死锁

总结

1.ReentrantReadWriteLock实现了ReadWriteLock接口,最主要的有两个方法:readLock和writeLock()用来获取读锁和写锁
2.锁的申请和释放策略
a) 多个线程只申请读锁,都可以申请到
b)如果有一个线程已经占用了读锁,则此时其它线程如果要申请写锁,则申请写锁的线程会一致等待释放读锁
c)如果有一个线程已经占用了写锁,则此时其它线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
d)要么是一个或者多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现

要么多读,要么一写

3.插队策略:为了防止饥饿,读锁不能插队
4.升降机策略:只能降级,不能升级
5.使用场合:相比于ReentrantLock适用于一般场合,ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进一步提高效率

公平锁/非公平锁

多线程竞争时,是否排队

什么是公平和非公平锁

公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定的情况下,可以插队

注意:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队

为什么要非公平锁

java设计者这样设计的目的,是为了提高效率

避免唤醒带来的空档期

公平的情况(以ReentraintLock为例)

如果在创建ReentraintLock对象时,参数填写为true,那么这就是个公平锁

public class FairLock {

    @Test
    public void test01(){
        PrintQueue queue = new PrintQueue();
        Thread thread[] = new Thread[10];

        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(queue));
        }

        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class Job implements Runnable{

    PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始打印");
        printQueue.printJob(new Object());
        System.out.println(Thread.currentThread().getName() + "打印完毕");
    }
}

class PrintQueue{
    private Lock queueLock = new ReentrantLock(true);

    public void printJob(Object document){
        queueLock.lock();
        try{
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
            Thread.sleep(duration * 100);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            queueLock.unlock();
        }

        queueLock.lock();
        try{
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
            Thread.sleep(duration * 100);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            queueLock.unlock();
        }
    }

}
特例

针对tryLock()方法,它是很猛的,它不遵守设定的公平的规则

例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其它现在等待队列里了.

优缺点
优势劣势
公平锁各线程公平平等,每个线程在等待一段时间后,总有执行的机会更慢,吞吐量更小
不公平锁更快,吞吐量更大有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行

可重入/不可重入锁

同一个线程是否可以重复获取同一把锁

可重入锁好处

避免死锁
提高封装性

public class RecursionDemo {

    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource() {
        lock.lock();
        try {
            System.out.println("已经对资源进行了处理");
            if (lock.getHoldCount()<5) {
                System.out.println(lock.getHoldCount());
                accessResource();
                System.out.println(lock.getHoldCount());
            }
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        accessResource();
    }
}

ReentrantLock底层实现AQS

isHeldByCurrentThread可以看出锁是否被当前线程持有

getQueueLength可以返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试时候使用,上线后用不到

不可重入:ThreadPoolExecutor的worker类

可中断锁/非可中断锁

是否可以中断

在java中,synchronized就不是可中断锁,而Lock是可中断锁,因为tryLock(time)和lockInterruptibly都能响应中断

自旋锁/非自旋锁

等锁的过程是否自旋

为什么需要自旋锁和非自旋锁

阻塞或唤醒一个java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间

如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长

如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁

而为了让当前线程"稍等一下",我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销.这就是自旋锁

阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况下,会直接把线程阻塞,直到被唤醒

如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源

在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的

AtomicInteger的实现

AtomicInteger的实现: 自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其它线程竞争导致没修改成功,就在while里死循环,直至修改成功

public class SpinLock {

    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {
            System.out.println("自旋获取失败,再次尝试");
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高

另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的

无锁 偏向锁 轻量级锁 重量级锁

这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。
首先为什么Synchronized能实现线程同步?
在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。

Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?
我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:
在这里插入图片描述

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
整体的锁状态升级流程如下:
在这里插入图片描述

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

锁优化

自旋锁和自适应

缩消除

锁粗化

我们写代码时候建议,如何优化

缩小同步代码块

尽量不要锁住方法

减少请求锁的次数

避免人为制造热点

锁中尽量不要再包含锁

选择合适的锁类型或合适的工具类

参考

1.面试官问我“Java中的锁有哪些?以及区别”,我跪了

2.java中的各种锁详细介绍

3.玩转Java并发工具,精通JUC,成为并发多面手

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值