1.在Java中守护线程和本地线程的区别?
- Java中的线程分为两种:守护线程(Daemon)和用户线程(User)
- 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(boolean);true表示把该线程设置为守护线程,反之则为用户线程。
- 注意:在调用Thread.setDaemon(true)方法必须在Thread.start()之前调用,否则会抛出异常。
- 二者区别:
- 唯一的区别就是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果User Thread全部已经撤离,Daemon没有可服务的线程,JVM撤离。
2.线程与进程的区别?
- 进程:本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位。
- 线程:是操作系统能够进行运算调度的最小单位。它是包含在进程当中的,是进程中的实际运作单位。一个进程中可以并发多个线程。
- 进程有自己独立的地址空间,线程是共享程序中的数据,使用的是相同的地址空间。
3.synchronized和Lock的区别?
- synchronzied是Java的内置的关键字,在JVM层面的,Lock是个Java类。
- synchronzied无法判断是否获取锁的状态,Lock是可以判断当前线程是否获取到锁。
- synchronized会自动释放锁,Lock需要调用unlock()方法释放锁资源。
- synchronized的锁可重入、不可中断、非公平的锁,Lock的锁是可重入、可中断、可公平的锁。
- Lock锁适合大量的同步代码的问题,synchronzied锁适合少量代码同步的问题。
- 示例:
- 假如两个线程Thread1和Thread2获取锁资源,
- 如果用synchronized,假如Thread1获取到锁资源,Thread2等待。如果Thread1阻塞,那么Thread2将会一直等待,很可能在成死锁。
- 如果用Lock,假如Thread1获取到锁资源,Thread2等待。如果Thread1阻塞,那么Thread2如果重试获取不到锁,那就直接放弃获取锁资源。
4.创建线程的方式有哪些?
-
继承Thread类,重写run方法,无返回值。
-
实现Runnable接口,重写run方法,无返回值。
-
实现Callable接口重写call方法,有Object返回值。
-
线程池创建:
-
newCachedThreadPool:创建一个带有缓存的线程池
-
newFixedThreadPool:创建一个定长的线程池,可以控制线程的最大并发数的,超出的线程会在队列中等待。
-
newScheduledThreadPool:创建一个定长的线程池,支持周期性任务执行的
-
newSingleThreadExecutor:创建一个单一的线程池,保证任务的有序性。
-
5.如何挂起和恢复一个线程?
- Thread.suspend()方法挂起一个线程,该方法不会释放锁资源,如果使用该方法将某个线程挂起,可能会引起其他资源的线程锁死锁。
- Thread.resume()本身方法没问题,但是不能独立于suspend()方法使用。
- JDK新的挂起和唤醒方法:
- wait()方法暂停执行,放弃已经获得的锁资源,进入等待状态。
- notify()随机唤醒一个在等待的线程。
- notifyAll()唤醒全部在等待的线程。
6.如何停止一个线程的运行?
-
stop():这是一个废弃的方法,开发中尽量不要使用,一旦调用,线程就会立即停止,可能会发生线程安全性问题。
-
interrupt():通过改变线程终止的标识,默认是false,线程执行,调用interrupt()方法标识改成true,线程停止执行。
7.如何自定义实现一个可重入锁?
- 可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他线程是不可以的。
- ReetrantLock就是可重入锁,可重入锁的意义就是防止死锁,实现的原理就是通过为每个锁关联一个请求计数器和一个占有它的线程。
- 当计数器为0时,认为锁是未被占有的,线程请求一个未占用的锁时,JVM将记录锁占用者,并且将请求计数器置成1,如果同一个线程再次请求到这个锁,计数器将递增。
- 每次占用的线程退出同步代码块,计数器将递减,直到计数器为0,锁被释放。
8.什么是内置锁、互斥锁?
- 内置锁其实就是一个互斥锁,每一个Java对象都可以作为一个实现的同步锁,这些锁为内置锁,获取内置锁的唯一途径就是进入这个锁的保护同步代码块,互斥锁就是最多只能一个线程抢占锁资源,当A尝试去获取B持有的锁资源时,A必须等待B释放锁资源后才可以去抢占锁资源,如果B不释放锁资源,那么A一致等待。
9.什么是原子类?
- 原子类其实就是在高并发下更新属性保证数据的正确性,确保在并发下的线程安全性。原子类相比synchronzied关键字和lock,性能更高,占用的资源少。
- atomic包里一共提供了13个类,有原子更新基本类型、原子更新数组、原子更新引用、原子更新属性。
- 基本数据类型有AtomicBoolean原子更新布尔类型、AtomicInteger原子更新类型、AtomicLong原子更新长整型,DoubleAddr对double的原子更新的性能优化,LongAddr对long原子更新的性能优化等等。
- 原子更新数组有AtomicIntegerArray对Integer数组类型的操作,AtmoicLongArray对Long数组类型的操作,AtomicReferenceFieldUpdater原子更新对象类型等等。
- 使用原子更新类时要注意修改字段必须要是volatile修饰保证线程可见性,不能是static变量,不能final修饰。
- 还有就是原子更新类提供了两个避免ABA问题的类,AtomicStampedRefernce携带版本号,AtomicMarkableReference携带boolean值。
- 什么是ABA问题:一个线程把数据A改成了数据B,然后又重新改回了A,此时另外一个线程读取的时候,发现A没有变化,就误以为还是原来的那个A,这就是ABA问题。
10.说一说synchronized的理解?
-
synchronized是java内置的关键字,解决线程安全性问题,在jvm层面,一共有三种修饰方式:
-
第一个修饰普通方法,锁的是当前对象。
- 假设开两个线程两个实例去掉方法,那么两个实例各自持有一个锁资源,互相不干扰,如果两个线程用同一个实例去掉,那么就持有一个锁资源,第一个线程释放锁后,第二个才会拿到锁。
-
第二个修饰静态方法,锁的是类对象。
- 无论开多少线程多少个实例去调用方法,都持有一个锁资源,生产环境一般不要锁住静态方法,会出现线程阻塞的情况。
-
第三个就是同步代码块。
- 同步代码块锁的是当前对象,只不过比同步方法,更细粒度的去锁住对象。
-
-
两种形式:
-
同步方法:生成的字节码文件中会多一个ACC_SYNCHRONIZED标志位,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标志,如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后释放monitor。在方法执行期间,其他任何线程都无法在获取同一monitor对象,也叫隐式同步。
-
同步代码块:加了synchronized关键字的代码段,生成的字节码文件会多出monitorenter和monitorexit两条指令,每个monitor维护着一个记录着拥有次数的计数器,未被拥有的monitor的该计数器位为0,当一个线程执行monitorenter后,该计数器自增1,当同一个线程执行monitorexit指令的时候,计数器在自增减1.当计数器为0的时候,monitor将被释放,也叫显示同步。
-
-
JDK1.6主要是增加了偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
13.说一说volatile关键字?
-
java的内存模型规定,将所有的变量都存在主存区中,当线程使用变量时,会把主存区里里面的变量复制到自己的工作内存,然后对工作内存的变量进行处理,处理完成之后更新到主存区。
-
volatile第一个特性:保证线程的可见性,当一个变量被volatile修饰时,线程在写入变量时不会把值缓存到寄存器,而是直接刷新到主存区。其他线程在读取值时,会从主存区获取到最新的值。
-
volatile第二个特性:保证有序性,防止指令重排。
- 什么是指令重排:执行顺序可以与代码顺序不一致,此过程就是指令重排。
- 假如说创建对象的操作,创建对象并不是一个原子性操作,创建对象首先是分配空间给对象,在空间内创建对象,然后在赋值给引用,中间如果任何一个步骤乱了,对象创建的就是不完整的。volatile就是这样保证执行的有序性。
-
保证volatile可见性和有序性的原理就是内存屏障。
- 内存屏障就是使CPU或编译器在对内存进行操作的时候,严格按照一定的顺序来执行,现在大多数的计算机为了提高性能采取乱序执行,这使得内存屏障称为必须。
-
volatile 用于多线程环境下的单次操作(单次读或者单次写的场景)
14.说一说并发工具类都有哪些,还有它们的作用?
- CountDownLatch:这个类使一个线程等待其他线程各自执行完毕后在执行。
- 是通过一个计数器来实现的,计数器的初始值是线程的数量,每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在锁上等待的线程就可以恢复工作了。最主要的api有三个await()、await(long timeout)、countDown();
- Semaphore:通常叫它信号量,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
- 可以简单的把它理解成我们停车场入口立着的那个显示器,每有一辆车进入,显示器上剩余的车位减1,每有一辆车离开,显示器上剩余车位加1,当显示器上剩余的车位为0时,入口就不再打开。直到有车辆出去为止。可以用做资源控制。最主要的api有acquire(),获取令牌(占位),release(),释放令牌,availablePermits()判断可用令牌数量。
- CyclicBarrier:可循环利用的屏障,它的作用就是会让所有的线程都等待完毕后,才会执行下一步行动。
- 类似在餐厅只有等朋友全部来齐了,才会开始吃饭。这里各个朋友就是各个线程,餐厅就是CyclicBarrier。最主要的api有await(),表示已将到达栅栏。reset()将屏障设为初始化状态。
- Exchanger:是一个用于线程间协作的工具类,Exchange用于线程间的数据交换。
- 它提供了一个共同点,在这个同步点两个线程可以交换彼此的数据。通过exchange方法进行数据交换,如果第一个线程先执行exchange方法,那么它会等待第二个线程也执行exchange方法,当两个线程都达到共同点时,这两个线程就可以交换数据。最主要的api有exchange,值传递将数据传给对方,exchange(V x,long timeout,TimeUnit unit),超过规定的时间没有传递报异常。
16.说一说什么是ThreadLocal?
-
ThreadLocal就是线程独立的数据区域,每个线程都有一个自己的资源空间,且相互不干扰。用来解决线程不安全问题。
-
举个例子,自定义类中定义一个ThreadLocal变量,分别提供对这个变量的set,get方法,主方法启动两个线程,对ThreadLocal变量进行++,那么运行结果为两个线程互不干扰,分别维护自己的ThreadLocal变量。
-
究其原理:Thread类中有两个变量,threadLocals和inheritableThreadLocals,它们两个都是ThreadLocal的内部类ThreadLocalMap类型,ThreadLocalMap它相当于一个HashMap的结构,默认情况下,每个线程的这两个变量都是null。当线程第一个调用ThreadLocal中的get或者set方法时,才会创建他们。ThreadLocal只是一个容器,线程的变量时存放在Thread里面的threadLocals这个变量里面,线程如果不终止,那么线程对应的 变量会一直存在,所以当线程的本地变量不在使用的时候,要调用remove()方法删除。
-
还有就是ThreadLocalMap中存的是什么,map中存储的key是线程ThreadLocal对象,value存的就是对应的副本值。
-
ThreadLocal它是不支持继承性的,父线程中设置一个变量,子线程是访问不到,InInheritableThreadLocal它是支持继承性的。
-
还有就是ThreadLocal里的变量使用完如果不及时remove()的话,可能会存在内存的泄露。ThreadLocalMap内部其实就是一个Entry[]数组,Entry继承自弱引用类,WeakReference。持有的泛型为ThreadLocal,就是ThreadLocalMap的key,弱引用,只要是进行是垃圾回收,那么就会被回收掉,但是ThreadLocal对象,一直不调用remove()方法,就会出现key为空,value不为空的情况,所以会出现内存泄露。
17.线程池的工作原理?
- 线程池的构造方法:
public ThreadPoolExecutor(
int corePoolSize, //指定线程池中核心线程数,这个几个线程,即使在没有用到的时候,也不会被回收。
int maximumPoolSize, //线程池中可以容纳的最大线程数。
long keepAliveTime, //就是除了核心线程数外,其他线程空闲保留的最长时间。
TimeUnit unit, //就是keepAliveTime参数的使劲按单位。
BlockingQueue<Runnable> workQueue, //等待队列,核心线程任务满了且没有空闲,新进入的线程会被放入等待队列。
ThreadFactory threadFactory, //创建线程的线程工厂。
RejectedExecutionHandler handler //拒绝策略,在核心线程满了且没有空闲,队列中也满了,最大容纳线程数也满了的情况下,执行handler。
)
-
先判断线程池中的核心线程们是否空闲,如果空闲,就把这个新的任务指派给某一个空闲线程去执行。如果没有空闲,并且当前线程池中的核心线程数还小于corePoolSize,那就在创建一个核心线程。如果线程池的线程数已经达到核心线程数,并且这些线程都繁忙,就把这个新来的任务放到等待队列中。如果等待队列满了,那么查看一下当前线程数是否达到maximumPoolSize,如果还未达到,就继续创建线程。如果已经到达了,就交给RejectedExecutionHandler来决定怎末处理这个任务。
-
RejectedExecutionHandler拒绝策略有四种:
- AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满。
- DisCardPolicy:不执行新任务,也不抛出异常。
- DisCardOldSetPolicy:将消息对列中的第一个任务替换为当前新进来的任务执行。
- CallerRunsPolicy:直接由调用execute的线程来执行当前任务。
18.线程一共几种状态,之间的状态是怎样转换的?
-
线程的状态一共有六种分别是新线程、运行、阻塞、等待、超时等待、终止。
- 新线程就是先创建一个线程对象,但是还没有调用start()方法。
- 运行就是调用了start()方法之后在JVM中执行的线程。
- 阻塞就是等待获取synchronized锁的状态的线程。
- 等待就是当对象调用wait()、join()方法时,进入等待状态,需要被唤醒。
- 超时等待就是调用wait()方法设置时间参数,当超过等待的时间后还没被唤醒,就直接进入运行状态。
- 终止就是线程执行完毕。
-
线程之间的状态切换
-
首先创建一个线程对象,这是一个新线程,调用start()方法,进入运行状态。
-
当线程对象调用wait()方法时,进入等待状态,当对象调用notify()、notifyAll()时进入运行状态。
-
当对象调用wait(时间参数)的时候进入超时等待状态,如果线程对象被唤醒,或者超过了等待的时间之后,进入运行状态。
-
如果线程正在等待其他线程释放锁资源,进入阻塞状态,获取锁资源之后,进入运行状态。
-
当线程运行完之后,进入终止状态。
-
19.你知道的进程间的调度算法有哪些?
-
先来先服务调度算法
- 按照作业/进程到达的先后顺序进行调度。
-
短作业优先调度算法
- 短作业/进程调度算法在实际情况中占有很大的比例,为了使它们优先执行。
-
高响应比优先调度算法
- 在每次调度时,先计算各个作业的优先权,优先权=总响应时间/要求服务时间。
-
时间片轮转调度算法
- 轮流的为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。
-
优先级调度算法
- 根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理。
20.线程的调度算法是怎样的,Java用的是那种?
-
线程调度是指系统为线程分配CPU使用权的过程,主要分两种:
-
协同式线程调度:线程执行时间由线程本身控制,线程把自己的工作执行完成后,要主动通知联系系统写换到另外一个线程上。最大好处就是实现简单,且切换操作对线程自己式可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一致阻塞在那里。
-
抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
-
-
Java线程调度就是抢占式调度,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程。
21.Java中都有哪些锁?
-
悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程想去拿数据的时候就会阻塞,比如synchronized。
-
乐观锁:每次拿数据的时候都认为别的线程不会修改,更新的时候会判断其他线程是否会去更新数据,通过版本号判断,如果数据被修改了就拒绝更新,比如CAS是乐观锁。
-
公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说,如果一个线程组里,能保证每个线程都能拿到锁,比如ReentrantLock。
-
非公平锁:获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是会存在线程饿死,也一直拿不到锁。
-
可重入锁:也叫递归锁,在外层使用锁后,同一个线程可以重复获取到锁。
-
不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
-
自旋锁:一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获取锁。
-
共享锁:也叫读锁,能查看但是无法修改和删除的一种数据锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享。
-
互斥锁:也叫写锁,该锁每一次只能别一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。
-
偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,获取锁的代价更低。
-
轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点。
-
重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会一直循环下去,当自旋一定次数的时候且还没有获取到锁,就会进入阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低。
22.了解CAS不,能否解释下什么是CAS?
-
Compare And Swap,即比较在交换,是实现并发应用的一种技术。
-
底层通过Unsafe类实现原子性操作包括三个操作数:内存地址(V)、预期原值(A)和新值(B)。
-
如果内存位置的值与预期原值相匹配,那么处理器回自动将该位置值更新为新值,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
-
CAS这个属于乐观锁,性能较悲观锁有很大的提高。
-
atomic原子类底层就是CAS实现,一定程度比synchonized好,因为后者是悲观锁。
23.线程池不允许使用Executors去创建,要通过ThreadPoolExecutor的方式原因?
-
Executors创建的线程池底层也是调用ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等,如果使用不当,会造成资源耗尽问题。
-
直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险。
-
常见线程池问题:
-
newFixedThreadPool和newSingleThreadExecutor:队列使用LinkedBlockingQueue,队列长度为Integer.MAX_VALUE,可能造成堆积,导致OOM。
-
newScheduledThreadPool和newCachedThreadPool:线程池里面允许最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM。
-
24.ThreadPoolTaskExecutor和ThreadPoolExecutor的区别?
-
ThreadPoolExecutor,这个类是JDK中的线程池类,继承自Executor,里面有一个execute()方法,用来执行线程,线程池主要提供一个线程队列,队列中保存着所有等待状态的的线程,避免了创建与销毁的额外开销。
-
ThreadPoolTaskExecutor,是Spring包下的,是Spring为我们提供的线程池类。
25.知道AQS吗?能否介绍下它的核心思想。
- AQS全称(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。它是一个Java提高的底层同步工具类,比如CountDownLatch、ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、FutureTask等等皆是基于AQS的。
- AQS简单来说:是用一个int类型的变量表示同步状态,并提供了一些列的CAS操作来管理这个同步状态对象
一个是state(用于计数器,类似gc的回收计数器)
一个是线程标记(当前线程是谁加的锁)
一个是阻塞队列(用于存放其他未拿到锁的线程)
-
例子:线程A调用了lock()方法,通过CAS将state赋值为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)方法初始化一个等待队列。
- acquireQueued()是现成在等待队列中获取资源,一直获取到资源后才返回,如果在等待过程中被中断,则返回true,否则返回false。
-
release(int arg):好比解锁unlock
-
独占模式下线程释放指定量的资源,里面是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了:在自定义同步在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
-
unparkSuccessor方法用于唤醒等待队列中下一个线程。
-
-
线程获取到锁成功后直接返回,不会进入等待队列中,只有获取锁失败才会进入等待队列中。获取失败时则将当前线程封装为Node.EXCLUSIVE的Node节点插入AQS的阻塞对列中的尾部。调用LockSupport.park(this)方式阻塞自己。
32.你知道的AQS有几种同步方式,实现同步器一般要覆盖哪些方法?
-
独占式:比如ReentrantLock
-
共享式:比如Semaphore
-
存在组合:组合式的如ReentrantReadWriteLock,AQS为使用提供了底层支撑,使用者可以自由实现组装实现。
-
实现同步器要重写的方法:
- boolean tryAcquire(int arg)
- boolean tryRelease(int arg)
- int tryAcquireShared(int arg)
- boolean tryReleaseShared(int arg)
- boolean isHeldExclusively()
-
实现支持独占锁的同步器应该实现tryAcquire、 tryRelease、isHeldExclusively
-
实现支持共享获取的同步器应该实现tryAcquireShared、tryReleaseShared、isHeldExclusively
33.有看过ReentrantLock的源码吗?
-
ReentrantLock中有一个内部类Sync继承自AQS,继承自Sync有公平锁FairSync和NonFairSync非公平锁的实现,默认构造方法式实现非公平锁,通过传入一个boolean类型值来确定是否为公平锁,true就是公平锁,加锁操作时,看sync实现的是公平锁还是非公平锁,lock()调用require()进行资源申请。unlock()就只调用release方法。
-
公平锁加锁过程:获取state的状态,判断state==0表示锁未被占用,可以抢占锁资源,在判断等待队列里面有没有其他等待线程,没有的话直接CAS更新,加锁操作,如果state!=0,由于是重入锁,则判断是否同个线程申请锁,如果是则state++,然后更新state值。如果不是当前线程就构建node对象加入到队列的尾部。
-
非公平锁加锁过程:不管等待队列里面是否有其他线程,直接使用CAS更新state值,如果更新成功,即state==0则加锁成功,如果更新失败,即state!=0,则按公平锁逻辑执行。
34.你知道ReentrantReadWriteLock和ReentrantLock的区别吗?
- ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现类,实现了读写锁的分离。
- 支持公平和非公平,底层也是基于AQS实现。
- 允许锁降级,从写锁降级为读锁,先获取写锁,然后获取读锁,最后释放写锁,但不能从读锁升级到写锁。
- 获取读锁后还可以获取读锁,获取了写锁之后既可以再次获取写锁又可以获取读锁。
- 读锁是共享的,写锁是独占的。读和读之间不会互斥,读和写、写和写之间才会互斥,主要提升了读写的性能。
- ReentrantLock是独占锁且可重入,相比synchronized而言功能更加丰富也更适合复杂的并发场景。但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防止线程A在写数据,线程B在读数据造成数据不一致,但是线程A在读数据,线程C也在读数据,读数据是没必要加锁的,但是还是加锁了,所以就有了ReadWriteLock读写锁接口。
35.你知道阻塞队列BlockingQueue不?简单介绍下。
-
BlockingQueue:JUC包下提供了线程安全的队列访问的接口,并发包下很多高级同步类的实现都是基于阻塞队列实现的。
- 当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满。
- 从阻塞队列读取数据时,如果队列为空,线程将会阻塞等待直到队列里面是非空的时候。
-
常见的阻塞队列:
- ArrayBlockingQueue:基于数组实现的一个阻塞队列,需要指定容量大小,FIFO队列。
- LinkedBlockingQueue:基于链表实现的一个阻塞队列,如果不指定容量大小,默认Integer.MAX_VALUE,FIFO先进先出顺序。
- PriorityBlockingQueue:一个支持优先级的无界阻塞队列,默认情况下元素采用自然顺序序升排序,也可以定义排序实现java.lang.Comparable接口。
- DelayQueue:延迟队列,在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素,里面的对象必须实现java.util.concurrent.Delayd接口并实现CompareTo和getDelay方法。
36.你知道非阻塞队列ConcurrentLinkedQueue不,它怎么实现线程安全的?
-
ConcurrentLinkedQueue是基于链表实现的无界线程安全队列,采用FIFO进行排序。
-
底层结构是Node,链表头部和尾部节点是head和tail,使用节点变量和内部类属性使用volatile声明保证了有序性和可见性。
-
插入、移除、更新操作使用CAS无所操作,保证了原子性。
-
假如多线程并发修改导致CAS更新失败,采用for循环插入保证更新操作成功。
37.线程并发的三要素都有哪些?
-
原子性:一个不可再被分割的颗粒,原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题。
-
有序性:程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序。
-
可见性:一个线程A对共享变量的修改,另一个线程B能够立刻看到。
38.volatile 变量和 atomic 变量有什么不同?
- volatile变量可以保证变量的有序性和可见性,但是它不能保证原子性。例如volatile修饰 count 变量那么 count++ 操作就不是原子性的。
- 而AtomicInteger 类提供的方法就是让这个变量具有原子性,通过CAS去操作的。