最全讲解大厂高并发面试真题一(题源收集于牛客)

前言(闲谈)

混迹csdn也有两年多了,虽然是一个大三的老学长,但是在发布文章方面却一直是一个小白,因为之前的知识层面并没有达到一定的高度,写出来的东西也怕会误人子弟,尽管有一些的项目的经验,但是觉得零散的知识片段也会给大家徒添疑惑。所以迟迟不知道该写些什么。
最近觉得自己的积累也算是有了点深度,也就想着开始把之前记录过的知识点都写成博客,给大家帮助的同时自己也再次清理一下思绪。
发觉作为应届生或者是说暑假实习生而言(俺也想去大厂当实习生!!!),有时候java基础方面的问题反而很少,因为招人进去以后很多时候也不一定就会做java,更多考察你的是你的网络基础方面知识,多线程/高并发基础,数据库,JVM等基础知识。所以这篇文章也就是围绕着多线程/高并发的基础知识点进行讲解,希望能给大家带来帮助呀。

正文

进程线程区别,线程安全和非线程安全区别

既然谈到什么区别就来谈谈 各是什么:
进程是系统资源分配和调度的基本单位 。
线程时任务调度与执行的基本单位。

区别: 同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器,堆栈,上下文) 一个进程至少包括一个线程。
线程是轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销比较小。

包含关系:只有一个线程的进程可以看做是单线程的,如果一个进程内有多个线程,在执行的过程是多条(线程)共同完成的; 线程是进程的一部分所以也被称之为轻量级进程。

线程有自己的私有属性TCB,线程id,寄存器,硬件上下文,而进程也有自己的私有属性进程控制块PCB,但是这些私有的属性是不被贡献的,用来标示一个进程或者一个线程。

非线程安全:

多个线程同时对对象中的同一个实例变量进行操作时会出现值被更改,值不同步的情况,进而影响程序的执行流程。

线程安全:

是多线程在访问时候,采用加锁机制,当一个线程访问某个类的数据的时候对其进行保护,其他线程不能访问该线程直到该线程读取完成以后 其他的线程才能对其进行访问 不会出现数据的不一致性或则数据污染。个人认为比较简练的回答是:如果你的代码在多线程执行和单线程下执行永远都能够获得一样的结果,那么你的代码就是线程安全的。有几个需要注意的地方,就是线程安全的几个级别:
1)不可变
像String、Integer、Long这些,都是final类型的类,任何⼀个线程都改变不了它们的值,要改变除⾮新创建⼀个,因此这些不 可变对象不需要任何同步⼿段就可以直接在多线程环境下使⽤ 2)绝对线程安全
不管运⾏时环境如何,调⽤者都不需要额外的同步措施。要做到这⼀点通常需要付出许多额外的代价,Java中标注⾃⼰是线程安 全的类,实际上绝⼤多数都不是线程安全的,不过绝对线程安全的类,Java中也有,⽐⽅说CopyOnWriteArrayList、 CopyOnWriteArraySet
3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove⽅法都是原⼦操作,不会被打断,但也仅 限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现 ConcurrentModificationException,也就是fail-fast机制。
4)线程非安全(有些面试题目还会问你你知道有哪些线程安全类)
ArrayList、LinkedList、HashMap等都是线程⾮安全的类。

区别在于

非线程安全是指多线程操作同一个对象可能会出现问题,而线程安全则是多线程在操作同一个对象的时候不会出现问题。对于线程的安全是通过线程同步控制来实现的 也就是 synchronized 非线程安全是 通过异步实现

守护线程 本地线程

在java中线程分为两种: 守护线程和用户线程
任 何 线 程 都 可 以 设 置 为 守 护 线 程 和 用 户 线 程 , 通 过 方 法 Thread.setDaemon(boolean);true
则把该线程设置为守护线程,反之则为用户线程。
Thread.setDaemon()必须在 Thread.start()之前调用 , 否则运行时会抛出异常 。
两 者 的 区 别
唯 一的 区 别是 判 断虚 拟 机(JVM)何 时离 开 ,Daemon 是 为 其 他 线 程 提 供 服 务 , 如 果全 部 的 User Thread 已 经 撤 离 , Daemon 没 有 可 服 务 的 线 程 , JVM 撤 离 。 也 可以 理 解 为 守 护 线 程 是 JVM 自 动 创 建 的 线 程 ( 但 不 一 定 ) , 用 户 线 程 是 程 序 创 建 的线 程 ; 比 如 JVM 的 垃 圾 回 收 线 程 是 一 个 守 护 线 程 , 当 所 有 线 程 已 经 撤 离 , 不 再 产生 垃 圾 , 守 护 线 程 自 然 就 没 事 可 干 了 , 当 垃 圾 回 收 线 程 是 Java 虚 拟 机 上 仅 剩 的 线程 时, Java 虚 拟 机 会 自 动 离 开 。
扩 展 :Thread Dump 打 印 出 来 的 线 程 信 息 , 含 有 daemon 字 样 的 线 程 即 为 守 护进 程 , 可 能 会 有 : 服 务 守 护 进 程 、 编 译 守 护 进 程 、 windows 下 的 监 听 Ctrl+break的 守护 进 程、 Finalizer 守 护 进 程 、 引 用 处 理 守 护 进 程 、 GC 守 护 进 程 。

start,run,wait,notify,yiled,sleep,join等方法的作用以及区别

具体参看我的另外一篇博客,那篇博客就是以这个题目为出发点进行具体的讲解呀
跳转链接

什么是线程的上下文切换

上下文

首先 明白什么是上下文;
对于每个任务运行前,CPU都需要知道任务是从哪里加载的,又是从哪里开始运行的,就涉及到CPU寄存器和程序计数器

cpu的寄存器是cpu中内置容量小,但是速度较快的内存。
程序计数器是会存储cpu正在执行令的位置 或是即将执行的指令的位置。

上下文切换

  1. 将当前cpu的上下文 (就是说 cpu寄存器和程序计数器里面的内容)保存起来。
  2. 然后加载新任务的上下文 cpu寄存器和程序计数器。
  3. 最后跳到程序计数所指向的位置 运行新任务。
  4. 被保存起来的上下文会存储到系统的内核中 等待任务重新调度指向时候 再次加载进来。、

上下文切换分为 线程 进程 和中断上下文。

线程上下文切换

线程是调度的基本单位,而进程则是资源进行分配和拥有的基本单位。
内核中的任务的调度其实是在调度线程,进程只是给线程提供虚拟的内存全局变量等资源。线程在进行上下文的切换的时候 共享相同的虚拟内存和全局变量等资源不需要进行修改 但是对于 线程自己私有的数据 如 栈和寄存器要进行修改,
线程上下文切换的时候 分为两种的情况,就是 对于 两个线程属于不同的进程 两个线程属于相同的进程。

进程的上下文切换

进程是有内核管理和调度的 所以说 对于 进程的上下文切换 只会发生在 内核态 因此来说 进程的上下文切换 不但会包括 虚拟内存 栈 全局变量等 用户资源 还包括扩 内核堆栈 寄存器等 内核空间的状态。
所以来说 对于 进程的上下文切换 会比系统调用多一个步骤:
保存当前进程的内核状态和CPU寄存器之前 先把该进程的虚拟内存 栈保存起来 加载下一个进程的内核以后 还要刷新 进程的虚拟内核和用户栈。
保存上下文和恢复上下文需要内核在PUC上运行才能够完成

中断上下文切换

为了快速响应硬件的事件 中断处理会打断进程的正常调度和执行然后调用中断来处理程序 响应请求时间 在打断其他进程的运行的时候 也需要将之前进程的运行的情况保存下来 然后 等到中断结束以后 进程仍然可以恢复到原来的状态

对同一个cpu来说 中断处理比进程拥有更高的优先级 所以中断上下文切换不会与进程上下文切换同时发生

死锁与活锁的区别,死锁与饥饿的区别?

死 锁 : 是指两个或两个以上的进程( 或 线 程 )在执行过程中 , 因争夺资源而造成的一种 互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

1、互 斥 条 件 : 所 谓 互 斥 就 是 进 程 在 某 一 时 间 内 独 占 资 源 。
2、 请 求 与 保 持 条 件 : 一 个 进 程 因 请 求 资 源 而 阻 塞 时 , 对 已 获 得 的 资 源 保 持 不 放 。
3、 不 剥 夺 条 件 :进 程 已 获 得 资 源 , 在 末 使 用 完 之 前 , 不 能 强 行 剥 夺 。
4、 循 环 等 待 条 件 :若 干 进 程 之 间 形 成 一 种 头 尾 相 接 的 循 环 等 待 资 源 关 系。

如何破坏死锁

破坏死锁就是破话产生的条件:

  1. 破坏循环等待: 使用资源的有序分配 对所有的设备实现分类编号 所有进程只能采用按序号递增的形式进行资源的申请
  2. 破坏请求和保持: 采用资源预先分配的原子 就是说1进程运行前会将其所需要的资源一次性都分配给 就可以避免
  3. 破坏不可抢占: 当一个线程已经占用了独立性的资源以后 再去申请一独立资源无法满足就会退出原来占用的资源。
    对于 破坏互斥条件是不可能满足的

活 锁 :任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试 ,失 败 , 尝 试 , 失 败 。
活 锁和 死 锁 的 区 别在 于 ,处 于 活锁 的 实 体 是 在不 断 的改 变 状态 。 而处 于 死 锁 的 实 体 表 现 为 等 待 ; 活 锁 有 可 能 自 行 解 开 , 死 锁 则 不 能 。
饥 饿 : 一 个 或 者 多 个 线 程 因 为 种 种 原 因 无 法 获 得 所 需 要 的 资 源 , 导 致 一 直 无 法 执行 的 状 态 。
Java 中 导 致 饥 饿 的 原 因 :
1、 高 优 先 级 线 程 吞 噬 所 有 的 低 优 先 级 线 程 的 CPU 时 间 。
2、 线 程 被 永 久 堵 塞 在 一 个 等 待 进 入 同 步 块 的 状 态 , 因 为 其 他 线 程 总 是 能 在 它 之 前、持 续 地 对 该 同 步 块 进 行 访 问 。
3、 线 程 在 等 待 一 个 本 身 也 处 于 永 久 等 待 完 成 的 对 象 (比 如 调 用 这 个 对 象 的 wait 方法 ), 因 为 其 他 线 程 总 是 被 持 续 地 获 得 唤 醒 。

线程池构造函数7大参数,线程处理任务过程,线程拒绝策略

具体参看我写的这篇博客,对其有较为详细的概述
线程池

什么是阻塞队列,能说一下阻塞队列都有哪些类型吗

什么是阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法

  1. 支持组塞的插入方法: 意思是 当队列满的时候,队列会组塞插入元素的线程,直到队列不满。
  2. 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
    组塞队列常用于生产者和消费者的场景,生产者是向队列里面添加元素的线程,消费者是从队列中取元素的线程

不可用时候的处理

❑ 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException (“Queuefull”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
❑ 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
❑ 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
❑ 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

注意: 如果是无界组塞队列,队列不可能会出现满的情况,所以使用 put和offer方法永远不会被阻塞,而且使用 offer方法的时候,该方法永远返回的都是true

阻塞队列的类型

提供了七个组塞队列:
❑ ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序,默认情况下不保证线程公平访问队列

❑ LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

❑ PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

❑ DelayQueue:一个使用优先级队列实现的无界阻塞队列。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素

DelayQueue可以将DelayQueue运用在以下应用场景。
❑ 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
❑ 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。

❑ SynchronousQueue:一个不存储元素的阻塞队列。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。它支持公平访问队列。默认情况下线程采用非公平性策略访问队列

❑ LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

transfer方法

如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。transfer方法的关键代码如下。

Node pred =tryAppend(s,haveData);
return awaitMatch(s,pred,e,(how==TIMED),nanos);

第一行代码是试图把存放当前元素的s节点作为tail节点。第二行代码是让CPU自旋等待消费者消费元素。因为自旋会消耗CPU,所以自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。

tryTransfer方法

tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。

知道什么是JMM吗,说一说具体的工作流程

JVM是java虚拟机,JMM是java内存模型,具体的文章参看我之前的博文,有较为详细的整理哈。
Java内存模型

实现一个阻塞队列(字节面试原题)


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class block {
    private List<Integer> container =new ArrayList<>();
    private  volatile  int size;
    private  volatile  int capacity;
    private Lock lock=new ReentrantLock();

    private final Condition isnull =lock.newCondition();
    private final Condition isfull =lock.newCondition();
    zuse(int cap){
        this.capacity=cap;
    }
    public void add(int data){
        try{
            lock.lock();
            try{
             while(size>=capacity){
                 System.out.println("阻塞队列满了");
                 isfull.await();// 此时若是队列满的时候 添加的add 就会被阻塞起来 为满就等待
             }
            }
            catch (InterruptedException e){
                isfull.signal();
                //表示出现了异常 就会将其唤醒
                e.printStackTrace();
            }
            ++size;
            container.add(data);
            // 表示的是 对于 增加数据到里面时候 通知isnull 唤醒其其中的对象 可以进行数据的取用。
            isnull.signal();

        }finally {
            lock.unlock();
        }
    }
    public  int take(){
        try{
            lock.lock();
            try{
                while(size==0){
                    System.out.println("阻塞队列处于空的状态");
                    isnull.await();
                }
            }
            catch (InterruptedException e){
                isnull.signal();
                e.printStackTrace();
            }
            --size;
            int res=container.get(0);
            container.remove(0);
            isfull.signal();// 表示又将数据取了出去 对于 加入又可以进行工作 然后 唤醒
            return  res;
        }
        finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        zuse queue=new zuse(5);
        Thread t1=new Thread(()->{
           for(int i=0;i<100;i++){
               queue.add(i);
               System.out.println("加入"+i);
               try{
                   Thread.sleep(500);
               }catch (InterruptedException e){
                   e.printStackTrace();
               }
           }
        });
        Thread t2=new Thread(()->{
            for(;;){
                System.out.println("消费"+ queue.take());
                try{
                    Thread.sleep(500);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
t1.start();
t2.start();
    }
}

说一说Runnable接口和Callable接口的区别

Runnable接⼝中的run()⽅法的返回值是void,它做的事情只是纯粹地去执⾏run()⽅法中的代码⽽已;
Callable接⼝中的call() ⽅法是有返回值的,是⼀个泛型,和Future、FutureTask配合可以⽤来获取异步执⾏的结果。
注意: 这个特性其实是一个非常有用的特性,因为多线程比单线程更难,更复杂的一个原因就是多线程充满了未知性,某条线程是否 执⾏了?某条线程执⾏了多久?某条线程执⾏的时候我们期望的数据是否已经赋值完毕?⽆法得知,我们能做的只是等待这条多 线程的任务执⾏完毕⽽已。⽽Callable+Future/FutureTask却可以获取多线程运⾏的结果,可以在等待时间太⻓没获取到需要的 数据的情况下取消该线程的任务。

知道什么是CAS吗,说一说你对其的了解

(对于这种开放性的题目,一定要尽可能的表现自己,说的多一点,表示自己真正有去深入了解过)
首先什么是:(Compare and Swap)将指定内存地址的值与所给的某个值进行相比较,如果相等 就对值进行交换,如果不等就失败。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败, 线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试 。并且都是原子操作,虽然是进行读取 比较 然后写入.但是 是通过硬件命令保证原子性。
然后具体的实现:其中有三个操作数 需要读写的内存位置 ( V) 、进行比较的预期原值 (A)和拟写入的新值(B)。如果内存位置(V)的值与预期原值(A)相匹配,那么处理器会自动将该位置值更新为新 。否则处理器不做任何操作

CAS的缺点

1、 ABA 问 题 :
比 如 说 一 个 线 程 one 从 内 存 位 置 V 中 取 出 A,这时候另一个线程 two 也 从 内 存 中
取 出 A,并且 two 进 行 了 一 些 操 作 变 成 了 B,然后 two 又 将 V 位 置 的 数 据 变 成 A,
这 时 候 线 程 one 进 行 CAS 操 作 发 现 内 存 中 仍 然 是 A,然后 one 操作成功。尽管线
程 one 的 CAS 操作成功,但可能存在潜藏的问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
2、 循 环 时 间 长 开 销 大
对 于 资 源 竞 争 严 重 ( 线 程 冲 突 严 重 ) 的 情 况 , CAS 自 旋 的 概 率 会 比 较 大 , 从 而 浪 费 更 多 的 CPU 资 源 , 效 率 低 于 synchronized。
3
只 能 保 证 一 个 共 享 变 量 的 原 子 操 作 :

当 对 一 个 共 享 变 量 执 行 操 作 时 , 我 们 可 以 使 用 循 环 CAS 的 方 式 来 保 证 原 子 操 作 ,但 是 对 多 个 共 享 变 量 操 作 时 , 循 环 CAS 就 无 法 保 证 操 作 的 原 子 性 , 这 个 时 候 就 可 以 用 锁 来保证原子性。

说一说你对synchronized的理解

作用:

  1. 确保线程互斥访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决指令重排序问题
    对于 synchronized来说 具体的表现的形式有以下的几种:
    对于普通的同步方法而言: 锁是当前实例对象。
    对于 静态同步方法,锁是当前类的class 对象
    synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块

对于我们来说 synchronized是在java实现并发条件下数据同步访问的一个很重要的关键字,我们想要实现共享的资源在同一个时间只能够被同一个线程访问到,就可以使用到 synchronized关键字对类或者对象加锁。下面来介绍一下实现的原理是什么。
我们都知道的是 synchronized有两种的使用方法 同步方法或者是同步代码块。
此时我们对其进行一个反编译,看看具体的实现原理
对于同步方法来说 synchronized使用到 ACC_SYNCHRONIZED标记符来实现同步,对于同步代码块使用到 monitorentermonitorexit两个指令来实现同步。

同步方法而言:

方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块

同步代码块使用monitorentermonitorexit两个指令实现

大致内容如下: 可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

synchronized 与原子性

原子性是说要么全部执行,要么都不执行。
对于java来说为了保证原子性,提供了两个高级的字节码指令 monitorenter与 monitorexit,对应的也就是 synchronized。通过其修饰的代码在一段时间里面只能由一个线程访问到,无法被其他的线程所访问。因此在java中使用synchronized可以保证操作的原子性。

可见性

指的是对于多个线程对同一个共享变量的读取与写入操作时候,一个变量修改了这个值,对于其他的线程都是可见的。

对于学习过Java内存模型的也都知道

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
前面我们介绍过,被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。
而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。
所以,synchronized关键字锁住的对象,其值是具有可见性的

有序性

有序性就是说 程序的执行顺序按照的是代码的先后顺序执行的。

之前我们有了解过的是 有时候由于处理器优化和指令重排等,CPU还可能对输入的代码进行乱序执行,A * B+C最后可能会变成A+C * B,这就是执行的顺序出现问题导致的,但是对于 synchronized来说并不能够禁止指令的重排序和处理器优化的(关于这一点上 volitate能够禁止指令的重排序),就是说 既然synchronized无法阻止上述的事情发生,那么又是如何提供有序性的呢?

这里我们想到了之前在Java内存模型中的一句话 “
如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的
这里就提及到了 as-if-serial 语义。

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。

这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。
所以呢,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

公平锁和非公平锁的区别

哎呀当时整理时候不知道哪里保存下来的图片了,作者看到了可以联系,我注明出处啊。

在这里插入图片描述
上可以看出公平锁与非公平锁的区别所在。

为什么效率有差异性

对于公平锁来说,后来的线程要加上锁,即使锁处于空闲的状态, 也要检测是否还有其他的线程在等待中,如果有其他的线程还在等待,就挂起自己,然后加到队列的后面,然后唤醒的也是位于队列最前面的锁。在这样的情况下,例如一个新来的线程,在还有线程在等待时候,遇到即使锁处于空闲的状态,但是自己却不能够进行执行,先要挂起然后唤醒,但是对于一个非公平锁来说,少了这么一次的挂起与唤醒就会直接开始执行。

线程的开销而言

其实就是非公平锁效率高于公平锁的原因是因为对于非公平锁来说减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销,可以直接进行执行。

synchronized与lock锁的区别

对于 synchronized 来说会在加锁时候让其他的线程等待 只有当前线程执行完毕以后 线程才会释放会与对象锁的占用 但是若是出现异常的情况时候 JVM会让线程将锁释放 由于对于 synchronized来说 若是出现了阻塞的时候 剩下的线程就会一直处于等待的状态 但是对于lock、来说 其是一个类 里面有一些方法 能够控制阻塞线程等待的时间 或是响应中断来提高效率:

类别 synchronized lock
存在的层次来说 java的关键字 存在于jvm层面上面 是一个类
锁的释放 对于 synchronized来说 其是可以自行进行释放的 而且在线程发生异常的时候 也是会出现锁的释放 对于 lock不会对锁进行主动的释放 需要我们 在 try catch 语句中进行捕捉 在 finally里面 进行释放
锁的获取 若是a占用了所锁 并出现了阻塞的情况的时候 线程b就会一直处于等待的状态 对于 lick来说 有多重获取锁的方法 并不用一直处于等待的状态
锁状态的判断 无法判断锁的状态 可以对锁的状态进行判断
性能 少量同步 大量同步

lock中的重要的方法

trylock 是其中使用最多的方法 就是尝试去获取到锁 。如果锁出现了被其他的线程所占用 就进行等待
trylock(long time,TimeUnit unit) 在拿不到锁时候 会等待一段时间 在等待时间里面拿不到锁 就会返回false
lockInterruptibly : 若通过这个方法区获取锁的时候 发现线程在等待获取锁时候 就会响应中断 即中断线程等待的状态。
两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
注意我们此时若是一个线程已经获取到了锁 是不会被interrupt给打断的。打断的是组塞等待中的线程。

后记

这一篇都是一些比较重点和大的问题,剩下的部分会在下一篇文章中介绍到。

发布了44 篇原创文章 · 获赞 24 · 访问量 2286

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/105359717