Java并发面试题
创建一个线程的方式有哪几种
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
sleep()会让线程进入什么状态
- sleep()方法的作用是让当前线程暂停指定的时间,只是暂时让出CPU的执行权,并不会释放锁。会让线程进入阻塞状态。
线程池相关参数介绍,原理说明
- 参数介绍- int corePoolSize:线程池的核心线程数量
- int maximumPoolSize:线程池的最大线程数
- long keepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间
- TimeUnit unit:时间单位
- BlockingQueueworkQueue:任务队列,用来储存等待执行任务的队列 
- ThreadFactory threadFactory:线程工厂,用来创建线程,一般默认即可
- RejectedExecutionHandler handler:拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
 
- 原理说明- 线程池中线程数量小于corePoolSize,此时任务不会进等待队列,线程池直接创建一个线程Worker执行提交的任务
- 线程池中线程数量不小于corePoolSize并且等待队列未满,任务直接添加到等待队列,等待线程池调度执行
- 线程池中线程数量不小于corePoolSize但是等待队列已满且线程数量小于maximumPoolSize,线程池会进行扩容新创建一个线程Worker执行提交的任务,新创建的Worker会被添加到线程集合workers中
- 等待队列已满并且线程数量已达到maximumPoolSize,这种情况下线程池无法继续执行任务会拒绝任务,执行一个指定的拒接策略
- 线程池已关闭,拒绝任务,执行一个指定的拒接策略
 
syncronized的工作原理
- 介绍:synchronized关键字用于在线程并发执行时,保证同一时刻,只有一个线程可以执行某个代码块或方法,同时还保证了代码在执行完后所修改的数据对其它线程是可见的。
- 基本使用- 同步普通方法:锁是当前对象
- 同步静态方法:锁是当前类的Class对象
- 同步代码块:锁是括号里的对象
 
- 工作原理- 反编译字节码文件,发现方法的同步是通过指令monitorenter和monitorexit来完成的。
- Java中的每个对象都有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时会获取monitor的所有权将计数器加1,执行monitorexit指令时会释放monitor的所有权将计数器减1。
 
为什么说syncronized是可重入的
- 当线程执行monitorenter指令时会尝试获取monitor的所有权- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
 
reentrantlock和synchronized的区别
- synchronized是可重入的,reentrantlock也是可重入。
- synchronized是关键字,reentrantlock是Java类。
- synchronized是通过JVM的字节码实现的,每个锁对象都绑定一个monitor,进入一个synchronized同步逻辑时需要获取该monitor并在计数时加1,离开时释放并减1,reentrantlock是通过CAS操作实现的。
- synchronized的加锁和释放锁是自动的,reentrantlock需要手动加锁和释放锁。
- reentrantlock有读写锁实现,在有读和写的并发需求时可以实现更有效率的并发。
- synchronized是不可中断的,reentrantlock支持超时返回和中断。
什么是线程安全问题
- 当多个线程共享同一个全局变量时,在进行写入操作,可能会受到其它线程的干扰,就会让数据有问题,这样就叫做线程安全问题。
并发编程三要素
- 原子性:指的是一个操作不能再继续拆分,要么一次操作完成,要么就是不执行。
- 可见性:指的是一个变量在被一个线程更改后,其它的线程能立即看到最新的值。
- 有序性:指的是程序的执行按照代码的先后顺序执行。
volatile关键字的作用
- 禁止指令重排,保证了有序性。
- 保证变量的读写及时从缓存刷新到主存,保证了可见性。
synchronized关键字的作用
- 保证同一时刻,只有一个线程可以执行某个代码块或方法
- 保证了代码在执行完后所修改的数据对其它线程是可见的
- 可以保证有序性,可见性,原子性。
Unsafe类介绍
- Unsafe在sun.misc下,顾名思义,这是一个不安全的类,因为Unsafe类所操作的并不属于Java标准,Java的一系列内存操作都是交给jvm的,而Unsafe类却能有像C语言的指针一样直接操作内存的能力,同时也会带来了指针的问题。过度使用Unsafe类的话,会使出错率变得更大,因此官方才命名为Unsafe,并且不建议使用,连注释的没有。而为了安全使用Unsafe,Unsafe类只允许jdk自带的类使用。
什么是Java内存模型(共享内存模型)
- JMM决定一个线程对共享变量的写入何时对另一个线程可见。
- 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
- 线程A与线程B之间通信:线程A把本地内存A中更新过的共享变量刷新到主内存中去,线程B到主内存中去读取线程A之前已更新过的共享变量。
notify和notifyAll
notify唤醒一个线程,notifyAll唤醒所有的线程。
线程的状态
New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态)。
CAS底层实现
比较内存中当前存在的值和外部给定的期望值,只有两者相等时,才将这个内存值修改为新的给定值。CAS操作包含三个操作数,需要读写的内存位置(V)、拟比较的预期原值(A)和拟写入的新值(B),如果V的值和A的值匹配,则将V的值更新为B,否则不做任何操作。
CAS存在的问题
- ABA问题。
- 循环时间长开销大。
- 只能保证一个共享变量的原子操作。
什么是可重入锁
某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
什么是自旋锁
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
什么是公平锁
多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
什么是非公平锁
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
线程池的阻塞队列(博客园)
- ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。
- LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
- PriorityBlockingQueue:按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。
- DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
线程池的4种拒绝策略
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。
- DiscardOldestPolicy:丢弃最旧的一个请求,并尝试再次提交当前任务。
- DiscardPolicy:丢弃被拒绝的任务。
实现Runnable接口和继承Thread类的区别
- 实现Runnable接口可以再继承其它类。
- 继承Thread类不能再继承其它类。
- 实现Runnable接口的线程间可以完成资源的共享,同时处理同一资源。
- 继承Thread类的线程间都是独立运行的,资源不共享。
为什么要使用线程池
- 可以减少资源的消耗。线程的创建和销毁会造成一定的时间和空间上的消耗,而线程池可以让我们重复的利用已经创建好的线程,避免了不必要的浪费。
- 提高了系统的响应速度。线程池是利用已经创建好的线程,没有线程的创建和销毁,所以响应速度很快。
- 让线程更加便于管理。线程属于稀缺资源,我们不可以随意创建。运用线程池可以方便统一的管理。
Java中死锁的理解(死锁的简单理解及示例)
- 当两个或多个线程正在等待彼此释放所需资源(锁定)并陷入无限时间的阻塞。
- 两个或多个线程在执行过程中,相互争夺资源而造成的一种互相等待的现象。
停止一个线程用什么方法
Thread.interrupt()方法: 作用是中断线程。将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。
run方法和start方法的区别
调用start方法方可启动线程,而run方法只是thread类中的一个普通方法调用,还是在主线程里执行。
谈谈你对AQS的理解(知乎)
AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。它包含了state变量、加锁线程、等待队列等并发中的核心组件。
在Java中wait和sleep方法的不同
- 在等待时wait会释放锁,而sleep一直持有锁。
- wait通常被用于线程间交互,sleep通常被用于暂停执行。
什么是线程池
事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高了代码的执行效率。
Java中多线程间的通信怎么实现
- 共享变量。
- wait/notify机制。
Synchronized原理
- 对象锁原理。
- 代码块加锁解锁过程。
- 方法体加锁解锁过程。
对象锁原理
在Java中,每个锁对象内部都有一个monitor对象(监视器锁)。Java虚拟机中,monitor由ObjectMonitor实现,ObjectMonitor的三个成员变量:_owner(指向获得monitor的线程)、_EntryList(处于block状态的线程,会被加入到entry set)、_WaitSet(处于wait状态的线程,会被加入到wait set)。多个线程同时访问一段同步代码时,首先会进入_EntryList集合,进行阻塞等待,当线程获取到锁对象的monitor之后进入到_owner区域,并把monitor中的_owner变量指向该线程,同时monitor中的计数器count加一,若线程调用同步对象的wait()方法将释放当前持有的monitor,_owner变量重置为null,count置为0,同时该线程进入_WaitSet中等待唤醒,线程执行完同步代码后,也将_owner和count变量重置。
 
代码块加锁解锁过程
反编译字节码,底层使用monitorenter和monitorexit指令实现。执行monitorenter指令,线程尝试获取锁对象的monitor对象,若monitor的count变量为0,则将count设置为1,_owner设置为当前线程,如果线程已经获取到monitor对象,则可以重入该锁,将count计数器的值加一。执行monitorexit指令,会将计数器count的值减一,当计数器为0时,当前线程释放monitor对象,其它线程有机会获得monitor对象。
方法体加锁解锁过程
反编译字节码,底层通过ACC_SYNCHRONIZED访问标志判断一个方法是否同步方法。调用方法时,检测这个标志是否被设置,如果设置了,线程需要先获得锁对象的monitor对象,然后执行方法,最后在方法完成时释放monitor对象。
线程同步的方式
- 使用synchronized关键字同步方法。
- 使用synchronized关键字同步代码块。
- 使用特殊域变量(volatile)。
- 使用重入锁实现线程同步。
- 使用局部变量实现线程同步。
- 使用阻塞队列实现线程同步。
- 使用原子变量实现线程同步。
参考