并发面试题

内容分类详情
Java高频面试题汇总入口
JVMJVM面试题
并发并发面试题
SpringSpring面试题
分布式分布式面试题
SpringBootSpringBoot面试题
SpringCloudSpringCloud面试题
DubboDubbo面试题
MySQLMySQL面试题
MybatisMybatis面试题
RedisRedis面试题
RocketMQRocketMQ面试题
算法算法面试题
遇到的问题遇到的问题
面试官的其他问题面试官的其他问题
GitGit面试题

线程的生命周期

在这里插入图片描述

Thread常用方法

start()

启动线程,线程状态由创建变为就绪。

String getName()

返回线程的名称

run()

线程在被调度时执行的操作

Thread currentThread()

返回当前线程 。在 Thread 子类中就是 this ,通常用于主线程和 Runnable 实现类

interrupt()

中断线程,由运行状态到死亡状态。interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。

如果线程处于阻塞状态,调用interrupt方式,就会报错:

java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.thread.InterruptTest$InterruptThread.run(InterruptTest.java:17)
    at java.lang.Thread.run(Thread.java:748)

isAlive()

测试线程是否处于活动状态,线程调用 start 后,即处于活动状态

join(long millis)

主线程等待调用join方法的线程结束后,再继续执行主线程。

sleep(long millis)

睡眠指定时间,程序暂停运行,睡眠期间会让出 CPU 的执行权

yield()

暂停当前正在执行的线程对象,并执行其他线程。

Object 类中的常用 API

wait()

一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。而当前线程排队等候其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行.

notify()

一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。

notifyAll()

一旦执行此方法,就会唤醒所有被 wait 的线程。

锁的类型

从线程加锁时机分为:

  • 乐观锁
    乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。
  • 悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞。

从锁的公平性进行区分:

  • 公平锁:指多个线程按照申请锁的顺序来获取锁。指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

从线程根据锁是否重复获取:

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

从多个线程能否获取同一把锁分为:

  • 共享锁
  • 独享锁

悲观锁

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。

举例:
Java 中的 Synchronized 和 ReentrantLock 排他锁也是一种悲观锁思想的实现。

mysql的select for update语句就是悲观锁,该语句是基于索引的,悲观锁的有效范围是begin和commit之间,session1执行了select for update后没有提交,session2再执行select for update时就会阻塞,mysql会返回锁表超时的提示。

乐观锁

它总认为资源和数据不会被别人所修改,所以读取不会上锁,乐观锁的实现方案一般来说有两种:版本号机制 和 CAS实现 。乐观锁多适用于多读的应用类型,这样可以提高吞吐量。

乐观锁实现机制:版本号机制和CAS机制

版本号机制:
在这里插入图片描述

线程A在修改时判断,当前版本号是否一致,如果一致说明从内存中获取到的数据是最新的,则线程A可以执行修改操作。

mysql中使用乐观锁,往往会在业务表中添加version字段,执行更新语句时加上version字段。

update test set num = 12,version = version + 1 where id = 1 and version = 1;

CAS机制:
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

独享锁/共享锁

ReentrantLock,Synchronized,ReadWriteLock(其读锁是共享锁,其写锁是独享锁)

可重入锁

ReetrantLock,synchronized

synchronized void setA() throws Exception{
  Thread.sleep(1000);
  setB();
}

synchronized void setB() throws Exception{
  Thread.sleep(1000);
}

如果synchronized不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

公平锁/非公平锁

ReetrantLock默认是公平锁。
synchronized是非公平锁,由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

分段锁

JDK1.7的ConcurrentHashMap:数组+Segment+分段锁

Segment(分段锁):ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

在这里插入图片描述

JDK1.8的ConcurrentHashMap:synchronized 和 CAS 来操作

CAS的原理

由于CAS是CPU指令,我们只能通过JNI与操作系统交互,关于CAS的方法都在sun.misc包下Unsafe的类里,java.util.concurrent.atomic包下的原子类等通过CAS来实现原子操作。

全称是Compare And Swap,即比较再交换,是实现并发应用到的一种技术
底层通过Unsafe类实现原子性操作操作包含三个操作数 —— 内存地址(V)、预期原值(A)和新值(B)
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 ,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
CAS这个是属于乐观锁,性能较悲观锁有很大的提高
AtomicXXX 等原子类底层就是CAS实现,一定程度比synchonized好,因为后者是悲观锁
底层调用C++写的代码,直接请求CPU调用

在这里插入图片描述

synchronized实现原理

JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。
偏向锁->轻量级锁->重量级锁

javap命令对class文件进行反汇编,查看字节码指令如下:

在这里插入图片描述

synchronized锁升级

偏向锁

  • 适用情况:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
    偏向锁的偏是指会偏向第一个获得锁的线程。

  • 原理:当一个线程访问同步代码块并获取锁时,会通过CAS操作在Mark Word里存储锁偏向的线程ID。

  • 优点:在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。
    引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的锁执行操作。

轻量级锁

  • 适用情况:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,适用在多线程交替执行同步块的情况

  • 原理:当前线程的栈帧中的创建LockRecod,将锁对象的MarkWord复制到LockRecod中,CAS操作尝试将对象的MarkWord更新为指向LockRecord的指针,如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。

  • 优点:在多线程交替执行同步块的情况下,用CAS进行加锁和解锁而不是直接用重量级锁,避免性能消耗

重量级锁

monitor锁

锁升级自己的一些理解:

偏向锁是一段同步代码一直被一个线程所访问,只需要第一次用CAS存储在Mark Word里存储锁偏向的线程ID,后续就直接判断这个线程ID在不在,不需要再使用CAS了。
轻量级锁就是多线程交替执行同步块的情况下,每次都是用CAS操作尝试将对象的MarkWord更新为指向LockRecord的指针,而不是使用重量级锁阻塞其他线程。

AQS 原理(AbstractQueuedSynchronizer)

AQS的全称为(AbstractQueuedSynchronizer)抽象队列同步器,这个抽象类在java.util.concurrent.locks包下面。它是一个Java提高的底层同步工具类,比如CountDownLatchReentrantLockSemaphoreReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS
只要搞懂了AQS,那么J.U.C中绝大部分的api都能轻松掌握
简单来说包含:

  • 一个int类型的变量state(用于计数器,类似gc的回收计数器)表示同步状态,并提供了一系列的CAS操作来管理这个同步状态对象;

AQS维护了一个变量state,使用volatile修饰保证其可见性,目的是为了让多线程可以知道此资源当前的访问状态。

/**
 * The synchronization state.
 */
private volatile int state;

AQS维护了两个方法getState和setState,用来维护此状态,"1"表示已占用,"0"表示空闲。

  • 一个是线程标记(当前线程是谁加锁的);
  • 一个是阻塞队列(用于存放其他未拿到锁的线程);

例子:线程A调用了lock()方法,通过CASstate赋值为1,然后将该锁标记为线程A加锁。如果线程A还未释放锁时,线程B来请求,会查询锁标记的状态,因为当前的锁标记为 线程A,线程B未能匹配上,所以线程B会加入阻塞队列,直到线程A触发了 unlock() 方法,这时线程B才有机会去拿到锁,但是不一定肯定拿到

  • acquire(int arg)

    源码讲解,好比加锁lock操作

  • tryAcquire()

    尝试直接去获取资源,如果成功则直接返回,AQS里面未实现但没有定义成abstract,因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared,类似设计模式里面的适配器模式

  • addWaiter()

    根据不同模式将线程加入等待队列的尾部,有Node.EXCLUSIVE互斥模式、Node.SHARED共享模式;如果队列不为空,则以通过compareAndSetTail方法以CAS将当前线程节点加入到等待队列的末尾。否则通过enq(node)方法初始化一个等待队列

/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}
  • acquireQueued()

    使线程在等待队列中获取资源,一直获取到资源后才返回,如果在等待过程中被中断,则返回true,否则返回false

  • release(int arg)

    源码讲解 好比解锁unlock
    独占模式下线程释放指定量的资源,里面是根据 tryRelease()的返回值来判断该线程是否已经完成释放掉资源了;在自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false

  • unparkSuccessor(Node node)

    方法用于唤醒等待队列中下一个线程

在这里插入图片描述

在这里插入图片描述

如果需要线程安全,且效率高的Map,应该怎么做

多线程环境下可以用concurrent包下的ConcurrentHashMap, 或者使用Collections.synchronizedMap(),
ConcurrentHashMap虽然是线程安全,但是他的效率比Hashtable要高很多

synchronized和ReentrantLock的区别

ReentrantLocksynchronized使用的场景是什么,实现机制有什么不同?

ReentrantLocksynchronized都是独占锁

  • synchronized

1、是悲观锁会引起其他线程阻塞,java内置关键字,

2、无法判断是否获取锁的状态,锁可重入、不可中断、只能是非公平

3、加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单但显得不够灵活

4、一般并发场景使用足够、可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁

5、synchronized操作的应该是对象头中mark word,参考原先原理图片

  • ReentrantLock

1、是个Lock接口的实现类,是悲观锁,

2、可以判断是否获取到锁,可重入、可判断、可公平可不公平

3、需要手动加锁和解锁,且 解锁的操作尽量要放在finally代码块中,保证线程正确释放锁

4、在复杂的并发场景中使用在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

5、创建的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

6、锁机制是AQSstateFIFO队列来控制加锁

Redisson分布式锁实现原理

在这里插入图片描述

线程池的核心属性

  • threadFactory(线程工厂):用于创建工作线程的工厂。

  • corePoolSize(核心线程数):当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。

  • workQueue(队列):用于保留任务并移交给工作线程的阻塞队列。

  • maximumPoolSize(最大线程数):线程池允许开启的最大线程数。

  • handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。

  • keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止。

在这里插入图片描述

线程池的状态流转

RUNNING:接受新任务并处理排队的任务。

SHUTDOWN:不接受新任务,但处理排队的任务。

STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务。

TIDYING:所有任务都已终止,workerCount 为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。

TERMINATED:terminated() 已完成。

在这里插入图片描述

JDK中的线程池有哪些

线程池实现类使用的阻塞任务队列线程池特点使用注意事项
Executors.newFixedThreadPool(5);LinkedBlockingQueue固定线程数量,任务队列容量为int最大值任务队列可能导致OOM
Executors.newCachedThreadPool();SynchronousQueue核心线程数量为int最大值,任务队列仅作转发线程过多可能导致OOM
Executors.newSingleThreadExecutor();LinkedBlockingQueue核心线程数为一,线程异常将会创建新的线程执行任务,可以保证任务执行的顺序任务队列可能导致OOM
Executors.newScheduledThreadPool(5);DelayedWorkQueue指定频率执行任务,多个核心线程执行任务队列可能导致OOM
Executors.newSingleThreadScheduledExecutor();DelayedWorkQueue指定频率执行任务,单个核心线程执行任务队列可能导致OOM

为什么不用线程池的工具类创建线程

FixedThreadPool和SingleThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM异常
CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起OOM异常

线程池有哪些队列

  • ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序。

  • LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool 使用了该队列。

  • SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入 SynchronousQueue 中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。Executors.newCachedThreadPool使用了该队列。

  • PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

gzh-程序员灿灿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值