并发

56 阅读7分钟

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会被阻塞,其它线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后说有阻塞的线程将被唤醒
底层原理:调用awaut()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次被唤醒
Semaphore表示信号量,可以设置许可的数量,表示同时允许最多多少线程可以使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()来释放许可,当某个线程释放了某个许可后,会从将AQS中排队的第一个线程依次唤醒,一直到没有空闲许可

ReenTrantLock中tryLock()和lock()区别

tryLock() 表示尝试加锁,可能加锁成功也可能加锁失败,该方法不会阻塞线程,有返回值,如果加锁成功则返回true,反之则返回false
lock() 会阻塞加锁,一直阻塞到加到锁,会阻塞线程,方法没有返回值

ReenTrantLock的公平锁和非公平锁的底层实现

首先不管是公平锁还是非公平锁它们的底层都是通过AQS进行排队,它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁会先检查AQS队列中是否存在线程在排队,如果线程在排队,则当前线程也进行排队。如果是非公平锁,则不去检查线程是否在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没有竞争到锁都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只体现在线程加锁阶段,而非体现在线程被唤醒阶段。

另外ReenTrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的

Sychronized的偏向锁、轻量级锁、重量级锁

1.偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
2.轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
3.如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4.自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

Sychronized和ReentrantLock的区别

  • Sychronized是一个关键字,ReentrantLock是一个类\
  • Sychronized会自动加锁和释放锁,ReentrantLock需要手动加锁和释放锁\
  • Sychronized底层是JVM层面的锁,ReentrantLock是API层面的锁\
  • Sychronized是非公平锁,ReentrantLock可以是公平锁也可以是非公平锁\
  • Sychronized锁的是对象,锁信息报错在对象头中,ReentrantLock是通过代码中int类型的state标识来标识锁的状态\
  • Sychronized底层有一个锁升级的过程

ThreadLocal的底层原理

1.ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2.ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
3.如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象
4.ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

ThreadLocal应用场景

1.会话管理:可以使用ThreadLocal来存储每个请求的会话信息,如用户登录状态、请求参数等。这样可以确保每个请求在处理时都能访问到自己的会话数据,而不会受到其他请求的影响。
2.数据库连接管理:在数据库操作中,可以使用ThreadLocal来创建每个线程专用的数据库连接。这样可以避免线程之间共享数据库连接,提高数据库操作的并发性能和安全性。
3.过滤器和拦截器:在过滤器或拦截器中,可以使用ThreadLocal来存储与请求相关的信息,如请求头、参数等。这些信息可以在后续的处理环节中被访问,以便进行自定义的逻辑处理。
4.日志管理:在多线程应用中,可以使用ThreadLocal来存储每个线程的日志信息,以便在日志记录中关联到对应的线程。
5.线程安全性:在多线程环境下,当多个线程需要访问同一个共享资源时,可以使用ThreadLocal来保证每个线程拥有自己的资源副本,避免竞态条件和数据不一致。

阿里一面 如何查看线程死锁

可以通过jstack命令来查看,jstack命令会显示发生死锁的线程 或者两个线程去操作数据库时,数据库发生死锁,这时可以查询数据库死锁的情况

查询是否锁表
show OPEN TABLES where In_use > 0;
查询进程
show processlist;
查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

京东一面 如果你提交任务时,线程池队列已满,这时会发生什么

1.如果使用无界队列,继续提交任务
2.如果使用的是有界队列,提交任务时,如果队列满了,如果核心线程池没有达到上限,那么则增加线程,如果线程数达到最大值,则使用拒绝策略拒绝

蚂蚁一面 线程池的底层原理

1.如果此时线程池的数量小于corePoolSize,即使线程池处于空闲状态,也要创业新的线程来处理被添加的任务
2.如果此时线程池的数量等于corePoolSize,但是缓存队列workQueue未满,则任务放到缓存队列
3.如果此时线程池的数量大于等于corePoolSize,缓存队列workQueue满,但是线程数小于maxPoolSize,则创建新的线程执行被添加的任务
4.如果此时线程池的数量大于等于corePoolSize,缓存队列workQueue满,但是线程数等于maxPoolSize,则通过handler指定拒绝策略来处理此任务
5.如果线程池数量大于corePoolSize时,当某个线程空闲时间超过keepAliveTime,线程将被终止,这样线程池可以动态调整池中线程数

线程池默认的拒绝策略

AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务

为什么使用线程池

1.降低资源消耗,提高线程利用率,降低创建和销毁线程的消耗
2.提高相应速度,任务来了,之间有线程可用可执行,而不是先创建线程再执行
3.提高线程的可管理性,线程是稀缺资源,使用线程池可以统一分配调优监控