文章目录有道无术,术尚可求,有术无道,止于术。
资料整理来自网络
- 1. 并发的三大特性?
- 2. 进程与线程的区别?
- 3. 并行与并发的区别?
- 4. 什么是Java内存模型JMM?
- 5. 谈谈你对对线程安全的理解
- 6. synchronized的作用?
- 7. volatile关键字的作用?
- 8. synchronized的使用方式有哪几种?
- 9. 多线程的创建方式有哪些?
- 10. 实现Runnable接口创建多线程程序的好处有哪些?
- 11. callable 与 runnable 的区别?
- 12. 什么是线程池?为什么要使用它?
- 13. 常用的线程池有哪些?
- 14. 线程池的 7 大参数?
- 15. 线程池的拒绝策略有哪些?
- 16. 线程池处理流程?
- 17. start()方法和run()方法的区别?
- 18. sleep方法和wait方法有什么区别?
- 19. 线程池中线程复用原理?
- 20. 线程的生命周期?
- 21. 什么是JUC?
- 22. 说说JUC中的CAS机制?
- 23. 说说JUC中的AQS?
- 24. 守护线程是什么?
- 25. 如何停止一个正在运行的线程?
- 26. 什么是ThreadLocal?
- 27. Sychronized和ReentrantLock的区别?
- 28. 说说Sychronized的偏向锁、轻量级锁、重量级锁?
- 29. 什么是多线程的“锁池”和“等待池”?
- 30. ReentrantLock中的公平锁和非公平锁的底层实现?
- 31. ReentrantLock中tryLock()和lock()方法的区别?
- 32. notify()和 notifyAll()有什么区别?
- 33. 什么是java对象头?
- 34. 多线程锁的升级(锁膨胀)原理是什么?
- 35. 什么是死锁?
- 36. 怎么防止死锁?
- 37. 有三个线程 T1,T2,T3,怎么确保它们按顺序执行?
- 38. 线程池中的的线程数一般怎么设置?需要考虑哪些问题?
- 39. 说下java.util.concurrent.atomic包?
- 40. 说下java.util.concurrent.locks包?
- 41. 说下JUC中的各种锁及其特性?
- 42. 可重入锁和不可重入锁的区别?
- 43. 线程池的关闭方式有几种,各自的区别是什么?
- 44. 可以创建volatile 数组吗?
- 45. 用过读写锁吗,原理是什么,一般在什么场景下用?
- 46. Java 中用到的线程调度算法是什么?
- 47. LockSupport 的优势?
- 48. CyclicBarrier和CountDownLatch的区别
- 49. 什么是自旋 ?
- 50. SynchronizedMap和ConcurrentHashMap有什么区别?
并发的三大特性为原子性、可见性、有序性,如果需要实现线程安全,必须保证这三大特性。
原子性(Atomicity)
一个操作或者多个操作,要么全部执行并且不被打断,要么就都不执行。
i++操作分为以下几步:
- 将i从主存读到工作内存中的副本中
- +1的运算
- 将结果写入工作内存
- 将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)
可见性(Visibility)
当一个线程修改了共享变量的值,其他线程会马上知道这个修改。当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从缓存中读取。
若两个线程在不同的CPU,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值还是之前的,线程1对变量的修改线程2没看到,这个就是可见性问题。
对于可见性,Java提供了很多方式来保证:
- 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主内存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
- 通过synchronized和Lock也能够保证可见性,保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
有序性(Ordering)
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
总结:
- synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。
- 在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。
- 我们判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性)
进程
进程是指在内存中运行的应用程序实例,每个进程都有独立的内存空间;比如打开QQ,则表示开启了一个QQ进程。可通过任务管理器查看当前运行进程。
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的程序。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
线程
操作系统能够进行运算调度的最小单位。线程是指进程中的一个执行任务(控制单元),一个进程可以同时并发运行多个线程,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,任务管理器详细信息可查看。
一个进程之内可以分为一到多个线程。一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。
进程与线程的区别:
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂,同一台计算机的进程通信称为 IPC(Inter-process communication),不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP。线程通信相对简单,因为它们共享进程内的内存,比如多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
时间片: CPU分配给各个程序的运行时间(很小的概念);
并行(concurrent):指两个或多个事件在同一时刻点发生;单核单线程CPU运行时,实际线程高速切换,轮流执行;
并发(parallel):指两个或多个事件在同一时间段内发生;
单核cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。一般会将这种线程轮流使用 CPU 的做法称为并发。
多核 cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。
4. 什么是Java内存模型JMM?Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
主内存:主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
工作内存:每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
lock->read->load->use->assign->store->write->unlock
- lock(锁定)
作用于主内存中的变量,它将一个变量标志为一个线程独占的状态。 - unlock(解锁)
作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。 - read(读取)
作用于主内存中的变量,它把一个变量的值从主内存中传递到工作内存,以便进行下一步的load操作。 - load(载入)
作用于工作内存中的变量,它把read操作传递来的变量值放到工作内存中的变量副本中。 - use(使用)
作用于工作内存中的变量,这个操作把变量副本中的值传递给执行引擎。当执行需要使用到变量值的字节码指令的时候就会执行这个操作。 - assign(赋值)
作用于工作内存中的变量,接收执行引擎传递过来的值,将其赋给工作内存中的变量。当执行赋值的字节码指令的时候就会执行这个操作。 - store(存储)
作用于工作内存中的变量,它把工作内存中的值传递到主内存中来,以便进行下一步write操作。 - write(写入)
作用于主内存中的变量,它把store传递过来的值放到主内存的变量中。
不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问。
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。
在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
6. synchronized的作用?概念:多个线程同时操作共享资源时会引起的线程不安全问题。在JDK1.5版本以前,要解决这个问题需要使用synchronized关键字,synchronized提供了一种排他机制,也就是在同一时间只能有一个线程执行某些操作。
官网解释:synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行,具体表现如下:
- synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
- synchronized关键字包括monitor enter 和 monitor exit两个JVM指令,它能够保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存。
- synchronized的指令严格遵守java happens-before规则,一个monitor exit 指令之前必定要有一个monitor enter。
Java语言提供了弱同步机制,即volatile变量,以确保变量的更新通知其他线程。volatile变量具备变量可见性、禁止重排序两种特性。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
变量可见性
保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。
禁止重排序
volatile禁止了指令重排。比sychronized更轻量级的同步锁。在访问volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。
当对非volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同CPU cache中。而声明变量是volatile的,JVM 保证了每次读变量都从内存中读,跳过CPU cache这一步。
适用场景
值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。
8. synchronized的使用方式有哪几种?synchronized可以用于对代码块或方法进行修饰,而不能够用于对class 以及变量进行修饰。
1、 同步方法
在方法修饰符后添加synchronized关键字
public synchronized void ticket(){
try {
System.out.println(Thread.currentThread().getName() + "抢到第" + MAX-- + "张票");
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2、同步代码块
synchronized代码块,并添加一个锁对象。
public class TicketWindow implements Runnable {
private static int MAX = 100;
private final Object MUTEX = new Object();
public void run() {
// 抢票
while (MAX > 0) {
synchronized (MUTEX) {
System.out.println(Thread.currentThread().getName() + "抢到第" + MAX-- + "张票");
}
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意事项
- 使用synchronized代码块时,锁对象不能为null
private final Object MUTEX = new Object();
-
作用域
由于synchronized关键字存在排他性,也就是说所有的线程必须串行地经过synchronized保护的共享区域,如果synchronized作用域越大,则代表着其效率越低,甚至还会丧失并发的优势,synchronized关键字应该尽可能地只作用于共享资源(数据)的读写作用域。 -
锁对象
同步代码块:传入的对象锁
同步方法:非静态方法为对象实例this作为对象锁,静态方法是使用class类锁 -
同一个Runable实例
Runnable实例作为线程逻辑执行单元传递给Thread时,应为同一个实例,不然起起到互斥的作用。 -
多个锁的交叉导致死锁
多个锁的交叉很容易引起线程出现死锁的情况
synchronized (MUTEX) {
System.out.println(Thread.currentThread().getName() + "抢到第" + MAX-- + "张票");
synchronized (MUTEX02) {
System.out.println(Thread.currentThread().getName() + "抢到第" + MAX-- + "张票");
}
}
9. 多线程的创建方式有哪些?
1、继承Thread类
继承Thread类,重写run方法,创建线程的对象并调用start开启线程(注意:调用的是start方法,不是run方法)。
2、实现Runnable接口
3、实现Callable接口
4、使用线程池
- 避免了单继承的局限性:一个类只能继承一个类,类继承了Thread类就不能继承其他类,实现了Runnable接口,还可以继承其他的类,实现其他的接口
- 增强了程序的扩展性,降低了程序的耦合性(解耦),实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
- Runnable 比 Callable 古老一点, 前者源于 JDK1.0, 后者源于 Java 5.
- Runnable 接口用 run() 方法来描述一个任务(task), 而 Callable 使用 call().
- run()方法不会返回结果, 因为它的返回类型是 void. 而 Callable 是个 支持泛型的接口, 当要实现(implement)一个Callable接口的时候, 就会提供一个返回值类型.
- run()方法不会抛出 checked exception 异常, 而 call() 方法可以
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。
所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
13. 常用的线程池有哪些?Java其实早就已经给我们提供了六种快速创建线程池的方法,并且不需要设置繁琐参数,开箱即用。
- FixedThreadPool(有限线程数的线程池):特点是他的核心线程数和最大线程数是一样的,你可以把它看作成是一个固定线程数的线程池,因为他不会去将超出线程数的线程缓存到队列中,如果超出线程数了,就会按线程拒绝策略来执行。
- CachedThreadPool (无限线程数的线程池):这是一个可以缓存线程任务的线程池,并且直接执行,他的特点就是可以无限的缓存线程任务,最大可以达到Integer.MAX_VALUE,为 2^31-1,反正很大。
- ScheduledThreadPool (定时线程池):这个线程就是为了定时而发明的,它支持定时或周期性执行任务,比如10秒钟执行一次任务。
- SingleThreadExecutor (单一线程池)这个线程很适用于需要按照提交顺序去执行线程的场景,因为在他的线程池中,只有一个线程可以执行,他的原理其实跟FixedThreadPool有点相像,只不过他的核心线程数和最大线程数都是1,这样当提交者去提交线程的时候,就必须先让线程池中的线程执行完成之后才会去执行接下来的线程。这样就保证了线程的顺序性,而这种顺序性,前面几种线程的机制是做不到的 - SingleThreadScheduledExecutor(单一定时线程池):这个线程池像是SingleThreadExecutor和ScheduledThreadPool生的娃,他有SingleThreadExecutor单一线程池的特性,一次只能执行一个线程,又可以完成ScheduledThreadPool线程池的定时功能。如果你能理解这个线程服务的能力,你就能理解这个线程的能力。
- ForkJoinPool :ForkJoinPool线程池最大的特点就是分叉(fork)合并(join),将一个大任务拆分成多个小任务,并行执行,再结合工作窃取模式(worksteal)提高整体的执行效率,充分利用CPU资源。
所谓的线程池的 7 大参数是指,在使用 ThreadPoolExecutor 创建线程池时所设置的 7 个参数,如以下源码所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这 7 个参数分别是:
-
corePoolSize:核心线程数。是指线程池中长期存活的线程数。
-
maximumPoolSize:最大线程数,线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
-
keepAliveTime:空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。
-
TimeUnit:时间单位。空闲线程存活时间的描述单位。
-
BlockingQueue:线程池任务队列。
-
ThreadFactory:创建线程的工厂。线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
-
RejectedExecutionHandler:拒绝策略。当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。
拒绝策略提供顶级接口 RejectedExecutionHandler ,其中方法 rejectedExecution 即定制具体的拒绝策略的执行逻辑。
jdk默认提供了四种拒绝策略:
- CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
- AbortPolicy - 丢弃任务,并抛出拒绝执行RejectedExecutionException异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
- DiscardPolicy - 直接丢弃,其他啥都没有
- DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
- 在创建了线程池之后,等待提交过来的任务请求
- 当调用execute()方法添加一个请求任务的时候,线程池会做出判断
- 如果正在运行的线程数量小于corePoolSize(核心线程数),那么马上创建线程运行这个程序
- 如果正在运行的线程数量大于或者等于corePoolSize,那么将这个任务放入队列
- 如果这个时候队列满了并且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻执行这个任务
- 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务的时候,它会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定时间(keepAliveTime)时:线程会判断如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉。
start() 函数用来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码;通过调用 Thread 类的 start() 方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此 Thread 类调用方法 run() 来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
run() 函数只是类的一个普通函数而已,如果直接调用 run 方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待 run 方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
18. sleep方法和wait方法有什么区别?所属对象不同
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。
wait()方法,则是属于Object类中的。
原理不同
sleep()方法是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动苏醒。
wait()方法是Object类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程用调用notify()或notifyAll()时才苏醒过来,开发人员也可以给它指定一个时间使其自动醒来。
锁的处理机制不同
由于sleep()方法的主要作用是让线程暂停一段时间,时间一到则自动恢复,不涉及线程间的通信,因此调用sleep()方法并不会释放锁。
当调用wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。
使用区域不同
wait()方法必须放在同步控制方法或者同步语句块中使用,而sleep方法则可以放在任何地方使用。
sleep()方法必须捕获异常,而wait()、notify()、notifyAll()不需要捕获异常。在sleep的过程中,有可能被其他对象调用它的interrupt(),产生InterruptedException异常。
19. 线程池中线程复用原理?线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start(O)来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务“中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。
20. 线程的生命周期?传统线程模型中把线程的生命周期描述为五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
- 新建:当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1=new Thread()。
- 就绪:就是Thread实例调用了start()方法后,线程已经被启动,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行。例如:t1.start()。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能。
- 阻塞:线程在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态。
- 死亡:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。分为自然终止和异常终止。
其中值得一提的是阻塞(Blocked)这个状态:线程在Running的过程中可能会遇到各种阻塞(Blocked)情况,如下:
- 等待阻塞:调用运行线程的wait()方法,虚拟机会把该线程放入等待池(wait blocked pool)。如需将阻塞状态下的线程唤醒,可调用notify或者notifyAll()方法,线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到就绪行状态(Runnable)。
- 锁定(同步)阻塞:运行线程获取对象的同步锁时,该锁已被其他线程获得,虚拟机会把该线程放入锁定池(lock blocked pool )。
- 其他阻塞:调用运行线程的sleep()方法、join()方法或线程发出I/O请求时,进入阻塞状态。
JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题。
与JUC有关的包共有三个,分别为java.util.concurrent、java.util.concurrent.atomic和java.util.concurrent.locks,其中java.util.concurrent又可以分为三大类,分别为工具类,集合与线程池。
22. 说说JUC中的CAS机制?CAS的全称为Compare-And-Set, 它是CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。
CAS不加锁,保证一次性,但是需要多次比较。
缺点:
- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
- 只能保证一个共享变量的原子操作,对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
ABA问题
ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了。
23. 说说JUC中的AQS?AQS 是 AbstractQueuedSynchronizer的缩写,中文 抽象队列同步器,是构建各类锁和同步器的基础实现,这个类是并发包中的核心。
AQS就是一个同步器,要做的事情就相当于一个锁,所以就会有两个动作:一个是获取,一个是释放。获取释放的时候该有一个东西来记住他是被用还是没被用,这个东西就是一个状态。如果锁被获取了,也就是被用了,还有很多其他的要来获取锁,总不能给全部拒绝了,这时候就需要他们排队,这里就需要一个队列。这大概就清楚了AQS的主要构成了:
- 获取和释放两个动作
- 同步状态(原子操作)
- 阻塞队列
守护线程,也可称为服务线程,当程序中没有可服务的线程时会自动离开。因此,守护线程的优先级比较低,用于为其他的线程等提供服务。
java中最典型的守护线程就是垃圾回收线程。当我们的应中用没有任何常规线程运行时,就不会产生垃圾了,垃圾回收线程就无服务对象了,就会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
25. 如何停止一个正在运行的线程?终止线程的三种方法
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止线程(这个方法不推荐使用,由于stop和suspend、resume同样,也可能发生不可预料的结果)。
- 使用interrupt方法中断线程。
ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。
ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值。
如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象。
ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)。
27. Sychronized和ReentrantLock的区别?- sychronized是一个关键字,ReentrantLock是一个类
- sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
- sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
- sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
- sychronized底层有一个锁升级的过程
偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到
轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竟争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
重量级锁:如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞。
自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
29. 什么是多线程的“锁池”和“等待池”?锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAll()后等待池的线程才会开始去竟争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竟争锁。
不管是公平锁还是非公平锁,一旦没竟争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
31. ReentrantLock中tryLock()和lock()方法的区别?tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false。
lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值。
32. notify()和 notifyAll()有什么区别?如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
33. 什么是java对象头?由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。
Java对象保存在内存中时,由以下三部分组成:
-
对象头
-
实例数据
-
对齐填充字节
java的对象头由以下三部分组成:
-
Mark Word
-
指向类的指针
-
数组长度(只有数组对象才有)
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
synchronized用的锁存在Java对象头里,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。32位JVM的Mark Word可能变化存储为以下5种数据:
34. 多线程锁的升级(锁膨胀)原理是什么?JVM优化synchronized的运行机制,当JVM检测到不同的竞争状态时,就会根据需要自动切换到合适的锁,这种切换就是锁的升级。升级是不可逆的,也就是说只能从低到高。
锁级别:无锁->偏向锁->轻量级锁->重量级锁
-
当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
-
当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
-
当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
-
当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
-
偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
-
轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
-
自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
36. 怎么防止死锁?死锁的四个必要条件:
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
37. 有三个线程 T1,T2,T3,怎么确保它们按顺序执行?在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
38. 线程池中的的线程数一般怎么设置?需要考虑哪些问题?主要考虑下面几个方面:
- 线程池中线程执行任务的性质:
计算密集型的任务比较占 cpu,所以一般线程数设置的大小 等于或者略微大于 cpu 的核数;但 IO 型任务主要时间消耗在 IO 等待上,cpu 压力并不大,所以线程数一般设置较大。 - cpu 使用率:
当线程数设置较大时,会有如下几个问题:第一,线程的初始化,切换,销毁等操作会消耗不小的 cpu 资源,使得 cpu 利用率一直维持在较高水平。第二,线程数较大时,任务会短时间迅速执行,任务的集中执行也会给 cpu 造成较大的压力。第三, 任务的集中支持,会让 cpu 的使用率呈现锯齿状,即短时间内 cpu 飙高,然后迅速下降至闲置状态,cpu 使用的不合理,应该减小线程数,让任务在队列等待,使得 cpu 的使用率应该持续稳定在一个合理,平均的数值范围。所以 cpu 在够用时,不宜过大,不是越大越好。可以通过上线后,观察机器的 cpu 使用率和 cpu 负载两个参数来判断线程数是否合理。 - 内存使用率:
线程数过多和队列的大小都会影响此项数据,队列的大小应该通过前期计算线程池任务的条数,来合理的设置队列的大小,不宜过小,让其不会溢出,因为溢出会走拒绝策略,多少会影响性能,也会增加复杂度。 - 下游系统抗并发能力:
多线程给下游系统造成的并发等于你设置的线程数,例如:如果是多线程访问数据库,你就考虑数据库的连接池大小设置,数据库并发太多影响其 QPS,会把数据库打挂等问题。如果访问的是下游系统的接口,你就得考虑下游系统是否能抗的住这么多并发量,不能把下游系统打挂了。
java.util.concurrent.atomic原子操作类包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。
java.util.concurrent.atomic中的类可以分成4组:
- 标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
- 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
- 复合变量类:AtomicMarkableReference,AtomicStampedReference
在使用synchronized关键字的时候,会在生成的字节码中包含对应的指令或者标记。JVM在执行的时候,会根据这些标记获取和释放锁。锁的获取和释放都是由JVM控制的,我们并不能根据自己的需求控制获取锁和释放锁的一些细节。
如果需要可操作性、可控制性更强的锁,Java中提供了java.util.concurrent.locks包,其中有一些有用的锁的接口和实现。
在java.util.concurrent.locks 这个包中提供了Lock接口,该接口的定义如下:
public interface Lock {
void lock();
void lockInterruptibly();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throw InterruptedException;
boolean unlock();
Condition newCondition();
}
这其中的lock方法和unlock方法没有什么好讲的,其实作用和synchronized(lock)差不多。lock方法会阻塞式的等待锁。
lockInterrupitbly()方法中,获取到锁的线程被中断的时候,会抛出中断异常,并释放持有的锁。使用synchronized关键字获取锁的线程是做不到这一点的。
tryLock()方法则不会阻塞,如果没有获取到锁,会立即返回false,不会一直等待。
tryLock(long time, TimeUnit unit)则会在指定的时间段内等待锁。如果没有等到,则返回false。
最后一个方法newCondition(),这个是用于获取Condition对象,该对象用于线程之间的同步。
41. 说下JUC中的各种锁及其特性?1. ReentrantLock
从名字上就可以看出,这是一个可重入锁。该类的实现,依赖于其中的一个内部抽象类Sync,该类有两个具体的实现NonfairSync和FairSync。一个用于实现非公平锁,一个用于实现公平锁。具体的获取锁和释放锁的逻辑,其实都在Sync类及其两个子类中。
ReentrantLock是怎样实现可重入的呢?在ReentrantLock中,会有一个变量用来保存当前持有锁的线程对象。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread(); //获取当前线程
int c = getState(); //获取当前同步状态的值
if (c == 0) { //当前同步状态没有被任何线程获取的时候
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
上面的代码主要是一个if…else…语句。主体的逻辑是:
- 如果当前锁没有给任何线程持有,则直接获取锁(获取锁的方式使用的是compareAndSetState,底层实际上使用的是Compare And Swap(CAS)机制来实实现的)。获取成功则返回true来表示获取锁成功。
- 如果当前锁已经被其他线程持有,那么判断持有锁的线程是否是当前线程,如果是的话,则增加锁的计数,返回true,表示获取锁成功。
- 如果前面两条都没有成功,则返回false,表示获取锁失败。
可重入锁的释放也不是立即直接释放,而是每次减少计数,直到计数为0。如下面的代码所示:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
在实例化ReentrantLock类的时候,可以传入一个boolean类型的参数,用于指定是需要公平锁还是非公平锁(默认是非公平锁)。
通常来说,该类的使用方式如下:
private Lock lock = new ReentrantLock(); // 默认使用非公平锁
public void someMethodUsingLock(){
......
lock.lock();
try{
......
} catch (Exception e){
......
} finally {
lock.unlock();
}
}
上面的代码中,我们没有将lock.lock()这一行代码放在try中,是因为如果放在try中,并且在执行这一行代码的时候抛出了异常,那么就会进入finally的代码块中,会执行lock.unlock(),但是实际上当前线程可能并没有获取到锁,执行unlock又会抛出异常。
可重入特性实现了,那么公平/非公平又是怎么实现的?公平和非公平在tryLock(long time, TimeUnit unit)这个方法中有不同。我们一直跟踪到具体实现,让我们看看代码:
//非公平锁的实现代码
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
//公平锁的实现代码
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
可以看到,唯一的不同就是一个调用,公平锁在获取锁之前,调用了!hasQueuedPredecessors(),先判断是否有其他的线程已经在排队了。
2. ReadWriteLock
读写锁其实是一个组合锁。我们来看看Java中给出的ReadWriteLock接口:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
在Java给出的实现类ReentrantReadWriteLock中,分别实现了ReadLock和WriteLock类。它们的不同在于,在ReadLock的中,获取锁的代码是
public void lock() {
sync.acquireShared(1);
}
但是在WriteLock中,获取锁的代码是
public void lock() {
sync.acquire(1);
}
从名字上就可以看出,一个是共享锁,一个是排他锁。具体的实现方式,大家可以自己去看看源代码。
3. StampedLock
前面我们介绍的这些锁的实现,都是悲观锁,包括上面的ReentrantReadWriteLock。在ReentrantReadWriteLock中,在读的时候是不能写的,在写的时候也是不能读的。
在Java 8中,又引入了另外一种读写锁,StampedLock。这个锁和前面的ReentrantReadWriteLock的不同之处在于,StampedLock中,如果有线程获取了读锁,正在读取数据,另外一个线程可以获取写锁来写数据。 因此这种锁是一种乐观锁(至少在这种情况下是)。但是乐观锁带来的问题是读取的数据可能不一致,因此需要额外的代码来判断。大致的代码如下:
StampedLock stampedLock = new StampedLock();
......
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
....... //读取数据,可能是读取多个数据
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
//说明读取过程中有写入操作,因此可能读取到错误的数据
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
...... //重新读取数据
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
42. 可重入锁和不可重入锁的区别?
Java多线程有阻塞函数wait()方法和通知函数notify()方法
-
wait():阻塞当前线程
-
notify():唤起被wait()阻塞的线程
这两个方法是成对出现和使用的,要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出IllegalMonitorStateException异常,所以这两个方法必须在同步块代码里面调用。
核心问题场景:当一个线程获得当前实例的锁lock,并且进入了方法A,该线程在方法A没有释放该锁的时候,是否可以再次进入使用该锁的方法B?
不可重入锁:在方法A释放锁之前,不可以再次进入方法B
可重入锁:在方法A释放该锁之前,可以再次进入方法B
1)不可重入锁:
不可重入锁,是指同一个线程不可以重入上锁后的代码段。
当线程在访问A方法的时候,获取的A方法的锁,在A方法锁释放之前不能够访问其他方法(如方法B)的锁。
不可重入锁模型:{}{}{}{}{}都是独立的访问每一个方法,加锁 - 释放;加锁 - 释放。。。
2)可重入锁:
当线程在访问A方法的时候,获取A方法的锁,然后访问B方法获取B方法的锁,并计数加1,以此类推可以访问完了以后依次解锁。
可重入锁模型:{{{{}}}} 每次都可访问另一个方法,且加锁计数器加1,完全释放锁为计数器等于0
可重入锁,是指同一个线程可以重入上锁的代码段,不同的线程进入则需要进行阻塞等待。
Java的可重入锁有:reentrantLock(显式的可重入锁)、synchronized(隐式的可重入锁)
可重入锁诞生的目的就是防止死锁,导致同一个线程不可重入上锁代码段,目的就是让同一个线程可以重新进入上锁代码段。
43. 线程池的关闭方式有几种,各自的区别是什么?1.shutdown
第一种方法叫作 shutdown(),它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。
调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。
2. shutdownNow
它和 shutdown() 的区别就是多了一个 Now,表示立刻关闭的意思,不推荐使用这一种方式关闭线程池。
在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。
44. 可以创建volatile 数组吗?能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
45. 用过读写锁吗,原理是什么,一般在什么场景下用?synchronized和ReentrantLock实现的锁是排他锁,所谓排他锁就是同一时刻只允许一个线程访问共享资源,但是在平时场景中,我们通常会碰到对于共享资源读多写少的场景。对于读场景,每次只允许一个线程访问共享资源,显然这种情况使用排他锁效率就比较低下,那么该如何优化呢?
这个时候读写锁就应运而生了,读写锁是一种通用技术,并不是Java特有的。从名字来看,读写锁拥有两把锁,读锁和写锁。读写锁的特点是:同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;当进行写操作时,同一时刻其他线程的读操作会被阻塞;当进行读操作时,同一时刻所有线程的写操作会被阻塞。对于读锁而言,由于同一时刻可以允许多个线程访问共享资源,进行读操作,因此称它为共享锁;而对于写锁而言,同一时刻只允许一个线程访问共享资源,进行写操作,因此称它为排他锁。
在Java中通过ReadWriteLock来实现读写锁。ReadWriteLock是一个接口,ReentrantReadWriteLock是ReadWriteLock接口的具体实现类。在ReentrantReadWriteLock中定义了两个内部类ReadLock、WriteLock,分别来实现读锁和写锁。ReentrantReadWriteLock底层是通过AQS来实现锁的获取与释放的,因此ReentrantReadWriteLock内部还定义了一个继承了AQS类的同步组件Sync,同时ReentrantReadWriteLock还支持公平与非公平性,因此它内部还定义了两个内部类FairSync、NonfairSync,它们继承了Sync。
ReentrantReadWriteLock除了提供读锁、写锁的释放与获取外,还提供了一些其他和锁状态有关的方法。
ReentrantReadWriteLock底层使用AQS实现,巧妙地使用AQS的状态值的高16位表示获取到读锁的个数,低16位表示获取写锁的线程的可重入次数,并通过CAS对其进行操作实现读写分离,这在读多写少的场景下比较适用。
46. Java 中用到的线程调度算法是什么?线程调度一般指的是系统为线程分配处理器使用权的过程,这个过程会产生上下文切换,即操作系统的CPU利用时间片轮转的方式给每个任务分配一定的执行时间,然后把当前任务状态保存下来,接着加载下一任务的状态并执行,它是一个状态保存与加载的过程。
一般线程调度模式分为两种——抢占式调度和协同式调度。
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,线程的切换不由线程本身决定,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,线程的执行时间由线程本身控制,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
VM规范中规定每个线程都有优先级,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。JVM的规范没有严格地给调度策略定义,一般Java使用的线程调度是抢占式调度,在JVM中体现为让可运行池中优先级高的线程拥有CPU使用权,如果可运行池中线程优先级一样则随机选择线程,但要注意的是实际上一个绝对时间点只有一个线程在运行(这里是相对于一个CPU来说),直到此线程进入非可运行状态或另一个具有更高优先级的线程进入可运行线程池,才会使之让出CPU的使用权。
47. LockSupport 的优势?LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和notify/notifyAll方法实现。wait和notify/notifyAll方法只能在同步代码块里用。
LockSupport是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。它的内部其实两类主要的方法:park(停车阻塞线程)和unpark(启动唤醒线程)。
48. CyclicBarrier和CountDownLatch的区别两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用 作者:java白楠楠 https://www.bilibili.com/read/cv9273575 出处:bilibili
49. 什么是自旋 ?很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
50. SynchronizedMap和ConcurrentHashMap有什么区别?SynchronizedMap一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map。ConcurrentHashMap使用分段锁来保证在多线程下的性能。
ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
另外ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出
ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。



