- Java 多线程
- 并行和并发有什么区别?
- 并发编程三要素?(线程的安全性问题体现在)
- 多线程访问同步方法的7种情况
- Java内存模型
- happens-before原则(深入理解Java虚拟机)
- 进程和线程之间有什么不同?
- 多线程编程的好处是什么?
- 用户线程和守护线程有什么区别?
- 线程B怎么知道线程A修改了变量
- sleep方法和wait方法有什么区别?如何让正在运行的线程暂停一段时间?
- 为什么wait方法和notify/notifyAll()方法要在同步块中被调用
- 多线程同步有哪几种方法?
- java线程的调度策略
- 怎么唤醒一个阻塞的线程
- 什么是Java线程转储(Thread Dump ),Java中如何获取到线程dump文件(线程堆栈)
- 不可变对象对多线程有什么帮助
- 什么是多线程的上下文切换
- 如果你提交任务时, 线程池队列已满,这会发生什么
- Java中用到的线程调度算法是什么
- 什么是线程调度器(Thread Scheduler)和间分片(Time Slicing)?
- 单例模式的线程安全性
- Semaphore有什么作用
- 线程类的构造方法、静态块是被哪个线程用的
- 同步方法和同步块,哪个是更好的选择?
- 你如何确保main()方法所在的线程是Java 程序最后结束的线程?
- 线程之间是如何通信的?
- 为什么Thread类的sleep()和yield ()方法是静态的?
- 如何确保线程安全?
- Thread.interrupt() 方法的工作原理是什么?
- Java并发
- 创建线程的有哪些方式?
- Java Concurrency API中的Lock接口是什么
- Executors类是什么?
- 什么是并发集合类?
- 锁的分类
- 乐观锁常见的两种实现方式
- synchronized的作用?
- volatile关键字的作用
- 什么是CAS
- synchronized、volatile、CAS、Lock比较
- synchronized和ReentrantLock的区别
- 什么是Future?
- 什么是AQS
- 为什么说LockSupport是Java并发的基石?
- 常用的并发工具类有哪些?
- CyclicBarrie(栅栏)和CountDownLatch(闭锁)的区别
- ReadWriteLock是什么(最后一个工具)
- 说一下ConcurrentHashMap的工作原理,put()和get()的工作流程是怎样的?
- ConcurrentHashMap是如何保证并发安全的?
- ConcurrentHashMap是如何扩容的?
- JDK8中的ConcurrentHashMap有一个CounterCell,你是如何理解的?
- get/put操作的原子性
- ConcurrentHashMap中的key和value可以为null吗?为什么?
- ConcurrentHashMap的并发度是什么
- JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?
- ConcurrentHashMap与HashMap有什么区别?
- ConcurrentHashMap和HashTable的效率哪个更高?为什么?
- HashTable和ConcurrentHashMap的锁机制
- ConcurrentHashMap在JDK1.8中为什么要使用内置锁Synchronized来替换ReentractLock重入锁?
- 简单说下对 Java 中的原子类的理解?
- atomic 的原理是什么?原子类是如何利用 CAS 保证线程安全的?(以AtomicInteger为例)
- 原子类高并发会存在的问题
- Java死锁
Java 多线程
线程和进程的区别?
进程
一个进程就是CPU执行的单个任务的过程,是程序在执行过程当中CPU资源分配的最小单位,并且进程都有自己的地址空间,包含了运行态、就绪态、阻塞态、创建态、终止态五个状态。
PID
线程
线程是CPU调度的最小单位,它可以和属于同一个进程的其他线程共享这个进程的全部资源
两者之间的关系
一个进程包含多个线程,一个线程只能在一个进程之中。每一个进程最少包含一个线程。
两者之间的区别
其实最根本的区别在刚开始就说了:进程是CPU资源分配的最小单位,线程是CPU调度的最小单位
进程之间的切换开销比较大,但是线程之间的切换开销比较小。
CPU会把资源分配给进程,但是线程几乎不拥有任何的系统资源。因为线程之间是共享同一个进程的,所以线程之间的通信几乎不需要系统的干扰。
并行和并发有什么区别?
并发
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
并行
并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
并发和并行的区别
并发,指的是多个事情,在同一时间段内同时发生了。 并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。 并行的多个任务之间是不互相抢占资源的、
只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
并发编程三要素?(线程的安全性问题体现在)
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么 全部执行成功要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。 (synchronized,volatile)缓存一致性问题
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会进行指令重排序)
- 一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(单个线程)。多线程会存在安全问题
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
出现线程安全问题的原因:
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决办法:
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
多线程访问同步方法的7种情况
- 两个线程同时访问一个对象的同步方法
这里它们是同一把锁(this),争抢同一把锁的时候必然要相互等待只能有一个人持有
- 两个线程访问的是两个对象(实例)的同步方法
这种情况下synchronized是不起作用的,它们之间是不受干扰的原因是它们的锁对象不是同一个,所以不干扰并行执行
- 两个线程访问的是synchronized的静态方法
虽然它们是不同的实例,但是只要它们是静态的,那么对应的锁就是同一把,它们会一个一个执行.
- 同时访问同步方法与非同步方法(被synchronized修饰和没有被synchronized修饰的方法)
同时访问同步方法与非同步方法,synchronized只作用于加了同步的方法中,没有加synchronized的方法不会受到影响
- 访问同一个对象的不同的普通同步方法(不是static非静态的方法,那么它们是串行还是并行)
这两个都加了Synchronized的方法是同一个实例拿到的都是一样this,所以这两个方法没有办法同时运行,它们是串行执行的
- 同时访问静态synchronized和非静态synchronized方法(都是被synchronized所修饰的,不同的是一个是静态的一个不是静态的,这个时候有多线程去并发执行,会带来怎样的同步效果)
静态synchronized修饰的方法是类锁,非静态synchronized方法是方法锁形式,一个是.class一个是对象实例的this虽然都加了Synchronized但是它们还是会同时执行同时结束
- 如果方法抛出异常后,会释放锁吗?
方法抛出异常之后JVM会帮它释放锁,其它方法就会获得这把锁接着往下执行
(跟lock接口比较,lock即便抛出异常,你没有显式的去释放锁,lock是不会释放的,synchronized一旦抛出异常它会自动的释放的)
Java内存模型
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。
注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。
Java语言 本身对 原子性、可见性以及有序性提供了哪些保证
保证原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作
x = 10; //语句1
y = x; //语句2
x++; //语句3,特别注意
x = x + 1; //语句4
只有语句1是原子性操作
有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。
如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
保证可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
保证有序性
在Java里面,可以通过volatile关键字来保证一定的“有序性”
另外可以通过synchronized和Lock来保证有序性
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
happens-before原则(深入理解Java虚拟机)
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
进程和线程之间有什么不同?
一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。Java运行环境是一个包含了不同的类和程序的单一进程。线程可以被称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。
多线程编程的好处是什么?
1、发挥多核 CPU 的优势
多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。
2、防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但 是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
3、便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
用户线程和守护线程有什么区别?
什么是用户线程?用户态
当我们在Java程序中创建一个线程,它就被称为用户线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。
1、应用程序里的线程,一般都是用户自定义线程。
2、用户也可以在应用程序代码自定义守护线程,只需要调用Thread类的设置方法设置一下即可。
3、用户线程和守护线程几乎一样,唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了。 因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有守护线程。 也就是说,只要有任何非守护线程还在运行,程序就不会终止。
守护线程是什么?内核态
守护线程(即daemon thread),是个服务线程,准确地来说就是服务用户线程,这是它的作用
1、守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。
2、再换一种说法,如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。
3、守护线程又被称为“服务进程”“精灵线程”“后台线程”,是指在程序运行是在后台提供一种通用的线程,这种线程并不属于程序不可或缺的部分。 通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。
创建守护线程?
在Java语言中,守护线程一般具有较低的优先级,它并非只由JVM内部提供,
用户在编写程序时也可以自己设置守护线程,例如将一个用户线程设置为守护线程的方法就是在调用start()方法启动线程之前调用对象的setDaemon(true)方法,,否则会抛出IllegalThreadStateException异常。若将以上括号里的参数设置为false,则表示的是用户进程模式。
需要注意的是,当在一个守护线程中产生了其它线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此。
线程B怎么知道线程A修改了变量
volatile 修饰变量
synchronized 修饰修改变量的方法
wait/notify
while 轮询
sleep方法和wait方法有什么区别?如何让正在运行的线程暂停一段时间?
两者都可以暂停线程的执行。
Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
最主要是sleep方法没有释放对象锁,而wait方法释放了对象锁,使得其他线程可以使用同步控制块或者方法。
sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
特性:Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
为什么wait方法和notify/notifyAll()方法要在同步块中被调用
调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。
notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)
多线程同步有哪几种方法?
同步指的是在一定的时间内只允许某一个线程访问某个资源
同步方法
同步代码块
使用重入锁实现线程同步(ReentrantLock)
使用特殊域变量(volatile)实现同步(每次重新计算,安全但并非一致)
使用局部变量实现线程同步(ThreadLocal)以空间换时间
使用原子变量实现线程同步(AtomicInteger(乐观锁))
使用阻塞队列实现线程同步(BlockingQueue (常用)add(),offer(),put()
java线程的调度策略
在Java多线程环境中,为保证所有线程的执行能按照一定的规则执行
,JVM实现了一个线程调度器
,它定义了线程调度的策略
,对于CPU运算的分配都进行了规定,按照这些特定的机制为多个线程分配CPU的使用权
。
一般线程调度模式分为两种——抢占式调度
和协同式调度
。
- 抢占式调度指的是每条线程执行的时间、线程的切换
都由系统控制
,系统控制指的是在系统某种运行机制下,每条线程可能分同样的执行时间片
,也可能是某些线程执行的时间片较长
,甚至某些线程得不到执行的时间片
。在这种机制下,一个线程的堵塞不会导致整个进程堵塞
。 - 协同式调度指某一线程
执行完后主动通知系统切换到另一线程上执行
,这种模式就像接力赛一样
,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制
,线程切换可以预知
,不存在多线程同步问题
,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直**堵塞**
,那么可能导致整个系统崩溃。
考虑Java使用的是哪种线程调度模式。此问题的讨论涉及到JVM的实现
,JVM规范中规定每个线程都有优先级
,且优先级越高越优先
执行,但优先级高并不代表能独自占用执行时间片
,可能是优先级高得到越多的执行时间片
,反之,优先级低的分到的执行时间少但不会分配不到执行时间
。JVM的规范没有严格地给调度策略定义,我想正是因为面对众多不同调度策略,JVM要封装所有细节提供一个统一的策略不太现实,于是给了一个不严谨但足够统一的定义。
Java使用的线程调度是抢占式调度,在JVM中体现为让可运行池中优先级高的线程拥有CPU使用权,如果可运行池中线程优先级一样则随机选择线程,但要注意的是实际上一个绝对时间点只有一个线程在运行(这里是相对于一个CPU来说,如果你的机器是多核的还是可能多个线程同时运行的),直到此线程进入非可运行状态或另一个具有更高优先级的线程进入可运行线程池,才会使之让出CPU的使用权,更高优先级的线程抢占了优先级低的线程的CPU。
Java中线程会按优先级分配CPU时间片运行,那么线程什么时候放弃CPU的使用权?可以归类成三种情况:
- 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。
- 当前运行线程结束,即运行完run()方法里面的任务。
线程优先级
Java把线程优先级分成10个级别,线程被创建时如果没有明确声明则使用默认优先级,JVM将根据每个线程的优先级分配执行时间的概率。有三个常量Thread.MIN_PRIORITY、Thread.NORM_PRIORITY、Thread.MAX_PRIORITY分别表示最小优先级值(1)、默认优先级值(5)、最大优先级值(10)。
由于JVM的实现以宿主操作系统为基础,所以Java优先级值与各种不同操作系统的原生线程优先级必然存在某种映射关系,这样才足以封装所有操作系统的优先级提供统一优先级语义。例如1-10优先级值在linux可能要与0-99优先级值进行映射,而windows系统则有7个优先级要映射。
线程的调度策略决定上层多线程运行机制,JVM的线程调度器实现了抢占式调度,每条线程执行的时间由它分配管理,它将按照线程优先级的建议对线程执行的时间进行分配,优先级越高,可能得到CPU的时间则越长。
怎么唤醒一个阻塞的线程
suspend与resume
Java废弃 suspend() 去挂起线程的原因,是因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行 resume() 方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。
但是,如果 resume() 操作出现在 suspend() 之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。而且,对于被挂起的线程,它的线程状态居然还是 Runnable。
wait与notify
wait与notify必须配合synchronized使用,因为调用之前必须持有锁,wait会立即释放锁,notify则是同步块执行完了才释放
await与singal
Condition类提供,而Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使用Lock锁后无法使用wait方法
park与unpark
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程任意位置让线程阻塞。和Thread.suspenf()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出IException异常。可以唤醒指定线程。
区别
wait与notify必须配合synchronized使用,因为调用之前必须持有锁,wait会立即释放锁,notify则是同步块执行完了才释放
因为Lock没有使用synchronized机制,故无法使用wait方法区操作多线程,所以使用了Condition的await来操作
Lock实现主要是基于AQS,而AQS实现则是基于LockSupport,所以说LockSupport更底层,所以使用park效率会高一些
唤醒
如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;
如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
什么是Java线程转储(Thread Dump ),Java中如何获取到线程dump文件(线程堆栈)
线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用。有很多方法可以获取线程转储——使用Profiler,Kill -3命令,jstack工具等等。
获取
linux: 获取pid;jstack pid >tmp.txt
Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。
不可变对象对多线程有什么帮助
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
什么是多线程的上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
如果你提交任务时, 线程池队列已满,这会发生什么
如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
Java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
什么是线程调度器(Thread Scheduler)和间分片(Time Slicing)?
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。 一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是 更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。
单例模式的线程安全性
首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全
Semaphore有什么作用
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
线程类的构造方法、静态块是被哪个线程用的
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
同步方法和同步块,哪个是更好的选择?
同步的范围越小越好。
但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁—>解锁的次数,有效地提升了代码执行的效率。
你如何确保main()方法所在的线程是Java 程序最后结束的线程?
可以使用Thread类的joint()方法来确保所有程序创建的线程在main()方法退出前结束。
线程之间是如何通信的?
当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。Object类中wait()\notify()\notifyAll()方法可以用于线程间通信关于资源的锁的状态。
为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object 类里?
Java的每个对象中都有一个锁(monitor,也可以称为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。
为什么wait(), notify()和notifyAll ()必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
为什么Thread类的sleep()和yield ()方法是静态的?
Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
如何确保线程安全?
同步,
使用原子类(atomic concurrent classes),
实现并发锁,
使用volatile关键字,
使用不变类和线程安全类。
Thread.interrupt() 方法的工作原理是什么?
Java并发
创建线程的有哪些方式?
- 继承Thead类创建线程
- 继承Thread类并重写run方法
- 创建线程对象
- 调用该线程对象的start()方法来启动线程 ``` public class CreateThreadTest { public static void main(String[] args) { new ThreadTest().start(); new ThreadTest().start(); } }
class ThreadTest extends Thread{ private int i = 0;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " is running: " + i);
}
}
}
2. 实现Runnable接口创建线程
- 定义一个类实现Runnable接口,并重写该接口的run()方法
- 创建 Runnable实现类的对象,作为创建Thread对象的target参数,此Thread对象才是真正的线程对象
- 调用线程对象的start()方法来启动线程
public class CreateThreadTest { public static void main(String[] args) { RunnableTest runnableTest = new RunnableTest(); new Thread(runnableTest, “线程1”).start(); new Thread(runnableTest, “线程2”).start(); } }
class RunnableTest implements Runnable{ private int i = 0;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " is running: " + i);
}
}
}
3. 使用Callable和Future创建线程<br />和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大:call()方法可以有返回值,可以声明抛出异常。 <br />**背景**:Java5提供了Future接口来接收Callable接口中call()方法的返回值。Callable接口是 Java5 新增的接口,不是Runnable接口的子接口,所以Callable对象不能直接作为Thread对象的target。<br />针对这个问题,引入了RunnableFuture接口,RunnableFuture接口是Runnable接口和Future接口的子接口,可以作为Thread对象的target 。<br />同时,Java5提供了一个RunnableFuture接口的实现类:FutureTask ,FutureTask可以作为 Thread对象的target。
public interface Callable
- 定义一个类实现Callable接口,并重写call()方法,该call()方法将作为线程执行体,并且有返回值
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象
- 使用FutureTask对象作为Thread对象的target创建并启动线程
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;
public class CreateThreadTest {
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
FutureTask
class CallableTest implements Callable{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < 101; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " is running: " + sum);
return sum;
}
}
4. 应用程序可以使用Executor框架来创建线程池。Executor框架是juc里提供的线程池的实现。
<a name="d2f12b0a"></a>
## 创建线程的三种方式的对比?
<a name="72b7a9c6"></a>
### 1.实现Runnable/Callable接口相比继承Thread类的优势
1. 适合多个线程进行资源共享
1. 可以**避免java中单继承的限制**,Java接口功能
1. 增加程序的健壮性,代码和数据独立
1. 线程池只能放入Runable或Callable接口实现类,不能直接放入继承Thread的类
<a name="9a491f10"></a>
### 2.Callable和Runnable的区别
1. Callable重写的是call()方法,Runnable重写的方法是run()方法
1. call()方法执行后可以**有返回值**,run()方法没有返回值
1. call()方法**可以抛出异常**,run()方法不可以
1. **运行Callable任务可以拿到一个Future对象,表示异步计算的结果** 。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果
**你更喜欢哪种方式?为什么?**
**实现Runnable接口这种方式更受欢迎**(可以从任务角度理解,可读性更好),因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
<a name="9f78a7f6"></a>
## Java线程具有五中基本状态
<a name="9f78a7f6-1"></a>
### Java线程具有五中基本状态
1. 新建状态(New):
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2. 就绪状态(Runnable):
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3. 运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4. 阻塞状态(Blocked):
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
**等待阻塞:**运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
**同步阻塞 :** 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
**其他阻塞 :** 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
<a name="53a611e3"></a>
### Java多线程的就绪、运行和死亡状态
就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
运行状态转换为死亡状态:当此线程执行体执行完毕或发生了异常。
此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。
<a name="f29e23b1"></a>
## 可以直接调用Thread类的run ()方法么?
当然可以,但是如果我们调用了Thread的run()方法,它的行为就会和普通的方法一样,为了在新的线程中执行我们的代码,必须使用Thread.start()方法。
<a name="65ed7fc1"></a>
## 执行 execute() 方法和 submit() 方法的区别是什么呢?
**1) execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
**2) submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断**任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit) 方法则会**阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
<a name="b0efd9c1"></a>
## 什么是线程池?有哪几种创建方式?
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。
池化思想在计算机的应用也比较广泛,比如以下这些:
- 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
- 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
- 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。
线程池的优势主要体现在以下 4 点:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
<a name="dcdfa615"></a>
### 线程池使用
[线程池使用](https://blog.csdn.net/lanzhupi/article/details/113283942)
总体来说可分为 2 类:
- 一类是通过 ThreadPoolExecutor 创建的线程池;
- 另一个类是通过 Executors 创建的线程池。
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过ThreadPoolExecutor 创建的):
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
public static void fixedThreadPool() { // 创建 2 个数据级的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
}
};
// 线程池执行任务(一次添加 4 个任务)
// 执行任务的方法有两种:submit 和 execute
threadPool.submit(runnable); // 执行方式 1:submit
threadPool.execute(runnable); // 执行方式 2:execute
threadPool.execute(runnable);
threadPool.execute(runnable);
}
public static void fixedThreadPool() { // 创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(2); // 执行任务 threadPool.execute(() -> { System.out.println(“任务被执行,线程:” + Thread.currentThread().getName()); }); }
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
public static void cachedThreadPool() { // 创建线程池 ExecutorService threadPool = Executors.newCachedThreadPool(); // 执行任务 for (int i = 0; i < 10; i++) { threadPool.execute(() -> { System.out.println(“任务被执行,线程:” + Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }); } }
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
public static void singleThreadExecutor() { // 创建线程池 ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 执行任务 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + “:任务被执行”); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }); } }
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
public static void scheduledThreadPool() { // 创建线程池 ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5); // 添加定时执行任务(1s 后执行) System.out.println(“添加任务,时间:” + new Date()); threadPool.schedule(() -> { System.out.println(“任务被执行,时间:” + new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }, 1, TimeUnit.SECONDS); }
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
public static void SingleThreadScheduledExecutor() { // 创建线程池 ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor(); // 添加定时执行任务(2s 后执行) System.out.println(“添加任务,时间:” + new Date()); threadPool.schedule(() -> { System.out.println(“任务被执行,时间:” + new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }, 2, TimeUnit.SECONDS); }
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
public static void workStealingPool() { // 创建线程池 ExecutorService threadPool = Executors.newWorkStealingPool(); // 执行任务 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + “ 被执行,线程名:” + Thread.currentThread().getName()); }); } // 确保任务执行完成 while (!threadPool.isTerminated()) { } }
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。(**推荐使用**)
public static void myThreadPoolExecutor() { // 创建线程池 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10)); // 执行任务 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + “ 被执行,线程名:” + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
<a name="088a74e1"></a>
### 构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue
- 参数 1:corePoolSize<br />核心线程数,线程池中始终存活的线程数。
- 参数 2:maximumPoolSize<br />最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
- 参数 3:keepAliveTime<br />最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
- 参数 4:unit:<br />单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
- TimeUnit.DAYS:天
- TimeUnit.HOURS:小时
- TimeUnit.MINUTES:分
- TimeUnit.SECONDS:秒
- TimeUnit.MILLISECONDS:毫秒
- TimeUnit.MICROSECONDS:微妙
- TimeUnit.NANOSECONDS:纳秒
- 参数 5:workQueue<br />一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:**一个由链表结构组成的有界阻塞队列**。
- SynchronousQueue:**一个不存储元素的阻塞队列,即直接提交给线程不保持它们**。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 SynchronousQueue,线程池的排队策略与 BlockingQueue有关。
- 参数 6:threadFactory<br />线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
- 参数 7:handler<br />拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
- AbortPolicy:拒绝并抛出异常。(**默认**)
- CallerRunsPolicy:使用当前调用的线程来执行此任务。
- DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
- DiscardPolicy:忽略并抛弃当前任务。
<a name="30374b85"></a>
## 线程池执行流程
<a name="9da16e7d"></a>
## 线程池的优点?
- **提高线程的可管理性**:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。
- **降低资源消耗**:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- **提高响应速度**:当任务到达时,可以不需要等待线程创建就能立即执行。
<a name="99328a31"></a>
## 什么是Java Timer 类?如何创建一个有特定时间间隔的任务?
java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。
java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行。
<a name="c472419a"></a>
## ThreadLocal是什么?有什么用?
[ThreadLocal](https://www.jianshu.com/p/6fc3bba12f38)
<a name="71846ee9"></a>
### ThreadLocal 是什么?
在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
**ThreadLocal并不是一个Thread,而是Thread的局部变量**,也许把它命名为ThreadLocalVariable更容易让人理解一些。
在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
<a name="2025d24c"></a>
### ThreadLocal 的作用?
ThreadLocal是解决线程安全问题一个很好的思路,它通过**为每个线程提供一个独立的变量副本**解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
<a name="176a377a"></a>
### ThreadLocal的应用场景?
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。在下面会例举几个场景。
<a name="4be8d617"></a>
### ThreadLocal的内部原理
从源码中了解ThreadLocal的原理
- Returns the value in the current thread’s copy of this
- thread-local variable. If the variable has no value for the
- current thread, it is first initialized to the value returned
- by an invocation of the {@link #initialValue} method. *
- @return the current thread’s value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings(“unchecked”) T result = (T)e.value; return result; } } return setInitialValue(); }
/**
- Get the map associated with a ThreadLocal. Overridden in
- InheritableThreadLocal. *
- @param t the current thread
- @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
/**
- Variant of set() to establish initialValue. Used instead
- of set() in case user has overridden the set() method. *
- @return the initial value */ private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } ```
取得当前线程,
然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。在getMap中,是调用当前线程t,返回当前线程t中的一个成员变量threadLocals。实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类
然后接着下面获取到
如果使用ThreadLocal时,先进行get之前,必须先set,否则会报空指针异常
public class ThreadLocalExsample {
ThreadLocal<Long> longLocal = new ThreadLocal<>();
public void set() {
longLocal.set(Thread.currentThread().getId());
}
public long getLong() {
return longLocal.get();
}
public static void main(String[] args) {
ThreadLocalExsample test = new ThreadLocalExsample();
//注意:没有set之前,直接get,报null异常了
System.out.println("-------threadLocal value-------" + test.getLong());
}
}
ThreadLocal的应用场景# 数据库连接
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
ThreadLocal的应用场景# Session管理
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
ThreadLocal的应用场景# 多线程
* @Author 安仔夏天很勤奋
* Create Date is 2019/3/21
*
* 描述 Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。
* 因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,
* 它们也无法访问到对方的ThreadLocal变量。
*/
public class ThreadLocalExsample {
/**
* 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,
* 并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,
* 则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,
* 因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
*/
public static class MyRunnable implements Runnable {
/**
* 例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。
* 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的
* set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,
* 他们仍然无法访问到对方的值。
*/
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
//一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
//可以通过下面方法读取保存在ThreadLocal变量中的值
System.out.println("-------threadLocal value-------"+threadLocal.get());
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
运行结果
-------threadLocal value-------38
-------threadLocal value-------88
ThreadLocal 中 set 和 get 操作的都是对应线程的 table数组,因此在不同的线程中访问同一个 ThreadLocal 对象的 set 和 get 进行存取数据是不会相互干扰的。
Java Concurrency API中的Lock接口是什么
Java Concurrency API中有哪些原子操作类?
在包java.util.concurrent.atomic
下面的一Atomic开头的类都是原子类。例如AtomicInteger
,AtomicReference
等
Lock接口是什么
Lock是一个控制多线程访问共享资源的工具类,比使用synchronized方法或者语句有了更加扩展性的操作,结构灵活,可以有完全不同的属性,也可以支持多个相关类的条件对象。
相对于同步原语(synchronization
)有什么优势?
- 可以使锁更公平
- 可以使线程在等待锁的时候响应中断
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
- 可以在不同的作用域,以不同的顺序获取和释放锁
Executors类是什么?
Executor框架标准化了线程的调用,调度,执行以及异步任务的控制。
无节制的线程创建会引起内存溢出,创建有限线程数的线程池(ThreadPool)是一个好的选择,线程可以预先创建,回收再利用。
什么是并发集合类?
Java集合类都是快速失效的,这就意味着当集合被改变且一个线程在使用迭代器遍历集合的时候,迭代器的next()方法将抛出ConcurrentModificationException异常。
并发容器支持并发的遍历和并发的更新。主要的类有ConcurrentHashMap, CopyOnWriteArrayList 和CopyOnWriteArraySet
锁的分类
悲观锁
悲观锁,每次去请求数据的时候,都认为数据会被抢占更新(悲观的想法);所以每次操作数据时都要先加上锁,其他线程修改数据时就要等待获取锁。适用于写多读少的场景,synchronized就是一种悲观锁
乐观锁
在请求数据时,觉得无人抢占修改。等真正更新数据时,才判断此期间别人有没有修改过(预先读出一个版本号或者更新时间戳,更新时判断是否变化,没变则期间无人修改);和悲观锁不同的是,期间数据允许其他线程修改
自旋锁
一句话,魔力转转圈。当尝试给资源加锁却被其他线程先锁定时,不是阻塞等待而是循环再次加锁
在锁常被短暂持有的场景下,线程阻塞挂起导致CPU上下文频繁切换,这可用自旋锁解决;但自旋期间它占用CPU空转,因此不适用长时间持有锁的场景
乐观锁常见的两种实现方式
版本号机制
一般是在数据表中加上版本号字段 version
,表示数据被修改的次数。当数据被修改时,这个字段值会加1。
举个简单的例子:假设帐户信息表中有一个 version 字段,当前值为 1 ,而当前帐户的余额( balance )为 100 。
- 操作员 A 此时准备将其读出( version=1 ),并从其帐户余额中扣除 50( 100-50 );
- 操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 20 ( 100-20 );
- 操作员 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50 ),提交到数据库完成更新;
- 操作员 B 完成了操作,也将版本号加1( version=2 )试图向数据库提交数据( balance=80 ),但此时比对数据库记录版本发现,操作员 B 提交的数据版本号为 2 ,数据库记录的当前版本也为 2 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。
因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改,最终造成覆盖操作员 A 操作结果的可能。
CAS 算法
即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数:
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个 native 原子操作)。一般情况下,这是一个自旋操作,即不断的重试。
synchronized的作用?
简介
synchronized块是Java提供的一种原子性内置锁,JVM提供
- Java中的每个对象都可以隐式地扮演一个用于同步的锁的角色。这些Java内置的使用者看不到的锁被称为内部锁(intrinsic locks)),也叫作监视器锁(monitor locks))。换句话说:如果一个对象对多个线程可见,则该对象变量的所有读取或写入都是通过同步方法完成的.
- 线程在进入synchronized代码块会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用该内置锁资源的wait系列方法时释放该锁。
- 内部锁在Java中扮演了互斥锁(mutual exclusion lock,也称作mutex)的角色,意味着至多只有一个线程可以拥有锁,除了用于线程同步、确保线程安全外,关键字synchronized还可以保证线程间的可见性和有序性。
synchronized 使用的三种方式
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
Synchronized的两种锁目标
对象
包含方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己制定锁对象)
代码块形式:手动制定锁对象,在代码块上加上synchronized关键字
方法锁的形式:使用synchronized关键字去修饰普通方法,锁对象默认this
this锁对象只有一把锁,需要前面的线程释放第二个线程才能拿到所进入方法它是串行,自己制定锁对象可以并行
类
指synchronized修饰静态方法或制定锁对象为Class对象
- 概念:Java类可能有很多个对象,但只有一个Class对象,所以不管是从哪个实例过来的它都只有一把锁,类锁是一个概念上的东西并不是说它真实存在,它这个概念是用来帮我们理解实例方法和静态方法的区别的.所谓的类锁,不过是Class对象的锁而已,因为class对象只有一个
类锁的效果是只能在同一时刻被一个对象拥有,即使是不同的Runnable实例所对应的类所依然只有一个,这点跟对象锁是不同的,对象锁如果是不同的实例创建出来互相是不影响的,它们可以同时运行,但是只要用了类锁就只有一个能运行了.
- 形式1:synchronized加在static方法上
- 形式2:synchronized加在(*.class)代码块上
synchronized底层原理
修改代码块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
synchronized 同步语句块经过Javac编译后,会在同步块的前后分别形成 monitorenter 和 monitorexit 指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
根据《Java虚拟机规范》的要求,在执行 monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit 指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
修饰方法
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
JDK1.6之后的锁优化
在 JDK 1.5 时,synchronized 需要调用监视器锁(Monitor)来实现,监视器锁本质上又是依赖于底层的操作系统的 Mutex Lock(互斥锁)实现的,互斥锁在进行释放和获取的时候,需要从用户态转换到内核态,这样就造成了很高的成本,也需要较长的执行时间,这种依赖于操作系统 Mutex Lock 实现的锁我们称之为“重量级锁”。
在主流Java虚拟机实现中,Java的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,进行这种状态转换需要很多的处理器时间。尤其是对于代码特别简单的同步块(譬如被 synchronized修饰的getter()或setter()方法),状态转消耗的时间甚至会比用户代码本身执行的时间还要长。
JDK5升级到JDK6,HotPot虚拟机开发团队在这个版本花了大量资源去实现各种锁优化技术,如适应性自旋锁(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了提升锁的效率。
头信息
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据的长度在32位和64位的Java虚拟机中分别占用32个或64个比特,官方称它为“Mark Word”。这部分是实现偏向锁和轻量级锁的关键。
Mark Word,它被设计成一个非固定(比特位值变动)的动态数据结构,以尽可能的减少占用空间。锁的状态不同,存储的内容也不同。图例看JVM篇
当锁对象第一次被线程获取的时候,虚拟机会把对象头的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式,同时使用CAS操作把获得这个锁的线程的ID记录在对象的Mark Word中。如果CAS操作成功,持有偏向锁的线程以后在此进入这个锁的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁以及对Mark Word的更新操作等。)
一旦有另外一个线程来尝试获取这个锁,偏向模式马上就会结束。根据锁对象目前是否处于被锁定状态决定是否撤销偏向(偏向模式设置为“0”)。撤销后标志位恢复到未锁定(标志位为“01”或轻量级锁定(标志位为“00”)的状态)。
- 第二部分用于存储指向方法区类的类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。
偏向锁
偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
偏向锁的核心思想是如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。
因此,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次极有可能是同一个线程请求相同的锁。
而对于锁竞争比较激烈的场合,其效果不佳,因为这时偏向模式会失效。
使用Java虚拟机参数-XX:+UseBiasedLocking可以开启偏向锁。
轻量级锁
如果偏向锁失败,虚拟机也不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。
区别于重量级锁使用操作系统互斥量来实现,轻量级锁的加锁和解锁都使用CAS来完成。
加锁:线程在执行同步块之前,JVM会先在当前线程的桢栈中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
如果成功,当前线程获得锁,将对象Mark Word的锁标志位转变为“00”,表示此对象处于轻量级锁状态。
如果失败,表示其他线程抢先争夺到了锁,轻量级锁就不再有效,膨胀为重量级锁,并尝试使用自旋来获取锁。
解锁:轻量级锁解锁时,会使用CAS操作将Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁正在竞争,锁就会膨胀成重量级锁。
轻量级锁能提升性能的依据是“对于绝大部分锁,在整个同步周期内都不存在竞争”这一经验法则。
如果没有竞争,轻量级锁便通过CAS操作避免了使用互斥量的开销;
但如果存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而比传统的重量级锁更慢。
自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
挂起和恢复线程的操作都需要转入内核态中完成,如果加锁失败便简单粗暴的挂起线程可能得不偿失。因为虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间。为了这段时间去挂起和恢复线程并不值得。
这时,虚拟机会让没请求到锁的线程做几个空循环(自旋),这时线程并不会放弃处理器的执行时间,,在经过若干次循环后,如果可以得到锁,那便进入同步块。
JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改—XX:PreBlockSpin来更改。
另外,在 JDK1.6中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。
另外:如果在同一个锁对象上,自旋等待刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也很可能再次成功,进而允许自旋等待更长的时间。另一方面,如果对于某个锁,自旋很少成功获得锁,那以后要获取这个锁时可能直接省略掉自旋过程。
琐膨胀
synchronized 的状态总共有以下 4 种,也是膨胀顺序
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
HotSpot虚拟机的对象
锁消除
锁消除是一更加彻底的优化,指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
锁消除的主要判定依据是逃逸分析,就是观察某一个变量是否会逃出某一个作用域,如果判断到一段代码中,在堆上所有数据都不会逃逸出去被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然不用进行。
局部变量是在线程栈上分配的,属于线程私有的数据,不会被其他线程访问到。在这种情况下,内部所有加锁同步都是没有必要的,这时虚拟机会将这些无用的锁操作去除。
逃逸分析必须在-server模式下进行,可以使用-XX:+DoEscapeAnalysis参数打开逃逸分析。使用-XX:+EliminateLocks参数可以打开锁消除。
锁粗化
原则上,在编写代码的时候,总是推荐将同步块的作用范围限制得尽可能小,这样可以让同步操作所需的时间尽可能少,即使存在锁竞争,等待锁的线程也能尽可能块地拿到锁。(自旋)
但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那会导致不必要的性能损耗。如果虚拟机探测到这样的操作,就会把加锁同步的返回扩展(粗化)到整个操作序列的外部,这样只需要加锁一次就可以了。
锁膨胀
Synchronized的作用
能够保证同一时刻最多只有一个线程执行该段代码,以保证并发安全的效果
Synchronized的地位
- Synchronized是Java的关键字,被Java语言原生支持
- 是最基本的互斥同步手段
- 是并发编程中的元老,必学
volatile关键字的作用
背景
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。
由于程序运行过程中的数据是存放在主存(物理内存)当中的,
由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。从线程的角度理解,线程保留一套共享变量的副本(ThreadLocal)
缓存一致性问题
i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
在多线程中运行就会有问题,每个线程运行时有自己的高速缓存,同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。
为了解决缓存不一致性问题,通常来说有以下2种硬件层面解决方法:
- 通过在总线加LOCK锁的方式—多核
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
在锁住总线期间,其他CPU无法访问内存,导致效率低下。 - 通过缓存一致性协议
出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执
volatile关键字的作用
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
- 使用volatile关键字会强制将修改的值立即写入主存;
- 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
- 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
- volatile关键字能保证可见性没有错,可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
- volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
示例
假如某个时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;(注意设置缓存无效 和 从内存读取数据是两个概念)
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc只增加了1。
原子类
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
什么是CAS
背景
在JDK5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁,锁机制存在以下问题:
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险
volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一个悲观锁,synchronized就是一种独占锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。
而另一种更加有效的锁就是乐观锁,CAS就是一种乐观锁
在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量支持并发操作的原语(原语是由若干条指令组成的,用于完成一定功能的一个过程,具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断),从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。
在java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统性能的。而随着java不断的发展,java本地方法(JNI)的出现,为java程序越过jvm直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的concurrent包中,CAS理论是他实现整个java并发包的基石。
什么是CAS
CAS操作包含三个操作数—— 内存位置的值(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
CAS是一种有名的无锁算法。无锁编程,即不适用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步。
- CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术
- CAS是原子操作,保证并发安全,而不能保证并发同
- CAS是CPU的一个指令(需要JNI调用Native方法,才能调用CPU的指令
- CAS是非阻塞的、轻量级的乐观锁
为什么说CAS是乐观锁
乐观锁,严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,所以CAS不会保证线程同步。乐观的认为在数据更新期间没有其他线程影响。
CAS原理
CAS(Compare And Swap)就是将内存值更新为需要的值,但是有个条件,内存值必须与期望值相同。举个例子,内存值V、期望值A、更新值B,当V == A的时候将V更新为B。
CAS应用
由于CAS是CPU指令,我们只能通过JNI与操作系统交互,关于CAS的方法都在sun.misc包下Unsafe的类里,java.util.concurrent.atomic包下的原子类等通过CAS来实现原子操作。
乐观锁、悲观锁选择
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,
像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的吞吐量。
但如果是多写的情况,一般会经常发生冲突,这就会导致CAS算法会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
优缺点
优点
非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
问题
ABA问题: 线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。
自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源
synchronized、volatile、CAS、Lock比较
实现并发的基础
原子性、可见性、有序性
并发性比较
场景
CAS:单个变量支持比较替换操作,如果实际值与期望值一致时才进行修改
volatile:单个变量并发操作,直接修改为我们的目标值
synchronized:一般性代码级别的并发
Lock:代码级别的并发,需要使用锁实现提供的独特机制,例如:读写分离、分段、中断、共享、重入等 synchronized 不支持的机制。
原子性
CAS:保证原子性
volatile:单个操作保证原子性,组合操作(例如:++)不保证原子性
synchronized:保证原子性
Lock:保证原子性
并发粒度
CAS:单个变量值
volatile:单个变量值
synchronized:静态、非静态方法、代码块
Lock:代码块
编码操作性
CAS:调用 JDK 方法
volatile:使用关键字,系统通过屏障指令保证并发性
synchronized:使用关键字,加锁解锁操作系统默认通过指令控制
Lock:手动加锁解锁
线程阻塞
CAS:不会
volatile:不会
synchronized:可能会
Lock:可能会
性能(在合理使用情况下比较,比如我们可以用 volatile 实现的需求即不用 Lock)
CAS:主要表现在 CPU 资源占用
volatile:性能较好
synchronized:性能一般(JDK 1.6 优化后增加了偏向锁、轻量级锁机制)
Lock:性能较差
锁比较
锁重入
synchronized:支持
Lock:ReentrantLock 实现类支持
锁中断操作
synchronized:不支持中断操作
Lock:支持中断,支持超时中断
锁功能性
synchronized:独占锁、可重入锁
Lock:独占锁、共享锁、可重入锁、读写锁、分段锁 …
锁状态感知
synchronized:无法判断是否拿到锁
Lock:可以判断是否拿到锁
死锁
synchronized:可能出现死锁
Lock:需合理编码,可能出现死锁
synchronized和ReentrantLock的区别
底层实现
synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,
ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,
ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
是否可手动释放
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用;
ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private AtomicInteger atomicInteger;
public void increment() throws Exception {
lock.lock();
try {
while (number != 0) {
condition.await();
}
//do something
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
是否可中断
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;
ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
是否公平锁
synchronized为非公平锁
ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
锁是否可绑定条件Condition
synchronized不能绑定,通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。;
ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒
// 示例:用ReentrantLock绑定三个条件实现线程A打印一次1,线程B打印两次2,线程C打印三次3
class Resource {
private int number = 1;//A:1 B:2 C:3
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//1 判断
public void print1() {
lock.lock();
try {
//判断
while (number != 1) {
c1.await();
}
//2 do sth
for (int i = 1; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 2;
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//1 判断
public void print2() {
lock.lock();
try {
//判断
while (number != 2) {
c2.await();
}
//2 do sth
for (int i = 1; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 3;
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//1 判断
public void print3() {
lock.lock();
try {
//判断
while (number != 3) {
c3.await();
}
//2 do sth
for (int i = 1; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print1();
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print2();
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print3();
}
},"C").start();
}
// 结果
A 1 B 2 B 2 C 3 C 3 C 3 A 1 B 2 B 2 C 3 C 3 C 3
锁的对象
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;
ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
什么是Future?
为什么出现Future机制
从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
Future模式的核心思想是能够让主线程将原来需要同步等待的这段时间用来做其他的事情。(因为可以异步获得执行结果,所以不用一直同步等待去获得执行结果)
Future的相关类图
Future 接口
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
- 我们可以取消这个执行逻辑,如果这个逻辑已经正在执行,提供可选的参数来控制是否取消已经正在执行的逻辑。
- 我们可以判断执行逻辑是否已经被取消。
- 我们可以判断执行逻辑是否已经执行完成。
- 我们可以获取执行逻辑的执行结果。
- 我们可以允许在一定时间内去等待获取执行结果,如果超过这个时间,抛
TimeoutException
。
FutureTask 类
FutureTask
既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
如何使用Future机制
举个例子,假设我们要执行一个算法,算法需要两个输入 input1
和 input2
, 但是input1
和input2
需要经过一个非常耗时的运算才能得出。由于算法必须要两个输入都存在,才能给出输出,所以我们必须等待两个输入的产生。接下来就模仿一下这个过程。
package src;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureTaskTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
long starttime = System.currentTimeMillis();
//input2生成, 需要耗费3秒
FutureTask<Integer> input2_futuretask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
return 5;
}
});
new Thread(input2_futuretask).start();
//input1生成,需要耗费2秒
FutureTask<Integer> input1_futuretask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(2000);
return 3;
}
});
new Thread(input1_futuretask).start();
Integer integer2 = input2_futuretask.get();
Integer integer1 = input1_futuretask.get();
System.out.println(algorithm(integer1, integer2));
long endtime = System.currentTimeMillis();
System.out.println("用时:" + String.valueOf(endtime - starttime));
}
//这是我们要执行的算法
public static int algorithm(int input, int input2) {
return input + input2;
}
}
// 执行结果
8
用时:3001毫秒
什么是AQS
框架
AQS(AbstractQueuedSynchronizer),提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
底层实现的数据结构
维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
队列
Sync queue:同步队列,是一个双向链表。包括head节点和tail节点。head节点主要用作后续的调度。
Condition queue:非必须,单向链表。当程序中存在cindition的时候才会存在此列表。
使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架。
同步状态
private volatile int state;
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
在独占锁的时代这个值通常是0或者1(如果是重入的就是重入的次数),在共享锁的时代就是持有锁的数量。
AQS核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个int成员变量来表示同步状态。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
继承:子类通过继承并通过实现它的方法管理其状态(acquire和release方法操纵状态)。
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗?
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是:使用者继承AbstractQueuedSynchronizer并重写指定的方法。
将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
自定义同步器
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
示例
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。**当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。**
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会**unpark()主调用线程**,然后主调用线程就会从await()函数返回,继续后余动作。
为什么说LockSupport是Java并发的基石?
Java并发组件和并发工具类如下:(并发组件和并发工具大都是基于AQS来实现的)
- 并发组件:线程池、阻塞队列、Future和FutureTask、Lock和Condition。
- 并发工具:CountDownLatch、CyclicBarrier、Semaphore和Exchanger。
AQS中的控制线程又是通过LockSupport类来实现的,因此可以说,LockSupport是Java并发基础组件中的基础组件。
LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。
LockSupport工具实现原理
继承关系
类变量
构造函数
常用方法
public static void park(Object blocker) {
Thread t = Thread.currentThread();
// blocker在什么对象上进行的阻塞操作
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
// 超时阻塞
UNSAFE.park(false, nanos);
setBlocker(t, null);
}
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
park和unpark底层是借助系统层(C语言)方法pthread_mutex和pthread_cond来实现的,通过pthread_cond_wait函数可以对一个线程进行阻塞操作,在这之前,必须先获取pthread_mutex,通过pthread_cond_signal函数对一个线程进行唤醒操作。
注意,Linux下使用pthread_cond_signal的时候,会产生“惊群”问题的,但是Java中是不会存在这个“惊群”问题的,那么Java是如何处理的呢?
Java只会对一个线程调用pthread_cond_signal操作,这样肯定只会唤醒一个线程,也就不存在所谓的惊群问题。Java在语言层面实现了自己的线程管理机制(阻塞、唤醒、排队等),每个Thread实例都有一个独立的pthread_u和pthread_cond(系统层面的/C语言层面),在Java语言层面上对单个线程进行独立唤醒操作。
常用的并发工具类有哪些?
CountDownLatch(闭锁,我觉得叫门闩更好理解)
适用场景:在多线程执行过程中设置几个门闩,当所有的门闩被打开时,被挡在门外的线程才能继续执行。
import java.util.concurrent.*;
public class Demo {
public static void main(String[] args) throws Exception{
System.out.println(Util.testTime(3, new Runnable() {
@Override
public void run() {
System.out.println("test");
}
}));
}
}
class Util{
/**
* 测试n个线程并发执行某个任务的时间
* @param nThreads 线程数量
* @param task 需要并发执行的任务
* @return
* @throws InterruptedException
*/
public static long testTime(int nThreads,final Runnable task) throws InterruptedException {
//为启动门设置一个门闩,当门闩被打开时,放行所有被挡在门外的线程
final CountDownLatch startGate=new CountDownLatch(1);
//为结束门设置n个门闩,当n个门闩被打开时,放行所有被挡在门外的线程
final CountDownLatch endGate=new CountDownLatch(nThreads);
//测试n个线程并发执行任务task的时间
for(int i=0;i<nThreads;i++){
Thread t=new Thread(){
public void run(){
try {
startGate.await();
task.run();
endGate.countDown();
} catch (InterruptedException e) {
endGate.countDown();
}
}
};
t.start();
}
//循环中的内容使得有n个线程在startGate门外等着执行task任务
long start=System.nanoTime();
startGate.countDown();//打开startGate上的门闩
endGate.await();//等待endGate门开
long end =System.nanoTime();
return end-start;
}
}
CyclicBarrier(栅栏)
使用场景:多个线程彼此等待,当所有的线程都到达指定“地点”(指定代码位置),才开始继续执行。
public class Demo{
public static void main(String [] args) throws InterruptedException {
final CyclicBarrier cyclicBarrier=new CyclicBarrier(10, new Runnable() {
@Override
public void run() {
System.out.println("10个人都到达会议室,开始开会");
}
});
for(int i=0;i<10;i++){
final long tmp=i;
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000*(11-tmp));
System.out.println("person"+tmp+" come here");
try {
cyclicBarrier.await();//等待其他线程到达
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {}
}
});
thread.start();
}
}
}
Semophore(信号量)
使用场景:需要控制访问某个资源或者进行某种操作的线程数量。当达到指定数量时,只能等待其他线程释放信号量。
public class Demo{
private static Semaphore semaphore=new Semaphore(2);
public static void main(String [] args) throws InterruptedException {
ExecutorService executorService=Executors.newFixedThreadPool(4);
for(int i=0;i<4;i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Demo.doSomething();
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+" can't get semaphore.");
}
}
});
}
executorService.shutdown();
while(true){
if(executorService.isTerminated()){
System.out.println("over");
break;
}
}
}
public static void doSomething() throws InterruptedException{
/**
* 每个操作最多两个线程同时进行
*/
System.out.println(Thread.currentThread().getName()+" try to get semaphore.");
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" is doing something.");
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName()+" release semaphore.");
semaphore.release();
}
}
Exchanger(交换器)
使用场景:用于两个线程之间交换数据
public class Demo{
public static void main(String [] args){
Exchanger<String> exchanger=new Exchanger<String>();
String content1="content1";
String content2="content2";
ExecutorService es=Executors.newFixedThreadPool(2);
es.submit(new ExchangeDataTask<String>(exchanger,content1));
es.submit(new ExchangeDataTask<String>(exchanger,content2));
es.shutdown();
}
}
class ExchangeDataTask<V> implements Runnable{
private Exchanger<V> exchanger;
private V data;
public ExchangeDataTask(Exchanger<V> exchanger,V data){
this.exchanger=exchanger;
this.data=data;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"交换之前的数据:" + data);
V newData = exchanger.exchange(data);
System.out.println(Thread.currentThread().getName()+"交换之后的数据:" + newData);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
CyclicBarrie(栅栏)和CountDownLatch(闭锁)的区别
CountDownLatch和CyclicBarrier这两个类常常容易混淆。
CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.
CountDownLatch 是计数器, 线程完成一个就记一个, 就像 报数一样, 只不过是递减的.
而CyclicBarrier更像一个水闸, 线程执行就想水流, 在水闸处都会堵住, 等到水满(线程到齐)了, 才开始泄流.
对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待, 而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。
而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
CyclicBarrier假设有一个场景:每个线程代表一个跑步运动员,当运动员都准备好后,才一起出发,只要有一个人没有准备好,大家都等待.
ReadWriteLock是什么(最后一个工具)
ReadWriteLock描述的是:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作(修改数据)。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
// ReadWriteLock并不是Lock的子接口
只不过ReadWriteLock借助Lock来实现读写两个视角。在ReadWriteLock中每次读取共享数据就需要读取锁,当需要修改共享数据时就需要写入锁。看起来好像是两个锁,但其实不尽然
在JDK 6里面ReadWriteLock的实现是ReentrantReadWriteLock。
ReadWriteLock需要严格区分读写操作,如果读操作使用了写入锁,那么降低读操作的吞吐量,如果写操作使用了读取锁,那么就可能发生数据错误。
另外ReentrantReadWriteLock还有以下几个特性:
- 公平性非公平锁(默认) 这个和独占锁的非公平性一样,由于读线程之间没有锁竞争,所以读操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。因此非公平锁的吞吐量要高于公平锁。公平锁 利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前前提是写线程的等待时间要比所有读线程的等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁。
- 重入性读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。
- 锁降级写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
锁升级:读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。 - 锁获取中断读取锁和写入锁都支持获取锁期间被中断。这个和独占锁一致。
- 条件变量写入锁提供了条件变量(Condition)的支持,这个和独占锁一致,但是读取锁却不允许获取条件变量,将得到一个
UnsupportedOperationException
异常。 - 重入数读取锁和写入锁的数量最大分别只能是65535(包括重入数)。这在下节中有介绍。
实现
事实上在ReentrantReadWriteLock里锁的实现是靠java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的。这个类看起来比较眼熟,实际上它是AQS的一个子类,这中类似的结构在CountDownLatch、ReentrantLock、Semaphore里面都存在
同样它也有两种实现:公平锁和非公平锁,也就是java.util.concurrent.locks.ReentrantReadWriteLock.FairSync和java.util.concurrent.locks.ReentrantReadWriteLock.NonfairSync
在ReentrantReadWriteLock里面的锁主体就是一个Sync,也就是上面提到的FairSync或者NonfairSync,所以说实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样,所以前面才有读写锁是独占锁的两个不同视图一说。
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock( ) {
return sync.tryWriteLock();
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}
显然WriteLock就是一个独占锁,这和ReentrantLock里面的实现几乎相同,都是使用了AQS的acquire/release操作。当然了在内部处理方式上与ReentrantLock还是有一点不同的。
ReadLock获取的是共享锁
ReadWriteLock的读、写锁是相关但是又不一致的,所以需要两个数来描述读锁(共享锁)和写锁(独占锁)的数量。显然现在一个state就不够用了。于是在ReentrantReadWrilteLock里面将这个字段一分为二,高位16位表示共享锁的数量,低位16位表示独占锁的数量(或者重入数量)。2^16-1=65536
说一下ConcurrentHashMap的工作原理,put()和get()的工作流程是怎样的?
说一下ConcurrentHashMap的工作原理,put()和get()的工作流程是怎样的?
继承关系
class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {}
class AbstractMap<K,V> implements Map<K,V> {}
interface ConcurrentMap<K, V> extends Map<K, V> {}
全局变量
private static final long serialVersionUID = 7249069246763182397L;
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int NCPU = Runtime.getRuntime().availableProcessors();
构造函数
ConcurrentHashMap()
ConcurrentHashMap(int initialCapacity)
ConcurrentHashMap(Map<? extends K, ? extends V> m)
ConcurrentHashMap(int initialCapacity, float loadFactor)
ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)
工作原理
存储对象时,将key和vaule传给put()方法:
- 如果没有初始化,就调用initTable()方法对数组进行初始化;
- 如果没有hash冲突则直接通过CAS进行无锁插入;
- 如果需要调用transfer()扩容,就先进行扩容,扩容为原来的两倍;
- 如果存在hash冲突,就通过加锁的方式进行插入,从而保证线程安全。(如果是链表就按照尾插法插入,如果是红黑树就按照红黑树的数据结构进行插入);
- 如果达到链表转红黑树条件,就将链表转为红黑树;
- 如果插入成功就调用addCount()方法进行计数并且检查是否需要扩容;
put()的流程只是比HashMap多了一些保证线程安全的操作而已
注意:在并发情况下ConcurrentHashMap会调用多个工作线程一起帮助扩容,这样效率会更高。
获取对象时,将key传给get()方法:
- 计算hash值,定位table索引位置,如果头节点符合条件则直接返回key对应的value;
- 如果遇到正在扩容,则调用标记正在扩容的节点,查找该节点,匹配就返回;
- 以上条件都不符合,就继续向下遍历;
其实get()的流程跟HashMap基本是一样的。
ConcurrentHashMap是如何保证并发安全的?
JDK7中ConcurrentHashMap是通过ReentrantLock+CAS+分段思想来保证的并发安全的,ConcurrentHashMap的put方法会通过CAS的方式,把一个Segment对象存到Segment数组中,一个Segment内部存在一个HashEntry数组,相当于分段的HashMap,Segment继承了ReentrantLock,每段put开始会加锁。
在JDK7的ConcurrentHashMap中,首先有一个Segment数组,存的是Segment对象,Segment相当于一个小HashMap,Segment内部有一个HashEntry的数组,也有扩容的阈值,同时Segment继承了ReentrantLock类,同时在Segment中还提供了put,get等方法,比如Segment的put方法在一开始就会去加锁,加到锁之后才会把key,value存到Segment中去,然后释放锁。同时在ConcurrentHashMap的put方法中,会通过CAS的方式把一个Segment对象存到Segment数组的某个位置中。同时因为一个Segment内部存在一个HashEntry数组,所以和HashMap对比来看,相当于分段了,每段里面是一个小的HashMap,每段公用一把锁,同时在ConcurrentHashMap的构造方法中是可以设置分段的数量的,叫做并发级别concurrencyLevel.
JDK8中ConcurrentHashMap是通过synchronized+cas来实现了。在JDK8中只有一个数组,就是Node数组,Node就是key,value,hashcode封装出来的对象,和HashMap中的Entry一样,在JDK8中通过对Node数组的某个index位置的元素进行同步,达到该index位置的并发安全。同时内部也利用了CAS对数组的某个位置进行并发安全的赋值。
ConcurrentHashMap是如何扩容的?
JDK7中的ConcurrentHashMap和JDK7的HashMap的扩容是不太一样的,首先JDK7中也是支持多线程扩容的,原因是,JDK7中的ConcurrentHashMap分段了,每一段叫做Segment对象,每个Segment对象相当于一个HashMap,分段之后,对于ConcurrentHashMap而言,能同时支持多个线程进行操作,前提是这些操作的是不同的Segment,而ConcurrentHashMap中的扩容是仅限于本Segment,也就是对应的小型HashMap进行扩容,所以是可以多线程扩容的。
每个Segment内部的扩容逻辑和HashMap中一样。
JDK8中是支持多线程扩容的,JDK8中的ConcurrentHashMap不再是分段,或者可以理解为每个桶为一段,在需要扩容时,首先会生成一个双倍大小的数组,生成完数组后,线程就会开始转移元素,在扩容的过程中,如果有其他线程在put,那么这个put线程会帮助去进行元素的转移,虽然叫转移,但是其实是基于原数组上的Node信息去生成一个新的Node的,也就是原数组上的Node不会消失,因为在扩容的过程中,如果有其他线程在get也是可以的。
JDK8中的ConcurrentHashMap有一个CounterCell,你是如何理解的?
CounterCell是JDK8中用来统计ConcurrentHashMap中所有元素个数中的一部分,在统计ConcurentHashMap时,不能直接对ConcurrentHashMap对象进行加锁然后再去统计,因为这样会影响ConcurrentHashMap的put等操作的效率,在JDK8的实现中使用了CounterCell+baseCount进行统计。
baseCount是ConcurrentHashMap中的一个属性,某个线程在调用ConcurrentHashMap对象的put操作时,会先通过CAS去修改baseCount的值,如果CAS修改成功,就计数成功,
如果CAS修改失败,则会从CounterCell数组中随机选出一个CounterCell对象,然后利用CAS去修改CounterCell对象中的值,
因为存在CounterCell数组,所以,当某个线程想要计数时,先尝试通过CAS去修改baseCount的值,如果没有修改成功,则从CounterCell数组中随机取出来一个CounterCell对象进行CAS计数,这样在计数时提高了效率。
所以ConcurrentHashMap在统计元素个数时,就是baseCount加上所有CountCeller中的value值,所得的和就是所有的元素个数。
get/put操作的原子性
ConcurrentHashMap可以保证单个get/put操作的原子性,但是不能保证两个一起就是原子性。
如何解决? ConcurrentHashMap提供了两个方法
computeIfAbsent
:计算如果不存在。如果key不存在,存入计算结果并返回
computeIfPresent
:计算如果存在。如果key存在,计算公式并返回
Integer a1 = map.computeIfAbsent("a", (key) -> 1+1);
Integer a2 = map.computeIfPresent("a", (key,value) -> map.get(key)+value);
ConcurrentHashMap中的key和value可以为null吗?为什么?
不可以,因为源码中是这样判断的,进行put()操作的时候如果key为null或者value为null,会抛出NullPointerException空指针异常。
如果HashMap中存在一个key对应的value是null,那么当调用map.get(key)的时候,必然会返回null,那么这个null就有两个意思:
- 这个key从来没有在map中映射过,也就是不存在这个key;
- 这个key是真实存在的,只是在设置key的value值的时候,设置为null了;
这个二义性在非线程安全的HashMap中可以通过map.containsKey(key)方法来判断,如果返回true,说明key存在只是对应的value值为空。如果返回false,说明这个key没有在map中映射过。这样是为什么HashMap可以允许键值为null的原因,但是ConcurrentHashMap判断不了二义性的。
为什么ConcurrentHashMap判断不了呢?
此时如果有A、B两个线程,A线程调用ConcurrentHashMap.get(key)方法返回null,但是我们不知道这个null是因为key没有在map中映射还是本身存的value值就是null,此时我们假设有一个key没有在map中映射过,也就是map中不存在这个key,此时我们调用ConcurrentHashMap.containsKey(key)方法去做一个判断,我们期望的返回结果是false。但是恰好在A线程get(key)之后,调用constainsKey(key)方法之前B线程执行了ConcurrentHashMap.put(key,null),那么当A线程执行完containsKey(key)方法之后我们得到的结果是true,与我们预期的结果就不相符了。
ConcurrentHashMap的并发度是什么
程序在运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。
JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?
- JDK8中新增了红黑树
- JDK7中使用的是头插法,JDK8中使用的是尾插法
- JDK7中使用了分段锁,而JDK8中没有使用分段锁了
- JDK7中使用了ReentrantLock,JDK8中没有使用ReentrantLock了,而使用了Synchronized
- JDK7中的扩容是每个Segment内部进行扩容,不会影响其他Segment,而JDK8中的扩容和HashMap的扩容类似,只不过支持了多线程扩容,并且保证了线程安全
ConcurrentHashMap与HashMap有什么区别?
数据结构:在JDK1.7中ConcurrentHashMap底层采用分段数组+链表的方式实现。在JDK1.8中ConcurrentHashMap与JDK1.8中的HashMap底层数据结构一样,都是采用数组+链表或者数组+红黑树的方式实现。这二者底层数据结构都是以数组为主体的。
线程安全:HashMap是线程不安全的,ConcurrentHashMap是线程安全的。
ConcurrentHashMap和HashTable的效率哪个更高?为什么?
Hashtable也是线程安全的,但每次要锁住整个结构,并发性低。相比之下,ConcurrentHashMap获取size时才锁整个对象。
Hashtable对get/put/remove都使用了同步操作。ConcurrentHashMap只对put/remove同步。
Hashtable是快速失败的,遍历时改变结构会报错ConcurrentModificationException。ConcurrentHashMap是安全失败,允许并发检索和更新。
HashTable和ConcurrentHashMap的锁机制
HashTable是使用Synchronized来实现线程安全的,是使用一把锁锁住整个链表结构,效率非常低。当有一个线程访问同步方法的时候,其他线程是访问不了的,其他线程可能会被阻塞或者进入轮询状态。如果有一个线程正在执行put()操作的时候,其他线程是不可以进行put()操作的,也不可以进行get()操作,并发线程越多,竞争越激烈,效率越低下。
ConcurrentHashMap锁机制:
jdk7:对整个数组进行分段(每段都是由若干个hashEntry对象组成的链表),每个分段都有一个Segment分段锁(继承ReentrantLock分段锁),每个Segment分段锁只会锁住它锁守护的那一段数据,多线程访问不同数据段的数据,就不会存在竞争,从而提高了并发的访问率。
jdk8:ConcurrentHashMap在JDK1.8中采用Node+CAS+Synchronized实现线程安全,取消了segment分段锁,直接使用Table数组存储键值对(与1.8中的HashMap一样),主要是使用Synchronized+CAS的方法来进行并发控制。在put()的时候如果CAS失败就说明存在竞争,会进行自旋。
ConcurrentHashMap在JDK1.8中为什么要使用内置锁Synchronized来替换ReentractLock重入锁?
- 锁粒度降低了;
- 官方对synchronized进行了优化和升级,使得synchronized不那么“重”了;
- 在大数据量的操作下,对基于API的ReentractLock进行操作会有更大的内存开销;
简单说下对 Java 中的原子类的理解?
什么原子类
原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。
原子类作用
作用和锁有类似之处,是为了保证并发情况下的线程安全。
相对于锁的优势
粒度更细
原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度也大于原子变量的粒度
效率更高
除了在高并发之外,使用原子类的效率往往比使用同步互斥锁的效率更高,因为原子类底层利用了CAS,不会阻塞线程。
原子类种类
在JDK中J.U.C包下提供了种类丰富的原子类,
类型 | 具体类型 |
---|---|
Atomic* 基本类型原子类 | AtomicInteger、AtomicLong、AtomicBoolean |
Atomic*Array 数组类型原子类 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
Atomic*Reference 引用类型原子类 | AtomicReference、AtomicStampedReference、AtomicMarkableReference |
Atomic*FieldUpdater 升级类型原子类 | AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater |
Adder 累加器 | LongAdder、DoubleAdder |
Accumulator 积累器 | LongAccumulator、DoubleAccumulator |
atomic 的原理是什么?原子类是如何利用 CAS 保证线程安全的?(以AtomicInteger为例)
- jdk 8实现,看怎么实现累加原子操作的
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
- Unsafe类
Unsafe是CAS的核心类。由于java无法直接访问底层操作系统,而是需要通过本地方法来实现。JVM还是提供了Unsafe类,他提供了硬件层面的原子操作,可以直接操作内存的数据。
该方法实际调用的是,unsafe.getAndAddInt(this, VALUE, delta),通过硬件层面的原子操作实现累加
- AtomicInteger的一些关键代码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception var1) {
throw new Error(var1);
}
}
private volatile int value;
public final int get() {
return value;
}
......
}
首先获取Unsafe类型变量unsafe,并定义变量valueOffset
然后在静态代码块中value值在内存中的偏移地址,赋值给valueOffset变量。因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。
value是用volatile修饰的,他就是我们原子类存储值的变量,由于使用volatile修饰,所以在多线程中看到的value值都是同一份,保证了可见性。
- 实际执行cas的地方。Unsafe的getAndAddInt()方法
// var1是当前原子对象,var2是value在内存中的偏移量,var4是增量值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 +var4));
return var5;
}
先看do-while循环,它是一个无限循环,直到满足条件才退出循环。
do中的代码,获取到当前内存中的值赋值给var5。
再看看while中的退出条件。compareAndSwapInt这个方法参数,他们的实际意义是:
第一个参数 —> 当前原子类对象,即AtomicInteger 这个对象本身
第二个参数 —> 当前值的内存偏移量,接触它可以获取到value的值
第三个参数 —> 当前值,如果当前值不匹配,更新失败会返回false,开始下次循环。如果当前值匹配,同时更新成功,方法返回true,跳出循环,完成当前累加操作
第四个参数 —> 期望值,即当前值加上累加的值。
所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5 相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。
总结一下,Unsafe 的 getAndAddInt 方法是通过循环 + CAS 的方式来实现的,在此过程中,它会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败就重新获取,然后再次尝试更新,直到更新成功。
原子类高并发会存在的问题
在并发情况下,如果我们需要实现计数器(例如下载任务数),可以利用AtomicInteger和AtomicLong,这样一来可以避免加锁和复杂的代码逻辑。
public class AtomicLongDemo {
public static void main(String[] args) throws InterruptedException {
AtomicLong counter = new AtomicLong(0);
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
Thread.sleep(2000);
System.out.println(counter.get());
}
static class Task implements Runnable {
private final AtomicLong counter;
public Task(AtomicLong counter) {
this.counter = counter;
}
@Override
public void run() {
counter.incrementAndGet();
}
}
}
内部的 value 属性而言,也就是保存数值的属性,它是被 volatile 修饰的,所以它需要保证自身可见性。
当线程1执行incrementAndGet操作更新成功后,会将值向主内存中修改,同时主内存会将其他线程的value给修改掉,而且CAS也会经常失败,这两个操作是非常耗资源的。
在JDK 8中新增了LongAdder 类,来优化这一个问题。
public class LongAdderDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder counter = new LongAdder();
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
Thread.sleep(2000);
System.out.println(counter.sum());
}
static class Task implements Runnable {
private final LongAdder counter;
public Task(LongAdder counter) {
this.counter = counter;
}
@Override
public void run() {
counter.increment();
}
}
}
为什么
public class LongAdder extends Striped64 implements Serializable {}
abstract class Striped64 extends Number {
......
/**
* 单元格表。如果为非null,则大小为2的幂。
*/
transient volatile Cell[] cells;
/**
* 基本值,主要在没有争用时使用,也用作表初始化过程中的回退。通过CAS更新。
*/
transient volatile long base;
......
}
LongAdder中引入了分段累加的概念,内部的Cell[]数组和base变量都参与了计数
其中base在竞争不激烈的情况下,直接把累加的结果改到base变量上;
在竞争激烈的时候,各个线程会分散累加到自己所对应的Cell[] 数组的某一个对象中,而不是公用一个。LongAdder这样分段的思想将不同线程到不同Cell上的修改,避免了大量的冲突,提升了并发性。
和JDK 7 中的ConcurrentHashMap思想相同。(baseCount + CounterCell)
竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中降低了冲突的概率。这个思想的本质就是空间换时间,所以会耗费更多的内存。
最终的结果是通过LongAdder的sum()方法来获取的,将各个Cell值累计求和,再加上base返回。
AtomicLong和LongAdder使用如何选择
在低竞争的情况下,AtomicLong和LongAdder的性能相似;但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,经过试验,LongAdder 的吞吐量大约是 AtomicLong 的十倍,虽然性能提高了,但是LongAdder会耗费更多的空间
LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一
而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。
Java 8 中 Adder 和 Accumulator 有什么区别
Adder是通过CAS加分段思想来提高Atomic*的性能。
而LongAccumulator是LongAdder的功能增强版,LongAccumulator在LongAdder只有数值加减的基础上提供自定义的函数操作。
public class LongAccumulatorDemo {
public static void main(String[] args) throws InterruptedException {
LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0); // x是上一次的结果,y是传入的新值。
ExecutorService executor = Executors.newFixedThreadPool(8);
IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));
Thread.sleep(2000);
System.out.println(accumulator.getThenReset());
}
}
由于使用多线程,当前的方法只适用于即使执行顺序不同,结果依然一样的情况,即交 换 性 。
下面几种场景适合使用:
- 相加
- 相乘
- 最大值、最小值
Java死锁
Java死锁
当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
Java中导致死锁的原因
造成死锁必须达成的4个条件(原因):
- 互斥条件:一个资源每次只能被一个线程使用。 独占锁的特点之一。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。独占锁的特点之一,尝试获取锁时并不会释放已经持有的锁
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。 独占锁的特点之一。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
Java中如何避免死锁
避免死锁就是破坏造成死锁的,若干条件中的任意一个
在并发程序中,避免了逻辑中出现复数个线程互相持有对方线程所需要的独占锁的的情况,就可以避免死锁。
Java线程数过多会造成什么异常?
1.线程栈是需要分配内存空间的,所以有数量上限
2.cpu切换线程涉及到上下文恢复,这个是需要耗费时间的,如果线程非常多而且切换频繁(处理IO密集任务),这个时间损耗是非常可观的。
线程池应该设置多大,取决于你处理的任务类型。
- 对于CPU密集型的任务,因为线程中基本不会有阻塞导致让出CPU,只有在时间片用完以后,才可能让出CPU,这种情况发生线程切换的次数要少很多,因此不建议设置太大,netty的建议是设置为2倍的cpu核心数。
- 为何不设置为核心数呢?因为系统上不仅仅只跑你的java程序,还有别的进程也会来抢占你的cpu资源。因此,你需要在更多的争抢机会和更少的上下文切换之间取得平衡。
- IO密集型任务,仅仅是指你的IO很可能是未就绪需要阻塞等待的任务。那要提高吞吐量,一般有两种办法
- 一种是基于IO多路复用+NIO/AIO,这种办法实际是想办法去掉不必要的阻塞,尽量把阻塞型的IO密集任务,转成CPU密集任务,这样你只需要少量线程也可以获得很高的吞吐量。这就是为何select、poll 、epoll、nginx可以用很少的线程可以获得极大吞吐量的原因。
- 另一种办法就是很简单的扩大线程数了,理论上来说,只要你的线程数,不会导致明显的上下文切换损耗,而且不会造成内存溢出,线程池就可以设置的足够大。某大厂的rpc框架设置的默认最大线程数就超过了500,核心线程数也大大超过了CPU核心数。
说下对 Fork和Join 并行计算框架的理解?
分治
分治的思想,顾名思义分而治之。就像古代的王想治理好天下,单单靠他一个人是不够的,还需要大臣的辅助,把天下划分为一块块区域,分派的下面的人负责,然后下面的人又分派给他们的属下负责,层层传递。
这就是分治,也就是把一个复杂的问题分解成相似的子问题,然后子问题再分子问题,直到问题分的很简单不必再划分了。然后层层返回问题的结果,最终上报给王!
分治在算法上有很多应用,类似大数据的MapReduce,归并算法、快速排序算法等。 在JUC中也提供了一个叫Fork/Join的并行计算框架用来处理分治的情况,它类似于单机版的 MapReduce
。
fork (函数)
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。它不需要参数并返回一个整数值。下面是fork()返回的不同值。
- 负值:创建子进程失败。
- 零:返回到新创建的子进程。
- 正值:返回父进程或调用者。该值包含新创建的子进程的进程ID
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值,而父进程中返回子进程ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的副本,这意味着父子进程间不共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。
为什么fork会返回两次?
由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。过程如下图。
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值。
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fork函数返回的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0.
fork()在Linux系统中的返回值是没有NULL的.
Error Codes 出错返回错误信息如下:
EAGAIN
达到进程数上限.
ENOMEM
没有足够空间给一个新进程分配.
fork函数的特点概括起来就是<b>调用一次,返回两次</b>,在父进程中调用一次,在父进程和子进程中各返回一次。
fork的另一个特性是<b>所有由父进程打开的描述符都被复制到子进程中</b>。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的<b>引用计数</b>要增加。
join(函数)
Fork/Join框架
分治分为两个阶段,第一个阶段分解任务,把任务分解为一个个小任务直至小任务可以简单的计算返回结果。
第二阶段合并结果,把每个小任务的结果合并返回得到最终结果。
而Fork
就是分解任务,Join
就是合并结果。
Fork/Join框架主要包含两部分:ForkJoinPool、ForkJoinTask
。
ForkJoinPool
就是治理分治任务的线程池。它和在之前的文章提到[ThreadPoolExecutor](https://juejin.im/post/5cbaf0dd6fb9a068744e7beb)
线程池,共同点都是消费者-生产者模式的实现,但是有一些不同。ThreadPoolExecuto
r的线程池是只有一个任务队列的,而ForkJoinPool
有多个任务队列。通过ForkJoinPool
的invoke
或submit
或execute
提交任务的时候会根据一定规则分配给不同的任务队列,并且任务队列的双端队列。
execute 异步,无返回结果 invoke 同步,有返回结果 (会阻塞) submit 异步,有返回结果 (Future)
为啥要双端队列呢?因为ForkJoinPool
有一个机制,当某个工作线程对应消费的任务队列空闲的时候它会去别的忙的任务队列的尾部分担(stealing)任务过来执行(好伙伴啊)。然后那个忙的任务队列还是头部出任务给它对应的工作线程消费。这样双端就井然有序,不会有任务争抢的情况。
ForkJoinTask
这就是分治任务啦,就等同于我们平日用的Runnable
。它是一个抽象类,核心方法就是fork
和join
。fork
方法用来异步执行一个子任务,join
方法会阻塞当前线程等待子任务返回。
ForkJoinTask
有两个子类分别是RecursiveAction
和RecursiveTask
。这两个子类也是抽象类,都是通过递归来执行分治任务。每个子类都有抽象方法compute
差别就在于RecursiveAction
的没有返回值而RecursiveTask
有返回值。