@[toc]
章节名称 | 文章地址 |
---|---|
秋春招总结之MySQL | MySQL |
秋春招总结之Redis | Redis |
秋春招总结之并发多线程 | 并发多线程 |
每周二完定时更新
前言
关于Java多线程方面的知识涉及广泛,从最基础的输入一个指令,等待运行完成到批处理操作系统,再到后来进程和线程的提出与熟练运用到我们的日常生活中,无疑也是我们计算机的稳步发展的映照,这篇博客将尽可能的总结目前出现的一些面试题目已经自己遇到过的一些题目,希望在自己总结几个月来遇到的问题的同时也能够进行进一步的深化与升华,每一次的记录也都让我更加记忆深切。
1. 基础
进程与线程的区别
注:这个题目是学习计算机操作系统必备的题目,也是初步进行理解与掌握后面问题的关键所在
粗略回答:进程是操作系统进行资源分配与调度的基本单位,线程是任务调度与执行的基本单位,即CPU分配时间单位。他们的本质区别是是否单独占有内存地址空间以及其他系统资源。
区别
同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器,堆栈,上下文) 一个进程至少包括一个线程
线程是轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销比较小。
包含关系
只有一个线程的进程可以看做是单线程的,如果一个进程内有多个线程,在执行的过程是多条(线程)共同完成的; 线程是进程的一部分所以也被称之为轻量级进程。
什么是并发编程的三要素? 在Java中如何来保证多线程的安全运行。
三要素:
- 原子性:就是我们的程序是一个不可分割的整体,所有的操作要么全部都执行,要么都不执行或者都是失败,不可能说对于一段程序,一部分成功执行并提交,另外一部分执行失败。
- 有序性:程序的执行在原则上会按照我们写的顺序顺序执行下去(但是在有些情况下,为了能够提高处理效率,在满足一定的条件下,是运行进行指令的重排序)
- 可见性: 利用到JMM (Java内存模型)来实现对共享变量的共享可见(一个线程进行了修改,另一个线程就可以得知)
可能会出现的问题:
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决方案:
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
什么是并行,什么是并发,说一说两者之间的区别:
注: 面试中切实遇到过
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
并发: 多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行(其实只是分配时间片进行执行)。
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
2. 实现Java的多线程
对于这个题目来说可能会问道: 你来说一说创建线程有几种的方式?并说一说具体的区别
这里就需要自己来进行比较充分的准备,首先明白创建的几种方式,然后自己实现进行理解与掌握。
创建的四种方式:
继承 Thread 类;
- 定义一个类,继承Thread,并复写run()方法,对于run方法来说就是实现我们自己的业务代码。
- 实例化自己创建的类的对象,调用 start()方法(这里必须调用start 方法才算是真正的启动线程)
public class Demo {
public static class MyThread extends Thread{
public void run(){
System.out.println("Myhread");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("MyThread Is Run ");
}
}
这个执行结果大家可以去运行尝试一下,有以下r注意点:
- 我们在程序里面调用了 start(方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。
- 注意不可多次调用 start(方法。在第一次调用 start()方法后,再次调用 start()方法会抛出异常。
实现Runnable接口
- 定义一个自己的类,实现接口 Runnable,并复写run()方法。
- 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
- 调用线程对象的start()方法
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable);
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println("MyRunnable Is Run");
}
实现 Callable 接口
- 创建实现Callable接口的类myCallable
- 以myCallable为参数创建FutureTask对象
- 将FutureTask作为参数创建Thread对象
- 调用线程对象的start()方法
public class Demo {
public static class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
System.out.println("MyCallable");
return 0;
}
}
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
try {
Thread.sleep(1000);
System.out.println("返回结果 " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
}
使用线程池
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool。
具体的内容可以参看线程池的理解与运用,里面有具体详细的讲解与可能会出现的面试问题。
Thread类与Runnable接口的比较:
实现一个自定义的线程类,可以有继承 Thread类或者实现 Runnable接口这两种方式,它们之间有什么优劣呢?
- 由于Java"单继承,多实现"的特性, Runnable接口使用起来比 Thread更灵活。
- Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
- Runnable接口出现,降低了线程对象和线程任务的耦合性
- 如果使用线程时不需要使用 Thread类的诸多方法,显然使用 Runnable接口更为轻量。
所以,我们通常优先使用“实现 Runnable接口”这种方式来自定义线程类。
Callable
通常来说,我们使用 Runnable和 Thread来创建一个新的线程。但是它们有一个弊端,就是run方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
Callable 接口
与Runnable接口类似,同样是只有一个抽象方法的函数式接口,不同的是对于Callable来说 提供的方法是有返回值的,并且支持泛型:
public interface Callable{
V call() throws Exception;
}
对于Callable的使用来说一般都是配合到线程池工具:ExecutorService
来使用:
class Taskclass Task implements Callable{
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 2;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Task task =new Task();
Future result = executorService.submit(task);
// 注意调用 get 方法会阻塞当前线程 直到得到结果。
// 所以实际编码中 建议使用 可以设置超时时间的重载get方法。
System.out.println(result.get());
}
}
3. 线程的各个状态
- 新生(通过 new关键字创建一个线程)
- 就绪 (通过start关键之让线程进入就绪的状态)
- 运行 (通过CPU的调用进入到 运行状态)
- 阻塞 (线程在运行的过程中遇到了sleep(睡眠) yield(让步)wait(等待)会进入到阻塞的状态)有三种。
- 等待阻塞(o.wait->等待对列):
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)
中。
- 同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线
程放入锁池(lock pool)中。
- 其他阻塞(sleep/join)
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,
JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O
处理完毕时,线程重新转入可运行(runnable)状态。
死亡(也有三种的情况)
正常结束- run()或 call()方法执行完成,线程正常结束。
异常结束
- 线程抛出一个未捕获的 Exception 或 Error。
调用 stop
- 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
4. 各种状态下的问题
关于线程同步以及线程调度的相关方法
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
sleep和wait的区别
我们都知道的是对于sleep和wait都是会让线程出现暂停执行的状态,下面从几个方面进行剖析个体区别
- 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于
Object 类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然
保持者,当指定的时间到了又会自动恢复运行状态。
- 在调用 sleep()方法的过程中,线程不会释放对象锁。
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此
对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
- 使用的位置不同:对于wait来说使用之前要获取到锁的存在,所以必须放在同步代码,或者同步块中进行执行 但是 sleep来说可以放在任何的地方执行 。
- sleep需要捕获异常 。wait notify 等不需要这些。
sleep和yield 的区别
(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
start 和run的区别
- start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,
可以直接继续执行下面的代码。
- 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运
行。对于多线程来说只有真正意义上调用了start方法才算是对于线程的一个启动。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运
行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
join()
join()方法使调用该方法的线程在此之前执行完毕,也就是等待该方法的线程执行完毕后再往下继续执行。注意该方法也需要捕捉异常。
就是说让该线程在执行完RUN()方法以后再执行join方法后面的代码,就是说可以让两个线程合并起来,用于实现同步功能
yield()
该方法与sleep() 类似 只不过不能够由用户指定暂停多长的时间,并且yield ()方法只能让同优先级的线程有执行的机会。 前面提到了 sleep不会释放锁标识yield也不会释放锁标识。
实际上,yield()方法对应了如下操作;先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给次线程,否则继续运行原来的线程,所以yield()方法称为“退让”,它把运行机会让给了同等级的其他线程。
sleep 方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程此时获取CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,也没有受到I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,方可有机会运行。yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会。
wait()和notify()、notifyAll()
这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用。synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。wait() 方法使当前线程暂停执行并释放对象锁标示,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。notifyAll() 从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。
wait,notify阻塞唤醒确切过程?在哪阻塞,在哪唤醒?为什么要出现在同步代码块中,为什么要处于while循环中?
常见的 void wait 方法有
- wait( long timeout )
- wait()。
对于 无参的方法来说 : 在其他线程调用 此对象的notify 方法或者 nofifyall方法前 导致当前的线程处于等待的状态。
对于有参的函数来说 以上的两条成立的情况下 还会在时间超时之前也是处于等待的状态。
对于在执行完 wait方法以后。线程会释放掉所占用的锁标识 从而使线程所在的对象中的其他synchronized数据可被别的线程使用。 因为在执行wait 和 notify() 时候需要对锁标志进程处理和操作 一个是释放锁 一个是加锁 所以 就是来说 需要要在 synchronized函数中或者 函数块中进行调用,如果不在函数中 或是函数块中进行调用 虽然说可以编译通过。但是会出现 IllegalMonitorStateException
异常。
wait,notify 和notifyAll 这些方法为什么不在 thread类里面
一个很明显的原因是Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程来获得 由于 wait notify和notifyAll 都是锁级别的的操作,所以把他们定义在Object类中因为锁属于对象。
如何唤醒一个阻塞的线程
首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;
其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
5. 线程之间的通行
通过加锁
基本概念:
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)
在我们的线程之间,有ー个同步的概念。什么是同步呢,假如我们现在有2位正在抄暑假作业答案的同学:线程A和线程B。当他们正在抄的时候,老师突然来修改了一些答案,可能A和B最后写出的暑假作业就不ー样。我们为了AB能写出2本相同的暑假作业,我们就需要让老师先修改答案,然后A,B同学再抄。或者A,B同学先抄完,老师再修改答案。这就是线程A,线程B的线程同步。
理解为线程之前的同步是按照一定的顺序执行的。为了能够达到线程的同步,我们需要用锁进行实现。
无锁情况
static class ThreadA implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("ThreadA"+ i);
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("ThreadB"+ i);
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
运行结果:可以发现的是对于A与B来说都是各自独立工作,也就谈不上我们所说的两者之间进行通信的处理。
ThreadA8
ThreadA9
ThreadA10
ThreadA11
ThreadB0
ThreadA12
现在我们要求A执行完成之后B才能够继续执行下去需要使用到对象锁。
有锁情况
private static Object lock = new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock)
{
for(int i=0;i<100;i++){
System.out.println("ThreadA"+ i);
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("ThreadB" + i);
}
}
}
}
public static void main(String[] args) throws InterruptedException{
new Thread(new ThreadA()).start();
Thread.sleep(100);
new Thread(new ThreadB()).start();
}
这里声明了一个名字为lock的对象锁。我们在 ThreadA和 Thread B内需要同步的代码块里,都是用 synchronized
关键字加上了同一个对象锁lock
。
我们说到了,根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放lock
,线程B才能获得锁lock
这里在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。
利用等待/通知机制
第一种方法是利用到锁的机制,但是在有的时候获取锁可能会消耗很多的时间,而基于Object 类的wait
方法 和notify
方法,notifyAll
方法多线程的通知/等待机制正是解决锁问题的最好办法。
我们知道的是在同一时间里面,一个锁同一时刻是能是被一个线程所持有 ,现在假设A拥有了这个锁,这个时候对于线程B来说是不能够获取到这个锁,但是线程A可以利用 lock.wait 方法来让自己处于等待的状态,这个时候,lock就是相当于被释放。
这个时候线程B获取到锁并开始执行,可以在某一时刻里面使用到lock.notify() 通知之前持有锁但是进入到等待状态的A,表示A可以不用等待了,可以继续向下执行
需要注意的是,这个时候线程B并没有释放锁lock,除非线程B这个时候使用Lock.wait()
释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock
锁。
代码模拟实现:
private static Object lock = new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock)
{
for(int i=0;i<10;i++){
try{
System.out.println("ThreadA"+ i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock)
{
for(int i=0;i<10;i++){
try{
System.out.println("ThreadB"+ i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException{
new Thread(new ThreadA()).start();
Thread.sleep(100);
new Thread(new ThreadB()).start();
}
输出结果:
在上面的栗子中线程A和线程B先打印出自己需要的东西,然后使用notify()方法叫醒另一个正在等待的线程,然后利用到wait() 方法让自己陷入等待,并释放锁。
信号量
来利用到关键字volatile
来实现信号之间的通信。这里先来介绍以下主要的功能:
volatile
关键字能够保证内存的可见性,如果用volatile
关键字声明了一个在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。
下面举一个小栗子来模拟对于信号量时候的线程之间的通信:
让线程A先输出0,然后线程B输出1,再让线程A输出2 以此类推。
代码:
static volatile int single = 0;
static class ThreadA implements Runnable{
@Override
public void run() {
while(single<5){
if(single % 2 ==0){
System.out.println("ThreadA"+ single);
synchronized (this){
single++;
}
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
while(single<5){
if(single % 2 ==1){
System.out.println("ThreadB"+ single);
synchronized (this){
single++;
}
}
}
}
}
public static void main(String[] args) throws InterruptedException{
new Thread(new ThreadA()).start();
Thread.sleep(100);
new Thread(new ThreadB()).start();
}
实现结果:
ThreadA0
ThreadB1
ThreadA2
ThreadB3
ThreadA4
其他
join 方法
join()
方法是 Thread类的一个实例方法。它的作用是让当前线程陷入等待状态,等join的这个线程执行完成后,再继续执行当前线程。
有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。
如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法。
示例代码
static class ThreadA implements Runnable{
@Override
public void run() {
try{
System.out.println("我是子线程,我先睡一秒");
Thread.sleep(1000);
System.out.println(" 我是子线程,我完成了睡眠");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread thread =new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("标志");
// 在为是使用到jion时候,现打印出来 标志,然后再打印线程信息。
// 再使用到join 之后 先行打印 线程信息 再打印 标志
}
sleep 方法
sleep方法是 Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。它有这样两个方法·
- Thread. sleep(long)
- Thread.sleep(long, int)
同样,查看源码(JDK1.8)发现,第二个方法貌似只对第二个参数做了简单的处理,没有精确到纳秒。实际上还是调用的第一个方法
这里需要强调一下sleep方法是不会释放当前的锁的,而wait方法会。这也是最常见的一个多线程面试题,在文章的上面有进行具体的分析。
6. Java内存模型相关
对于Java内存模型也叫做JMM,涉及到关键字volatile
,这个关键字在后面会进行讲解,这里可以参看如下文章,,其中有对内存模型进行详细介绍:Java内存模型volatile
7. 重排序与Happens-before
什么是指令的重排序
在计算机执行程序时候,为了能够提高性能,编译器和处理器常常会对指令做重新排序处理,就是指令的重排序。
指令重排的条件
- 在单线程环境下不能改变程序的运行结果;
- 存在数据依赖关系的不允许重排序;
- 无法通过Happens-before原则推到出来的,才能进行指令的重排序
指令重排的三种情况:
- 编译器优化重排:编译器再不改变单线程程序语义的前体下,可以重新安排语句的执行顺序。
- 指令并行重排:才用到指令级并行基数来讲多条指令重叠执行,在不存在数据依赖(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
- 内存系统重排:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储( store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。
Happens-before
什么是Happens-before
一方面,我们需要JMM给我们提供一个强大的内存模型来编写代码,同时另外一方面对于编译器和处理器来说希望JMM对他们的约束越少越好,这样就可以进行跟多的优化处理,也就是希望是一个弱的内存模型。
于是对于JMM来说考虑了这两种的需求: 对于编译器和处理器来说:只要不改变程序的运行结果(单线程程序和正确同步了的多线程程序),编译器和处理器进行如何的优化都是可行的。
于是JMM提供了一个Happens-before(JSR-133规范),来满足我们在简单易懂的前提下,并且提供了足够强的内存可见性保证。
就是说对于这个规则来说,我们只要是遵循了 就能够保证其在JMM中具有强的内存可见性。
定义
- 如果一个操作 happens-before另一个操作,那么第一个操作的执行结果将对第操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 两个操作之间存在 happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens- before关系来执行的结果一致,那么JMM也允许这样的重排序
天然的happens-before有哪些
在Java中,有以下天然的 happens-before关系:
- 程序顺序规则:一个线程中的每一个操作, happens-before于该线程中的任意后续操作。·
- 监视器锁规则:对一个锁的解锁, happens- before于随后对这个锁的加锁。
- volatile变量规则:对一个 volatile域的写, happens- before于任意后续对这个volatile域的读。
- 传递性:如果 A happens- before b,且 B happens-before C,那么 A happensbefore c。
- start规则:如果线程A执行操作 Thread. start0启动线程B,那么A线程的Thread B.stat0)操作 happens-before于线程B中的任意操作
- join规则:如果线程A执行操作 Thread join()并成功返回,那么线程B中的任意操作 happens-before于线程A从 Thread join0操作成功返回。
as-if-serial规则和happens-before规则的区别(重点)
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
8. Volatile关键字
相关的重要概念
内存可见性
在Java内存模型那一章我们介绍了JMM有一个主内存,每个线程有自己私有的工作内存,工作内存中保存了一些变量在主內存的拷贝。
内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值
重排序
为了优化程序性能,对原来有的指令执行顺序进行优化重新排序。重排序可能发生在很多的阶段,比如编译重排序,CPU 重新排序
happens-before规则
是一个给程序员使用的规则,只要程序员在写代码的时候遵循 happens- before规JMM就能保证指令在多线程之间的顺序性符合程序员的预期。
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile有什么具体功能
Java 提供了 volatile 关键字来保证可见性和禁止指令重排。
- volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
- 禁止指令的重排序功能。
volatile与普通变量排序的规则
1.如果第一个操作是 volatile读,那无论第二个操作是什么,都不能重排序
2.如果第二个操作是 volatile写,那无论第一个操作是什么,都不能重排序
3.如果第一个操作是 volatile写,第二个操作是 volatile读,那不能重排序。
9. synchronized 与锁
有什么作用
在 Java 中,synchronized
关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized
代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock
来实现的,Java 的线程是映射到操作系统的原生线程之上的。
如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
平时是怎么使用这个关键字的,在项目中如何利用
synchronized关键字最主要的三种使用方式:
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结:
- synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
- synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
双重校验实现对象单例
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。
请知道一条原则:同步的范围越小越好。
说一下synchronized底层实现
既然说到底层的实现原理,就不免要进行对代码的反编译处理,查看相应的字节码文件:
首先来看一个简单的实现:
public class sysDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized");
}
}
}
然后现进行编译成为.class文件:javac sysDemo.java。
再进行反编译:javap -v sysDemo
可以看到的是在执行同步代码块前后都有一个monitor字样,其中前面的是monitorenter
,后面的是monitorexit
.
于是我们推断出:一个线程要执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter
表示进入,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit
指令。
为什么会有两个monitorexit
呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。
synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
说一下对于在Java6 之后进行的锁的升级
Java8为了減少获得锁和释放锁带来的性能消耗,引入了“偏冋锁”和“轻量级锁“在Java6以前,所有的锁都是”重量级“锁。所以在Java6及其以后对象其实有四种锁状态,它们级别由低到高依次是
1.无锁状态
2.偏向锁状态
3.轻量级锁状态
4.重量级锁状态
对象头
因为对于java中,其锁都是基于对象的,首先我们来看一看一个对象的锁的信息都存放在什么位置:
每个Java对象都有对象头。如果是非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位处理器中,一个字宽是32位;在64位虚拟机中,一个字宽是64位。对象头的内容如下图:
主要来看对于mark Word 中存放的都是什么内容:
偏向锁
我们发现在大多数的情况下,锁不仅存在多线程竞争,而且总是由同一个线程多次获取到,于是就引入了偏向锁:
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能
翻译过来就是说,我们会对锁设置一个变量,若是发现是true,代表资源没有竞争,就是说没有其他的线程想要来获取这个锁,就也不需要在添加各种的加锁/解锁的流程。但是若是false时候,代表存在其他线程来竞争资源,就才会进行后面的操作。
实现原理:
一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向的线程|D。当下次该线程进入这个同步块时,会去检查锁的 Mark Word里面是不是放的自己的线程ID。
如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS
操作来加锁和解锁;如果不是,就代表有另一个线程来竟争这个偏向锁。这个时候会尝试使用CAS
来替换 Mark Word!里面的线程ID为新线程的ID,这个时候要分两种情况:
- 成功,表示之前的线程不存在了, Mark Word里面的线程D为新线程的D,锁不会升级,仍然为偏向锁.
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁
CAS: Compare and Swap
比较并设置。用于在硬件层面上提供原子性操作。在lnte处理器中,比较并交换通过指令
cmpxchg
实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
轻量级锁
多个线程在不同时间段获取到同一个把锁,即不存在锁竞争的情况,也就没有后续的线程阻塞。 针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。
轻量级加锁:
JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为Displaced Mark Word
。如果一个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的 Displaced Mark Word里面。
然后线程尝试用CAS将锁的 Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竟争锁,当前线程就尝试使用自旋来获取锁
自旋:不断尝试去获取锁,一般用循环来实现。
自旋是需要消耗CPU
的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。
但是JDK采用了更聪明的方式——适应性自旋
,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
自旋也不是一直进行下去的,如果自旋到一定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。
轻量级释放
在释放锁时,当前线程会使用CAS操作将 Displaced Mark Word
的内容复制回锁的Mark Word
里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
重量级锁
重量级锁依赖于操作系统的互斥量(muteκ)实现的,而操作系统中线程间状态的转专换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。
总结升级流程(常问重点)
每一个线程在准备获取共享资源时:第一步,检查 Mark Word里面是不是放的自己的 Threadld,如果是,表示当前线程是处于“偏向锁"。
第二步,如果 Markward不是自己的 Threadld,锁升级,这时候,用CAS来执行切换,新的线程根据 Mark Word里面现有的 Threaded,通知之前线程暂停,之前线程将 Markward的内容置为空。
第三步,两个线程都把锁对象的 Hashcode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS
操作,把锁对象的 Markward的內容修改为自己新建的记录空间的地址的方式竞争 Markward。
第四步,第三步中成功执行CAS
的获得资源,失败的则进入自旋
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败
第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程之间存在竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级 | 竞争的线程恩不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求相应时间,同步块执行速度非常快 |
重量级 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,相应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
10.乐观于悲观锁
什么是乐观锁
乐观锁又称为“无锁,顾名思义,它是乐观派。
乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性于无锁操作中没有锁的存在,因此不可能岀现死锁的情况,也就是说乐观锁天生免疫死锁
什么是悲观锁
悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对毎次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
CAS
CAS全称是比较并交换“Compare And Swap” 在CAS中,有三个值:
- V: 要更新的值
- E: 预期的值
- N: 新值
比较并交换的过程如下判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。所以这里的预期值E本质上指的是“旧值”
当多个线程同时使用cAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作
CAS的三大问题以及解决方案
ABA 问 题 :
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存 中取出A,并且two进行了一些操作变成了B,然后two又位置的数据变成A, 这时候线程one进行CAS操作发现内存中仍然是A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。
Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
循环时间长,开销大
对 于 资 源 竞 争 严 重 ( 线 程 冲 突 严 重 ) 的 情 况 , CAS 自 旋 的 概 率 会 比 较 大 , 从 而 浪 费 更 多 的 CPU 资 源 , 效 率 低 于 synchronized。
在一次操作过程中只能保证一个共享变量的原则操作
当 对 一 个 共 享 变 量 执 行 操 作 时 , 我 们 可 以 使 用 循 环 CAS 的 方 式 来 保 证 原 子 操 作 ,但 是 对 多 个 共 享 变 量 操 作 时 , 循 环 CAS 就 无 法 保 证 操 作 的 原 子 性 , 这 个 时 候 就 可 以 用 锁 来保证原子性。
AQS(AbstractQueuedSynchronizer)
即抽象队列同步器:从字面上理解的意思是:
- 抽象: 抽象类,只实现了一些主要的逻辑,有些方法由子类来实现。
- 队列: 使用先进先出(FIFO)队列来存储数据结构。
- 同步: 实现了同步的功能
AQS是一个用来构建锁和同步器的框架,使用AQS
能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock
,Semaphore
,其他的诸如ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
等等皆是基于AQS
的。当然,我们自己也能利用AQS
非常轻松容易地构造出符合我们自己需求的同步器。
原理解析
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS 的原理图:
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
1
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
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);
}
123456789101112
AQS 对资源的共享方式
AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
- 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
123456
默认情况下,每个方法都抛出 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(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
。
实现步骤
首先
第一步使用到acquire(int arg)
方法拿到这个线程的共享资源的状态 这个状态是使用到 volatile
来修饰 只有当获取到的state大于0的时候才表示获取锁是成功的(重入一次就会加一 释放一次状态就会减一 )如果失败就会把当前线程包装成一个node节点放入到队列中(FIFO)
第二步: 在当不能够获取到状态值大于1的时候 表示没有成功获取到锁 这个时候 就会放入到队列中 使用到的是 addWaite
方法 将该线程包装成一个节点 加入到队列中 ,若是加入到队列的尾部失败 会看 这个队列是否已经初始化成功 若是成功 保证只有一个头结点是初始化成功的 没有成功 就使用 CAS 保证只用一个线程节点创建成功 最后 使用 enq方法无限自旋 知道 cas成功 返回一个节点
第三步:在完成以上以后 此时这个线程就会成功加入到等待队列中 然后进行挂起 等待被唤醒。 然后调用 boolean acquireQueued
先把锁标记为默认的false 然后 去判断当前节点的前置节点是不是头结点 是头结点 将使用到 sethead
设置成为头结点 只用头结点才是CPU正在执行的线程节点
第四步:如果前置的节点不是头结点时候
boolean shouldParkAfterFailedAcquire(Node pred, Node node)
获取到前驱节点的状态 前置节点的waitStatus
是Node.SIGNAL
则返回true,然后会执行parkAndCheckInterrupt()方法进行挂起 此时若不是时候 就会一直向后走 直到走到一个最近的正常等待的状态然后 排在她的后面
第五步: 找到以后 使用 park 进入休息的状态 有两种方法被唤醒 一种是 unpark 一种是 interrupt
第六步: 被唤醒以后 看自己是否有资格能够拿到号 表示能够进入运行的状态 就是 head指向当前节点。如果没有拿到就继续之前的操作
11. 锁接口与类
前面学习到的是 java原生的锁——基于对象的锁。一般是配合关键字synchronized 来使用。下面来学习于介绍位于 java.util.concurrent.locks
包下的几个其他的锁的类和接口
synchronized 的不足之处
- 如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行·
- synchronized无法知道线程有没有成功获取到锁
- 使用 synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。而这些都是locks包下的锁可以解决的。
锁的分类
什么是可重复锁于不可重入锁
可重入:就是说是一个支持重新进入的锁,也就是说这个锁支持一个线程对资源进行重复的加锁。
synchronized
关键字就是使用的重入锁。比如说,你在一个 synchronized
实例方法里面调用另一个本实例的 synchronized
实例方法,它可以重新进入这个锁,不会出现任何异常。
不可重复: 就是不支持重复进入的锁。不支持一个线程对资源进行重复的加锁。
ReentrantLock
就是可重入锁的代表
什么是公平锁于非公平锁
这里的“公平”,其实通俗意义来说就是“先来后到",也就是FIFO。如果对一个锁来说,先对锁获取请求的线程一定会先被满足,后对锁获取请求的线程后被满足,那这个锁就是公平的。
反之,那就是不公平的。一般情况下,非公平锁能提升一定的效率。但是非公平锁可能会发生线程饥饿(有些线程长时间得不到锁)的情况。所以要根据实际的需求来选择非公平锁和公平锁
ReentrantLock
支持非公平锁于公平锁两种
具体详解(图片来源网络):
如上可以看出公平锁与非公平锁的区别所在。
为什么效率有差异性: 对于公平锁来说,后来的线程要加上锁,即使锁处于空闲的状态, 也要检测是否还有其他的线程在等待中,如果有其他的线程还在等待,就挂起自己,然后加到队列的后面,然后唤醒的也是位于队列最前面的锁。在这样的情况下,例如一个新来的线程,在还有线程在等待时候,遇到即使锁处于空闲的状态,但是自己却不能够进行执行,先要挂起然后唤醒,但是对于一个非公平锁来说,少了这么一次的挂起与唤醒就会直接开始执行。
什么是读写锁与排他锁
对于 synchronized
用的锁和 Reentrantlock
,其实都是“排它锁"。也就是说,这些锁在同一时刻只允许一个线程进行访问而读写锁可以再同一时刻允许多个读线程访问。
Java提供了Reentrantreadwritelock
类作为读写锁的默认实现,内部维护了两个锁读锁,一个写锁。通过分离读锁和写锁,使得在“读多写少"的环境下,大大地提高了
性能注意,即使用读写锁,在写线程访问时,所有的读线程和其它写线程均被阻塞
类
ReentrantLock
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。
在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。
重入性的实现原理
要想支持重入性,就要解决两个问题:
1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。
ReentrantReadWriteLock
是ReadWriteLock 接口的JDK默认实现,与ReentrantLock的功能类似,同样是可重入的,支持公平锁与非公平锁,不同的是还可以支持“读写锁”
锁与类的总结
synchronized 和 volatile 的区别是什么?
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别
- volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
synchronized 于 lock 的区别
类别 | synchronized | lock |
---|---|---|
存在的层次来说 | java的关键字 存在于jvm层面上面 | 是一个类 |
锁的释放 | 对于 synchronized来说 其是可以自行进行释放的 而且在线程发生异常的时候 也是会出现锁的释放 | 对于 lock不会对锁进行主动的释放 需要我们 在 try catch 语句中进行捕捉 在 finally里面 进行释放 |
锁的获取 | 若是a占用了所锁 并出现了阻塞的情况的时候 线程b就会一直处于等待的状态 | 对于 lick来说 有多重获取锁的方法 并不用一直处于等待的状态 |
锁状态的判断 | 无法判断锁的状态 | 可以对锁的状态进行判断 |
性能 | 少量同步 | 大量同步 |
synchronized、volatile、CAS 比较
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
主要区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
- 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
12. 阻塞队列
什么是组塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
- 支持组塞的插入方法: 意思是 当队列满的时候,队列会组塞插入元素的线程,直到队列不满。
- 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
组塞队列常用于生产者和消费者的场景,生产者是向队列里面添加元素的线程,消费者是从队列中取元素的线程
#### 不可用时候的处理
❑ 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException ("Queuefull")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
❑ 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
❑ 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
❑ 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
注意: 如果是无界组塞队列,队列不可能会出现满的情况,所以使用 put和offer方法永远不会被阻塞,而且使用 offer方法的时候,该方法永远返回的都是true
### 阻塞队列的类型
提供了七个组塞队列:
❑ ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序,默认情况下不保证线程公平访问队列
❑ LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
❑ PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
❑ DelayQueue:一个使用优先级队列实现的无界阻塞队列。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素
DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。
❑ 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
❑ 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。
❑ SynchronousQueue:一个不存储元素的阻塞队列。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。它支持公平访问队列。默认情况下线程采用非公平性策略访问队列
❑ LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
- transfer方法
如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。transfer方法的关键代码如下。
Node pred =tryAppend(s,haveData);
return awaitMatch(s,pred,e,(how==TIMED),nanos);
第一行代码是试图把存放当前元素的s节点作为tail节点。第二行代码是让CPU自旋等待消费者消费元素。因为自旋会消耗CPU,所以自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。
- tryTransfer方法
tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。
#### LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是JDK的bug,使用时还是用带有First和Last后缀的方法更清楚。
Condition接口与实例
方法名称 | 描述 |
---|---|
void await throw InterruptedException | 当前线程进入到等待状态 直到使用当前线程来调用 signal 或中断 当前线程就可以来进入到运行的状态 并且从 await 中返回 返回还有可能是使用到 interrupt()方法 中断当前线程。如果当前线程能够从 await中返回 表示 已经获取到 condition对象锁对应的锁 |
signal | 唤醒一个等待在Condition上的线程 该线程从等待方法返回前必须获得与Condition相关联的锁。 |
下面理解到了具体的实现部分 我们来实现一个阻塞队列。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class block {
private List container =new ArrayList<>();
private volatile int size;
private volatile int capacity;
private Lock lock=new ReentrantLock();
private final Condition isnull =lock.newCondition();
private final Condition isfull =lock.newCondition();
zuse(int cap){
this.capacity=cap;
}
public void add(int data){
try{
lock.lock();
try{
while(size>=capacity){
System.out.println("阻塞队列满了");
isfull.await();// 此时若是队列满的时候 添加的add 就会被阻塞起来 为满就等待
}
}
catch (InterruptedException e){
isfull.signal();
//表示出现了异常 就会将其唤醒
e.printStackTrace();
}
++size;
container.add(data);
// 表示的是 对于 增加数据到里面时候 通知isnull 唤醒其其中的对象 可以进行数据的取用。
isnull.signal();
}finally {
lock.unlock();
}
}
public int take(){
try{
lock.lock();
try{
while(size==0){
System.out.println("阻塞队列处于空的状态");
isnull.await();
}
}
catch (InterruptedException e){
isnull.signal();
e.printStackTrace();
}
--size;
int res=container.get(0);
container.remove(0);
isfull.signal();// 表示又将数据取了出去 对于 加入又可以进行工作 然后 唤醒
return res;
}
finally {
lock.unlock();
}
}
public static void main(String[] args) {
zuse queue=new zuse(5);
Thread t1=new Thread(()->{
for(int i=0;i<100;i++){
queue.add(i);
System.out.println("加入"+i);
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
for(;;){
System.out.println("消费"+ queue.take());
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
13. ThreadLocal
概念
线程本地变量,也叫作线程的本地存储而言: 作用是 对于那些公有的变量,有时候会被很多的线程访问,这个时候就是会出现线程安全的问题,并且由于使用到 synchronized关键字进行修饰时候 并发程度会很低,不能够满足于日常的使用。于是使用threadlocal来维护变量为每一个变量在使用该变量的线程里面提供一个独立的变量副本。
### 特点
1.对于每一个线程而言都有一个属于自己的ThreadLocalMap,可以将线程自己的对象保存到其中,各管各的,这样就可以正确访问到自己的对象。
- 将一个公用的ThradLocal 静态实例作为key,将不同对象的引用保存到不同线程的TheadlocalMap中,然后在线程执行的各处通过这个静态的ThreadLocal实例的get()方法获取得道自己线程保存的那个对象,避免了将这个对象最为参数传递的麻烦,也就避免对这个值进行直接更改的麻烦。
- 其实这个ThreadLocalMap就是线程里面的一个对象,也是ThreadLocal里面的一个内部类。在Thrad类中进行定义
ThreadLocal.ThreadLocalMop threadLocals =null;
- 每一个ThreadLocal对象都有一个唯一的ID
- 在我们访问ThreadLocal中的变量的时候,利用这个唯一的值 去本地线程ThreadLocalMap中查找对应的值
### 具体使用(源码解析)
set函数的使用
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
/// 可以看到在ThreadLocal创建的时候, 是获取到了当前线程t, 然后获取线程t的本地存储ThreadLcoalMap,然后对map进行操作,看map是否存在。
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// map存在时候 将key为此当前线程 value 为想要放入的值
map.set(this, value);
else
/// 当然如果当前线程还没有创建过ThreadLocalMap,则创建Map
createMap(t, value); /// 创建Map 的过程 看下边ThreadLocalMap 中方法。
}
#### get方法
/**
* 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);
/// 同样是获取到了线程内部的引用 map对象, 通过map内部getEntry方法 获取到该ThreadLocal对应的对象Entry
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果没有, 默认为null, 所以在没有set 而且没有重写initialValue方法的话,获取到的值就是null。
return setInitialValue();
}
/**
* 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
/// 如果还没有该map ,则使用当前初始值来创建。默认为null 和初始的创建是相同的 。
createMap(t, value);
return value;
}
/**
* 返回为当前线程创建的初始化值使用, 一般在get方法中调用和 remove方法后get调用,
* 如果使用可以通过内部类继承然后重写该方法即可。
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
* 获取到Thread的内部的ThreadLocalMap类的引用对象 threadLcoals
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
#### remove 方法
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
这里的entry类型的数组对象会在后面讲到 是如何建立的,这里也就是简单的移除 也就不用大费周章去讲解了
*/
private void remove(ThreadLocal> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocal 与 Thread的关系
说起ThreadLocal 与Thread的关系,借用Java编程思想中对线程本地存储的定义,防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。 简单理解一下就是Thread上的LocalVariables了, 既然如此就进入Thread源码看一下。 很明显就找到了相关引用。
/* Thread中引用了 ThreadLocal.ThreadLocalMap 很明显一线程中可以放置很多变量 , 这个Map可以在ThreadLocal中维护 */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal 可继承的ThreadLcoal
这个Map可以在ThreadLocal中维护
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocalMap
前面讲到了 在set与get过程中都有用到 ThreadLocalMap下面也从具体的方法中进行理解:
### 基础的结构
继承自弱应用WeakReference ,使用了 泛型ThreadLocal
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
Create方法
在ThreadLocal的 set方法中,若是对应的map不存在,表示此线程时第一次执行set方法,就需要我们维护一个Entry类型的table
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
/// 创建一个线性table , Entry 为item ,初始化INITIAL_CAPACITY 默认为16个
table = new Entry[INITIAL_CAPACITY];
/// 计算出第一个key的索引 i
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
/// 初始化 并设置size 值
table[i] = new Entry(firstKey, firstValue);
size = 1;
/// 同时 设置容器大小的临界值,并传入初始化大小。
setThreshold(INITIAL_CAPACITY);
}
get方法
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
* /// 通过Key的hash值来创建其对应的索引值,找到entry
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
/// 如果没有在直接的table线性表中找到的话。
return getEntryAfterMiss(key, i, e);
}
可以看出来的是也是会考虑到哈希碰撞的问题,因为在set值的过程中可能会出现在对于一个线程来说,一个节点上面会对应两个值,一般会取(key的hashcode的值& table.length-1)获取一个数组的位置,将其放入到该节点的位置。这里相当于是一个逆运算,直接取到该节点上的值。
下面来具体讲解一个对于hash碰撞是如何使用 getEntryAfterMiss 方法来解决:
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
return e;
// 如果 k为空的直接从链表中擦除 方便GC进行回收
if (k == null)
expungeStaleEntry(i);
else
//循环找到下一个index
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
Set方法
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
//如果键和传入的键相同 则覆盖
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//如果数组中没有冗余的null值并且如果size大于临界值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
/// 进行扩容 ,这里就不再进行解释如何扩容了。
rehash();
}
remove 方法
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
// 置空,以便GC回收
expungeStaleEntry(i);
return;
}
}
}
散列
通过上面了解到了ThreadLocalMap的引用以及 get和set的问题,此时就出现了一个问题如何保证每个线程中引用ThreadLocalMap中创建的ThreadLocal是唯一的,并且进行高效的存取就成了一个至关重要的问题。
我们会发现对于上面所讲到的set和get方法都有用到threadLocalHashCode 其实这里的散列的方法和HashMap1的类似。但是这个的Hash key生成器不是 ThreadLocal对象的Hash值,而是 从0开始的AtomicInteger 通过 getAndAdd HASH_INCREMENT 来生成的
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
这样 就保证了每一个ThreadLocal的对象都有一个唯一的ID信息
- 关于为什么要使用
0x61c88647 这里具体讲解的大概是 黄金分割比例与斐波那契数列的相关内容,为了能够 让生成的 hashcode尽可能的分布在大小为2的N次方的数组里面具体讲解
总结
首先对于我们来说,ThreadLocal并不是用来解决共享变量的多线程的访问,而是说 通过 ThreadLocal.set()到线程中的对象是该线程自己使用的对象,其他线程时不能够访问到的,各个线程访问到的是不同的对象。另外说 ,ThreadLocal使得各个线程能够保持各自独立的对象,并不是通过 set() 方法来实现的,而是通过每个线程中的new对象的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过Thr.srt()将这个新创建的对象的引用保存到各线程的自己的map中,每个线程都有这样属于自己的mao,执行get() 方法的时候,各线程从自己的map中取出放进去的对象。因此取出的是各自自己线中的对象。ThreadLocal实例是最为map的key来使用
- 如何使用ThreadLocal为每一个线程创建变量的副本:
- 首先 在每个线程Thread内部有一个ThreadLocal。ThreadLocalMap类型的成员变量 threadLocals,这个threadLocals 就是用来存储实际变量的副本的,键值为当前ThreadLocal变量,value为变量的副本(即T类型的变量)
- 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
- 为什么ThreadLocals 的类型ThreadLocalMap1的键值为ThreadLocal对象,是因为每个线程中可有多个threadLocal 变量,就是说可以使用ThreadLocal创建多个实例变量。
- 我们在使用到 get之前必须先试用到set,否则会出现空指针异常:
使用
可以使用到数据库的连接和Session管理等。
private static ThreadLocal connectionHolder
= new ThreadLocal() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
Session 管理
private static final ThreadLocal threadSession = new ThreadLocal();
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的了解还不算是很深刻,主要是对于多线程自己使用到的太少太少了 多线程要多加实践
1 .其他
线程安全
非线程安全:多个线程同时对对象中的同一个实例变量进行操作时会出现值被更改,值不同步的情况,进而影响程序的执行流程。
线程安全:是多线程在访问时候,采用加锁机制,当一个线程访问某个类的数据的时候对其进行保护,其他线程不能访问该线程直到该线程读取完成以后 其他的线程才能对其进行访问 不会出现数据的不一致性或则数据污染。
区别:非线程安全是指多线程操作同一个对象可能会出现问题,而线程安全则是多线程在操作同一个对象的时候不会出现问题。对于线程的安全是通过线程同步控制来实现的 也就是 synchronized 非线程安全是 通过异步实现
死锁与活锁
区别
死锁:是 指 两 个 或 两 个 以 上 的 进 程 ( 或 线 程 ) 在 执 行过程中 , 因争 夺 资 源 而 造成的 一 种 互 相 等 待 的 现 象 , 若 无 外 力 作 用 , 它 们 都 将 无 法 推 进 下 去 。
四个必要的条件:
1、互 斥 条 件 : 所谓互斥 就 是 进 程 在 某 一 时 间 内 独 占 资 源 。
2、 请 求 与 保 持 条 件 : 一 个 进 程 因 请 求 资 源 而 阻 塞 时 , 对 已 获 得 的 资 源 保 持 不 放 。
3、 不 剥 夺 条 件 :进 程 已 获 得 资 源 , 在 末 使 用 完 之 前 , 不 能 强 行 剥 夺 。
4、 循 环 等 待 条 件 :若 干 进 程 之 间 形 成 一 种 头 尾 相 接 的 循 环 等 待 资 源 关 系 。
活 锁 : 任 务 或 者 执 行 者 没 有 被 阻 塞 , 由 于 某 些 条 件 没 有 满 足 , 导 致 一 直 重 复 尝 试 ,
失 败 , 尝 试 , 失 败 。
区别:处 于 活锁 的 实体 是 在不 断 的改 变 状态 , 所谓 的 “活 ”, 而处 于 死 锁 的 实 体 表 现 为 等 待 ; 活 锁 有 可 能 自 行 解 开 , 死 锁 则 不 能 。
死锁与饥饿的区别
一 个 或 者 多 个 线 程 因 为 种 种 原 因 无 法 获 得 所 需 要 的 资 源 , 导 致 一 直 无 法 执行 的 状 态 。
Java 中 导 致 饥 饿 的 原 因 :
1、 高 优 先 级 线 程 吞 噬 所 有 的 低 优 先 级 线 程 的 CPU 时 间 。
2、 线 程 被 永 久 堵 塞 在 一 个 等 待 进 入 同 步 块 的 状 态 , 因 为 其 他 线 程 总 是 能 在 它 之 前
持 续 地 对 该 同 步 块 进 行 访 问 。
3、 线 程 在 等 待 一 个 本 身 也 处 于 永 久 等 待 完 成 的 对 象 (比 如 调 用 这 个 对 象 的 wait 方
法 ), 因 为 其 他 线 程 总 是 被 持 续 地 获 得 唤 醒 。
5、Java 中用到的线程调度算法是什么?
采 用 时 间 片 轮 转 的 方 式 。 可 以 设 置 线 程 的 优 先 级 , 会 映 射 到 下 层 的 系 统 上 面 的 优
先 级 上 , 如 非 特 别 需 要 , 尽 量 不 要 用 , 防 止 线 程 饥 饿
上下文
首先 明白什么是上下文;
对于每个任务运行前,CPU都需要知道任务是从哪里加载的,又是从哪里开始运行的,就涉及到CPU寄存器和程序计数器。
cpu的寄存器是cpu中内置容量小,但是速度较快的内存。
程序计数器是会存储cpu正在执行令的位置 或是即将执行的指令的位置。
上下文切换
- 将当前cpu的上下文 (就是说 cpu寄存器和程序计数器里面的内容)保存起来。
- 然后加载新任务的上下文 cpu寄存器和程序计数器。
- 最后跳到程序计数所指向的位置 运行新任务。
- 被保存起来的上下文会存储到系统的内核中 等待任务重新调度指向时候 再次加载进来。、
上下文切换分为 线程 进程 和中断上下文。
线程上下文:
线程是调度的基本单位,而进程则是资源进行分配和拥有的基本单位。
内核中的任务的调度其实是在调度线程,进程只是给线程提供虚拟的内存全局变量等资源。线程在进行上下文的切换的时候 共享相同的虚拟内存和全局变量等资源不需要进行修改 但是对于 线程自己私有的数据 如 栈和寄存器要进行修改,
线程上下文切换的时候 分为两种的情况,就是 对于 两个线程属于不同的进程 两个线程属于相同的进程。
进程的上下文切换
进程是有内核管理和调度的 所以说 对于 进程的上下文切换 只会发生在 内核态 因此来说 进程的上下文切换 不但会包括 虚拟内存 栈 全局变量等 用户资源 还包括扩 内核堆栈 寄存器等 内核空间的状态。
所以来说 对于 进程的上下文切换 会比系统调用多一个步骤:
保存当前进程的内核状态和CPU寄存器之前 先把该进程的虚拟内存 栈保存起来 加载下一个进程的内核以后 还要刷新 进程的虚拟内核和用户栈。
保存上下文和恢复上下文需要内核在PUC上运行才能够完成
中断上下文切换
为了快速响应硬件的事件 中断处理会打断进程的正常调度和执行然后调用中断来处理程序 响应请求时间 在打断其他进程的运行的时候 也需要将之前进程的运行的情况保存下来 然后 等到中断结束以后 进程仍然可以恢复到原来的状态
对同一个cpu来说 中断处理比进程拥有更高的优先级 所以中断上下文切换不会与进程上下文切换同时发生