多线程_锁(八股文)

在上篇博客中我总结了有关多线程的基础知识点,今天这篇博客我来总结一下有关“锁”更详细的知识,一起来看

目录

常见锁策略

乐观锁VS悲观锁

普通的互斥锁VS读写锁

重量级锁VS轻量级锁

自旋锁VS挂起等待锁

公平锁VS非公平锁

可重入锁 vs 不可重入锁

CAS

CAS应用场景

ABA问题

synchronized原理

基本特征

加锁过程(锁升级)

锁消除

锁粗化

 Callable接口

理解Callable

JUC的常见类

ReentrantLock(可重入锁)

synchronized与ReentrantLock的区别

原子类 

 线程池

 ExecutorService 和 Executors 

ThreadPoolExecutor 构造方法

信号量 Semaphore

CountDownLatch 

线程安全的集合类

多线程环境使用 ArrayList

多线程下使用队列

多线程环境使用哈希表

concurrentHashMap的优点

经典面试题:Hashtable和HashMap、ConcurrentHashMap 之间的区别

死锁

产生死锁的必要条件

破坏循环等待


常见锁策略

加锁的时候的咋加的?

乐观锁VS悲观锁

乐观锁:预测接下来锁冲突的概率不大

悲观锁:预测接下来锁冲突的概率很大

synchronized自适应锁既是乐观锁又是悲观锁,当前锁冲突概率不大以乐观锁的方式运行,往往是纯用户态执行。

一旦发现锁冲突概率大了,以悲观锁的方式运行,进入内核对当前线程进行挂起等待

普通的互斥锁VS读写锁

synchronized就属于普通的互斥锁,两个加锁操作之间发生竞争

读写锁将加锁操作细化了,分为了“加读锁”,“加写锁”

在Java标准库中提供了ReentrantReadWriteLock类,来实现加读写锁

ReentrantReadWriteLock.ReadLock 类表示一个读锁 . 这个对象提供了 lock / unlock 方法进行
加锁解锁 .
ReentrantReadWriteLock.WriteLock 类表示一个写锁 . 这个对象也提供了 lock / unlock 方法进
行加锁解锁
注意只要在多线程下有线程涉及到写就会引发线程安全问题,读写锁特别适合于 " 频繁读, 不频繁写 " 的场景中

重量级锁VS轻量级锁

重量级锁:锁开销大,做的工作多,主要依赖操作系统提供的锁容易产生阻塞等待

轻量级锁:锁开销小,做的工作少,尽量在用户态完成加锁,避免用户态与内核态的切换,避免挂起等待

synchronized是自适应锁,既是重量级锁又是轻量级锁,根据冲突情况

自旋锁VS挂起等待锁

自旋锁是轻量级锁的具体实现,当发生锁冲突会迅速尝试获取锁

挂起等待锁是重量级锁的具体实现,发生冲突就挂起等待

synchronized作为轻量级锁的时候内部是自旋锁,作为重量级锁的时候内部是挂起等待锁

公平锁VS非公平锁

注意此处的公平表示为“先来后到”,synchronized 是非公平锁

操作系统的内部对于挂起等待锁是非公平的,如果想使用公平锁就要使用数据结构来控制实现

可重入锁 vs 不可重入锁

允许同一个线程多次获取同一把锁

为解决上诉的问题就引入了可重入锁 ,可重入锁在内部记录锁是哪个线程获取到的,当发现加锁线程与持有锁的线程是同一个则会让直接获取,同时还会在锁内部加个计数器,记录是第几次加锁,通过计数器来控制释放锁

 Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的

CAS

CAS是操作系统/硬件给JVM提供的另一种更轻量的原子操作的机制

CAS是CPU提供的一个特殊指令

boolean CAS(address, expectValue, swapValue) {//CAS实现的伪代码
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
 }

注意以上是伪代码通过Java代码实现的,实际是由一个CPU指令完成的

CAS应用场景

1.实现原子类

public static AtomicInteger count=new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++) {
                count.getAndIncrement();//count++;
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

2实现自旋锁(伪代码)

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

ABA问题

A->A

A->B->A(A修改数据为B后又改回A)

给要修改的数据引入版本号, 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期,如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增,如果发现当前版本号比之前读到的版本号大, 就认为操作失败

synchronized原理

基本特征

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁

加锁过程(锁升级)

无锁(没加锁)

偏向锁(刚开始加锁,未产生竞争)标记表示加锁

轻量级锁(产生锁竞争)用户态自旋

重量级锁(激烈的锁竞争)内核态挂起等待

锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

每个append都会加锁解锁但是在单线程下这是白白浪费的资源

锁粗化

一段逻辑中如果出现多次加锁解锁 , 编译器 + JVM 会自动进行锁的粗化

 Callable接口

Callable 是一个接口,   相当于把线程封装了一个 " 返回值 "
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {//重写call()方法
        int sum = 0;
        for (int i = 1; i <= 1000; i++) {
            sum += i;
       }
        return sum;
   }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);//包装一下
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();//get()获取结果,拿到结果前阻塞等待
System.out.println(result);

理解Callable

Callable Runnable 相对 , 都是描述一个 " 任务 ". Callable 描述的是带有返回值的任务 ,
Runnable 描述的是不带返回值的任务 .
Callable 通常需要搭配 FutureTask 来使用 . FutureTask 用来保存 Callable 的返回结果 . 因为
Callable 往往是在另一个线程中执行的 , 啥时候执行完并不确定 .
FutureTask 就可以负责这个等待结果出来的工作

JUC的常见类

ReentrantLock(可重入锁)

可重入互斥锁 synchronized 定位类似 , 都是用来实现互斥效果 , 保证线程安全
 public static void main(String[] args) {
        ReentrantLock locker=new ReentrantLock();
        try {
            locker.lock();//加锁
           
        }finally{
            locker.unlock();//解锁
        }
    }

synchronized与ReentrantLock的区别

1.synchronized单纯的关键字,以代码块为单位进行加锁解锁

  ReentranLock则是一个类,提供了lock方法加锁,unlock方法解锁
2.synchronized是非公平锁,ReentranLock默认是非公平锁但是可以在构造实例时通过指定一个参数,切换到公平锁模式, 构造方法传入一个 true 开启
3.synchronized 使用时不需要手动释放锁 . ReentrantLock 使用时需要手动释放
4.ReentranLock提供了一个特殊的加锁操作,tryLock();默认的lock()加锁失败就阻塞,而使用tryLock() 加锁失败不会阻塞继续执行并返回false ,另外tryLock还能够设置等待时间
5. 更强大的唤醒机制 . synchronized 是通过 Object wait / notify 实现等待/ 唤醒 . 每次唤醒的是一 个随机等待的线程 . ReentrantLock 搭配 Condition 实现等待/ 唤醒 , 可以更精确控制唤醒某个指 定的线程 .

原子类 

原子类内部用的是 CAS 实现,之前介绍过这里总结方法, AtomicInteger 举例,常见方法有

addAndGet(int delta);   i += delta;
decrementAndGet();     --i;
getAndDecrement();     i--;
incrementAndGet();     ++i;
getAndIncrement();      i++;

 线程池

某个线程不再使用了 , 并不是真正把线程释放 , 而是放到一个 " 池子 " 中, 下次如果需要用到线程就直接从池子中取 , 不必通过系统来创建了

 ExecutorService 和 Executors 

ExecutorService 表示一个线程池实例 .
Executors 是一个工厂类 , 能够创建出几种不同风格的线程池
newFixedThreadPool(): 创建固定线程数的线程池
newCachedThreadPool(): 创建线程数目动态增长的线程池
newSingleThreadExecutor(): 创建只包含单个线程的线程池
newScheduledThreadPool(): 设定 延迟时间后执行命令,或者定期执行命令 . 是进阶版的 Timer
ExecutorService submit() 方法能够向线程池中提交若干个任务
Executors 本质上是 ThreadPoolExecutor 类的封装

ThreadPoolExecutor 构造方法

 

ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                                              new SynchronousQueue<Runnable>(),
                                              Executors.defaultThreadFactory(),
                                              new
ThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
    pool.submit(new Runnable() {
        @Override
        void run() {
            System.out.println("hello");
       }
   });
}

信号量 Semaphore

信号量 , 用来表示 " 可用资源的个数 ". 本质上就是一个计数器
1.申请一个可用资源,信号量-=1,称为p操作
2.释放一个可用资源,信号量+=1,称为v操作
当信号量为0时在申请就会阻塞等待, Semaphore PV 操作中的加减计数器操作都是原子的 , 可以在多线程环境下直接使用,可以将信号量理解为广义的锁,当信号量取值0~1就会退化为一个普通锁
public static void main(String[] args) throws InterruptedException {
        //构造方法传入有效资源个数
        Semaphore semaphore=new Semaphore(3);
        semaphore.acquire();//p操作 申请资源
        semaphore.release();//v操作 释放资源·
    }

CountDownLatch 

同时等待 N 个任务执行结束
 public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch=new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Thread t=new Thread(()-> {
                try {
                    Thread.sleep(3000);
                    System.out.println("到达终点");
                    latch.countDown();//相当于撞线
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
        latch.await();//等待所有线程完成撞线
        //调用CountDown的次数达到初始化的值await返回,否则阻塞等待
        System.out.println("结束");
    }

线程安全的集合类

多线程环境使用 ArrayList

1.使用同步机制 (synchronized 或者 ReentrantLock)

2.Collections.synchronizedList(new ArrayList);//对ArrayList的封装

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized

3.使用 CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器,当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复
制出一个新的容器然后新的容器里添加元素,添加完元素之后再将原容器的引用指向新的容器

多线程下使用队列

1. ArrayBlockingQueue  基于数组实现的阻塞队列

2.LinkedBlockingQueue 基于链表实现的阻塞队列

3.PriorityBlockingQueue  基于堆实现的带优先级的阻塞队列

4.TransferQueue             最多只包含一个元素的阻塞队列

多线程环境使用哈希表

HashMap 本身不是线程安全的
在多线程环境下使用哈希表可以使用 : 1.Hashtable 2.ConcurrentHashMap
Hashtable只是将关键方法加上了synchronized关键字, 相当于直接针对 Hashtable 对象本身加锁

concurrentHashMap的优点

1.concurretHashMap将锁粒度细化, 每个哈希桶添加一把锁,只有在两个线程同时访问一个哈希桶才会产生冲突
2.读不加锁,写才加锁
3.CAS特性,更高效的操作 比如size操作
4.对扩容进行优化,“化整为零”, 每次扩容一点逐渐完成整个扩容,查询时新旧一起查,新增则在新表中新增,直到完成扩容

经典面试题:HashtableHashMapConcurrentHashMap 之间的区别

HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用
CAS 机制. 优化了扩容方式. key 不允许为 null

死锁

死锁是多线程代码中常见BUG

产生死锁的必要条件

1.互斥使用:线程1拿到锁A,其他线程无法获取到A

2.不可抢占:线程1拿到锁A,其他线程只能阻塞等待,等到线程1主动释放锁而不是强行将锁抢走

3.请求保持:当线程1拿到锁A之后,就会一直持有这个获取到锁的状态,直到说主动释放

4.循环等待:线程1等待线程2,线程2又尝试等待线程1

其中循环等待就可以通过代码编写格式来打破进而避免死锁

破坏循环等待

针对多把锁进行编号,约定获取多把锁时明确获取顺序比如从小到大

//可能造成死锁
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
};
t1.start();
Thread t2 = new Thread() {
    @Override
    public void run() {
        synchronized (lock2) {
            synchronized (lock1) {
                // do something...
           }
       }
   }
};
t2.start();

//约定获取锁的顺序
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
};
t1.start();
Thread t2 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
 };
t2.start();

 好的以上就是有关多线程下锁的一些知识总结还有一些面试常见问题,希望对你有帮助 欢迎点赞,评论 蟹蟹!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值