概念
JUC就是java.util .concurrent工具包的简称。这是一个处理线程的工具包。
为什么要使用多线程
是因为多个线程之间会发生数据竞争,导致CPU线程调度时出现问题,不能够保证线程内执行代码的原子操纵;
多线程的时候,一个cpu维护多组寄存器(寄存器时用来保存计算所需要的数据的)
例如:多线程i++。
- 从内存读取i到寄存器
- 寄存器里的数+1
- 寄存器写回内存
当多个线程的时候,两个线程同时走了第一步,,都把1放入自己的寄存器,导致结果时2。
synchronized关键字
synchronized的三种应用方式
修饰实例方法、修饰this(可属于第三种)
,作用于当前实例加锁,进入同步代码前要获得当前实例的锁静态方法、修饰类对象.class(也可以属于第三种)
,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁修饰代码块``,指定加锁对象
,对给定对象加锁
,进入同步代码库前要获得给定对象的锁。
synchronized基本定义
synchronized可以作用于一段代码或方法,既可以保证可见性,有可以保证有序性。
- 可见性:一个synchronized能保证同一时刻只有一个线程获取锁,执行完同步代码后,在释放锁之前会把变量的修改刷新到主内存中。
- 原子性:一个操作一旦开始,就不会被其他线程干扰。
对象在内存中的布局
在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
其中对象头包含对象标记和类元信息两部分,Java对象头是实现 synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
Mawrk Word
Mark Word(对象标记)用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节, 也就是32bit)
synchronized的锁升级和获取过程
JDK早期的时候,synchronized的底层时重量级的,需要去找操作系统申请锁,导致效率非常低。
执行时间短(加锁代码),线程数少,用自旋
执行时间长,线程数多,用系统锁
自旋锁(CAS)
:让不满足条件的线程等待一会看能不能获得锁,通过占用处理器的时间来避免线程切换带来的开销。自旋等待的时间或次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁
:大多数情况下,锁总是由同一个线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,偏向锁是一个可重入的锁
。如果锁对象头的Mark Word里存储着指向当前线程的偏向锁
,无需重新进行CAS操作来加锁和解锁。当有其他线程尝试竞争偏向锁时,持有偏向锁的线程(不处于活动状态)才会释放锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定进而升级为轻量级锁
。
轻量级锁
:减少无实际竞争情况下,使用重量级锁产生的性能消耗。JVM会现在当前线程的栈桢中创建用于存储锁记录的空间 LockRecord,将对象头中的 Mark Word 复制到 LockRecord 中并将 LockRecord 中的 Owner 指针指向锁对象。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则当前线程获取到锁,失败则表示其他线程竞争锁当前线程则尝试使用自旋的方式获取锁。自旋获取锁失败则锁膨胀升级为重量级锁。
重量级锁
:通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。
Synchronized 结合 Java Object 对象中的 wait,notify,notifyAll
前面我们在讲 synchronized 的时候,发现被阻塞的线程什 么时候被唤醒,取决于获得锁的线程什么时候执行完同步代码块并且释放锁。那怎么做到显示控制呢?我们就需要借助一个信号机制:在 Object 对象中,提供了wait/notify/notifyall,可以用于控制线程的状态。
wait/notify/notifyall 基本概念
1.wait 方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以 wait 必须要获得一个监视器锁。
2.对于 notify 来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。
3.每个对象可能有多个线程调用wait方法,所以需要有一个等待队列存储这些阻塞线程。这个等待队列应该与这个对象绑定,在调用wait和notify方法时也会存在线程安全问题所以需要一个锁来保证线程安全。
线程与进程
- 进程:进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能【调度】)
- 线程:线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。
volatile关键字
概念
volatile关键字,使一个变量再多个线程间可见
,会强制所有线程都会去堆内存中读取running值。volatile不能保证多个线程并发修改running变量时所带来的不一致问题
,也就是说volatile不能替代synchronized。(volatile不能保证原子性)
volatile的作用
-
保证线程的可见性
-
堆内存是线程共享的,同时线程都有一块专属的工作内存,正常线程去访问堆中一个值的时候,会将这个值
copy一份到工作内存
中,改完再写回去。问题是这个线程的修改,其他线程并不可见
。 -
加了volatile后能保证一个线程的改变,另一个线程能立马看到。volatile不能保证原则性,
只能保证每次读到都是最新的
。 -
本质上是使用了cpu的高速缓存一致性协议,MESI。
-
即Modified(被修改)、Exclusive(独占的)、Shared(共享的)、Invalid(无效的)。MESI的基本思想就是如果发现CPU操作的是共享变量,其他CPU中也会出现这个共享变量的副本,在CPU执行代码期间,会发信号通知其他CPU自己正在修改共享变量,其他CPU收到通知后会把共享变量置为无效状态。
-
禁止指令重排序
-
cpu为了提高效率,它会把指令并发执行,可能第一个指令执行到一半,第二个就开始了。
-
通过加入内存屏障来禁止特定类型的处理器重排序。
volatile的使用场景
-
boolean isStop = false; while(!isStop){ ... } isStop = true;
-
volatile不会像锁一样造成线程阻塞,在某些读操作远大于写操作的情况下,使用volatile比锁性能更好,一般在以下这种情况下:
-
对变量的写操作不依赖于当前值。
-
该变量没有包含在具有其他变量的不变式中。
-
CAS+volatile = atomic,atomic能保证原子性就用的CAS和volatile
多线程
4种线程池
-
newFixedThreadPool 定长线程池
-
最大线程数等于核心线程数,线程池线程不会因为闲置被销毁
-
队列使用的是LinkedBlockingQueue无界阻塞队列
-
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
-
newCachedThreadPool 可缓存线程池
-
核心线程数为0,线程的数量最高无限,有空闲线程就复用,没有就新建
-
队列使用SynchronousQueue不缓存任务队列
-
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())
-
newSingleThreadExecutor 单线程线程池
-
只有一个线程工作
-
队列使用LinkedBlockingQueue无界阻塞队列
-
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
-
newScheduleThreadPool 固定大小线程池,支持定时即周期性任务执行
-
队列使用DelayedWorkQueue 延迟队列
-
ThreadPoolExecutor 自定义线程池
线程池参数
-
核心线程数
corePoolSize
-
最大线程数
maxPoolSize
-
空闲线程存活时间
keepAliveTime
-
空闲时间存活时间单位
TimeUnit
-
工作队列
workQueue
-
ArrayBolckingQueue 有界阻塞队列,如果达到maxPoolSize,则会执行拒绝策略
-
LinkedBlockingQueue 无界阻塞队列,有新任务会一直存入队列,maxPoolSize其实是无效
-
SynchronousQuene 不缓存任务的阻塞队列,没有可用线程直接创建新线程,达到maxPoolSize后,执行拒绝策略
-
PriorityBlockingQueue 具有优先级的无界阻塞队列,优先级通过参数Compareator实现
-
DelayedWorkQueue 延迟队列
-
线程工厂
threadFactory
-
创建新线程时使用的工厂,可以用来设置创建线程的一些参数定线程名等等
-
拒绝策略
handler
-
CallerRunsPolicy
-
该策略下,在调用者线程中直接执行被拒绝的任务的run方法,除非线程池shutdown则直接抛弃任务
-
AbortPolicy
-
直接抛弃任务,并抛出RejectedExecutionExecption异常
-
DiscardPolicy
-
直接抛弃任务,什么都不做
-
DiscardOldestPolicy
-
抛弃队列中最早的那个任务,尝试把这次拒绝的任务放入队列
synchronized和lock的区别
- synchronized属于
jvm层面的关键字
,底层通过monitorenter指令实现的;而lock是属于JVM的一个类 - synchronized在代码
执行异常
时或正常执行完毕
后,jvm会自动释放锁;而lock必须加上一场处理,而且必须在finally块上写上unlock()释放锁
- synchronized
不可中断
,只能等待程序执行完毕或者异常退出;而lock可以通过interrupt来中断
- synchronized不能精确唤醒指定的线程;而lock可以通过condition精确唤醒
- synchronized
无法判断锁的状态
,从而无法知道是否获取锁;而lock可以判断锁的状态
- synchronized适合少量同步代码块,lock适合大量同步代码块
- 都是可重入锁
多线程间通信
join
join()
方法的作用就是阻塞当前线程,等待join()
方法的线程执行完毕后再执行后面的代码。原理:通过synchronized锁线程的实例对象,通过wait阻塞线程,执行完成后通过notifyAll()
方法唤醒线程。
wait和notifyAll
需要多个线程使用同一把锁,可以wait释放资源,也可notifyAll通知线程结束等待
CountDownLatch
CountDownLatch它本身是一个计数器,可以设置一个初始值,通过线程调用countDown方法将计数器减一,当计数器为0的时候通知await()方法,被通知方法可以调用countDownLatch.await()方法检查计数器的值是否为0。(但是countDownLatch方法只有一个先线程awaiti()方法会得到响应)
CyclicBarrier
创建一个CyclicBarrier对象,设置同时等待的线程数,然后这些线程都开始准备,准备好了调用cyclicBarrier.await()方法,当所有方法都调用后,才开始一起执行
Callable
可以将子线程的值返回给主线程
volatile
通过volatile是线程间变量可见来通知
BlockingQueue
当生产者试图向BolckingQueue中放入元素时,如果队列已满,则线程阻塞。当消费者从BlockingQueue中取元素时,如果队列已空,消费者则被阻塞。
Condition
await(),signal(),signalAll()
ThreadLocal的理解
ThreadLocal叫做线程变量,ThreadLocal的源码中只给出了get(),set(),remove()的方法。
-
set方法
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
从set方法可以看到先获取了线程t,然后通过getMap(t)方法中获取ThreadLocalMap,如果不存在则新建一个。
然后再看getMap方法,调用当前线程,返回当前线程的threadLocals,这就是ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ConcurrentHashMap的原理
舍弃了分段锁的实现方式,元素都放在node数组中,每次锁住的是一个Node对象,而不是某一段数组,所以支持的写的并发度更高
死锁的条件
- 互斥:就是线程在某一时间独占的资源,不能共享的。
- 不可剥夺:当资源被占用的情况下,在未使用完之前,不可被强行剥夺。
- 请求保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 循环等待:若干线程之间形成头尾相连的循环等待资源关系。
怎么防止死锁
- 尽量使用tryLock设置超时时间,超时自动退出,防止死锁。
- 按照一定顺序加锁
- 尽量减少同步代码块,减少锁的颗粒度,不要将几个功能用同一把锁