【架构师面试-JUC并发编程-5】-锁

1:synchronized和ReentrantLock的区别

1:含义不同

Synchronized 是关键字,属于 JVM 层面,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成;

Lock 是 java.util.concurrent.locks.lock 包下的,是 JDK1.5 以后引入的新 API 层面的锁;

2:使用方法不同

Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁;ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁;

3:等待是否可以中断

Synchronized 不可中断,除非抛出异常或者正常运行完成;ReentrantLock 可以中断。一种是通过 tryLock (long timeout, TimeUnit unit),另一种是 lockInterruptibly () 放代码块中,调用 interrupt () 方法进行中断;

4:加锁是否公平

Synchronized 是非公平锁;ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁;

2:锁的分类

 

3:乐观锁和悲观锁

1:悲观锁

悲观锁:synchronized和lock接口

适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗。典型情况:

①临界区有IO操作

②临界区代码复杂或循环量大

③临界区竞争非常激烈

2:乐观锁

乐观锁:典型例子就是原子类、并发容器等

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

4:可重入锁和非可重入锁

可重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

lock/reentrantlock/GetHoldCount.java

package lock.reentrantlock;
 
import java.util.concurrent.locks.ReentrantLock;
 
public class GetHoldCount {
    private  static ReentrantLock lock =  new ReentrantLock();
 
    public static void main(String[] args) {
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
}

在递归调用中可重入锁的用法

lock/reentrantlock/RecursionDemo.java

package lock.reentrantlock;
import java.util.concurrent.locks.ReentrantLock;
 
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();
    }
}

5:公平锁和非公平锁

1:概述

公平指的是按线程请求的顺序来分配锁

非公平是指不完全按请求的顺序,在一定情况下可以插队。

2:为什么会设计非公平锁

为提高效率,避免唤醒带来的空档期

例如排队买火车票:早期买火车票需要通宵排队买票,如果我排在第2个位置,在开始卖票时,第一个位置的人买完票后轮到我买,但因为排了一个通宵,我有点不清醒发呆中(线程从阻塞状态唤醒),此时第一个人突然又回来去问下发车的时间,这时可以让第一个人插队,因为他的插队并不影响我买票,我还处于慢慢变清醒状态中。

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

lock/reentrantlock/FairLock.java

package lock.reentrantlock;
 
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/**
 * 描述:     演示公平和不公平两种情况
 */
public class FairLock {
 
    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
        Thread thread[] = new Thread[10];
        //10个线程去打印
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue));
        }
        //启动10个线程
        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } 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(false);
 
    //每份文档打印两份,模拟线程在完成第一次打印后,插队打印第二次
    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 * 1000);
        } 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 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }
}

6:自旋锁和阻塞锁

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

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

在两个或以上线程同时并行执行时,可以让请求锁的线程不放弃CPU的执行时间,看持有锁的线程是否很快会释放锁。为让当前线程等一下,需要让当前线程进行自旋,如果在自旋完成后,锁定同步资源的线程释放锁,则当前线程就可以不必阻塞而是直接获取同步资源,避免切换线程的开销。

1:自旋锁的实现原理

是lock方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

2:自旋锁存在的问题

如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

7:锁优化

1:减少锁持有时间

2:减少锁粒度

将大对象拆分成小对象,增加并行度,降低锁竞争。

ConcurrentHashMap允许多个线程同时进入

3:锁分离

根据功能进行锁分离

ReadWriteLock在读多写少时,可以提高性能。

4:锁消除

锁消除是发生在编译器级别的一种锁优化方式。

有时候我们写的代码完全不需要加锁,却执行了加锁操作。

5:锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是在某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不要迷恋发哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值