011Java并发包017锁总结

1 分类

1.1 说明

在很多并发文章中,会提及各种各样锁,如公平锁、乐观锁等等,各种锁的分类如下:

可重入锁
独享锁/共享锁
互斥锁/读写锁
公平锁/非公平锁
乐观锁/悲观锁
分段锁
自旋锁

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

1.2 可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

对于synchronized和ReentrantLock而言,都是可重入锁。

可重入锁的一个好处是可一定程度避免死锁,如果不是可重入锁的话,可能造成死锁。

1.3 独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。

对于synchronized和ReentrantLock而言,都是独享锁。

但是对于ReadWriteLock而言,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

1.4 互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是ReentrantLock。读写锁在Java中的具体实现就是ReadWriteLock。

1.5 公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序。非公平锁的优点在于吞吐量比公平锁大。

对于synchronized而言,是一种非公平锁。

对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。

1.6 乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。悲观锁在Java中的使用,就是利用各种锁。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

1.7 分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于JDK1.8以前的ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

1.8 自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

2 分级

指锁的状态,是通过对象监视器在对象头中的字段来表明的。一共有四种:无锁,偏向锁,轻量级锁,重量级锁。

锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

2.1 无锁

程序没有锁的竞争,此时是无锁状态。

2.2 偏向锁

一段同步代码一直被一个线程所访问,那么该线程在后续访问同步代码时,会自动获取偏向锁。

线程在获取对象的偏向锁后,会在对象头的对象标记中设置指向当前线程的指针,当前线程再次进入同步代码时,比较对象头中存储的指针即可,不需要再次加锁和释放锁,提高了性能。

当线程发现指针指向的线程不是当前线程时,意味着发生了竞争,此时偏向锁就会升级为轻量锁。

产生竞争时,持有偏向锁的线程会释放锁,如果没有竞争,线程只有在同步代码执行完后才会释放偏向锁。

偏向锁可以降低获取锁的代价,提高了只有一个线程执行同步时的代码执行的效率。

2.3 轻量级锁

线程在获取偏向锁后,同步代码被另一个线程所访问,偏向锁就会被释放并升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁。

轻量级锁是为了在线程近乎交替执行同步代码时提高性能,通过自旋的形式不会阻塞,但如果线程始终获取不到资源,长时间使用自旋会消耗CPU。

2.4 重量级锁

当轻量级锁自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。

重量级锁会让其他申请的线程进入阻塞,响应时间缓慢,执行同步代码块时间较长,但重量级锁不会使用自旋,也就不会导致消耗CPU。

3 synchronized特性

3.1 性能变化

在JDK1.5以前,使用synchronized关键字加锁是操作系统级别的重量级操作,其底层依赖对象监视器和操作系统互斥锁,挂起线程和恢复线程都需要转入内核态去完成,阻塞线程和唤醒线程都需要由操作系统切换CPU状态来完成,这种切换的时间成本相对较高,这也是为什么早期synchronized操作效率低的原因。

在JDK1.6之后,为了减少获得锁和释放锁所带来的性能消耗引入了轻量级锁和偏向锁,通过对象头中的对象标记根据锁标志位的不同进行复用,并且新增了锁升级策略。

3.2 锁消除

假如方法中的synchronized代码块锁住的对象时方法中定义的一个局部对象,而不是一个共享对象,那编译器就会无视代码块,相当于并没有对锁住的对象加锁,消除了锁的使用。

public class DemoTest {
    public static Object obj = new Object();
    public void method() {
        Object o = new Object();
        synchronized (o) {
            System.out.println(o.hashCode() + " " + obj.hashCode());
        }
    }
    public static void main(String[] args) {
        DemoTest demo = new DemoTest();
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                demo.method();
            }, String.valueOf(i)).start();
        }
    }
}

synchronized关键字锁住的是局部对象,不是多个线程使用的共享对象,所以不会对同步代码块加锁。

3.3 锁粗化

假如方法中前后相邻的synchronized代码块都是同一个锁对象,那编译器就会把这几个代码块合并成一个更大的代码块,加粗加大范围,一次申请锁使用即可,避免多次的申请和释放锁,提升了性能。

public class DemoTest {
    public static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (obj) {
                System.out.println("1");
            }
            synchronized (obj) {
                System.out.println("2");
            }
            synchronized (obj) {
                System.out.println("3");
            }
        }, "a").start();
        new Thread(()->{
            synchronized (obj) {
                System.out.println("4");
            }
            synchronized (obj) {
                System.out.println("5");
            }
            synchronized (obj) {
                System.out.println("6");
            }
        }, "b").start();
    }
}

多个同步代码块锁住的是同一个共享对象,所以会将这几个同步代码块合并成一个代码块处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值