最近自己在整理关于并发编程相关的知识点,要细致的了解每个知识背后产生的原因和相关处理并发的底层原理,确实还是比较需要时间好好消化的,现在对于一般的知识点一般我会几个方面着手去了解,从几个方面看,分别是:原理、使用场景、优缺点、产生的原因、代码接入实例,废话少说想在就带大家去了解java并发相关的知识,本人计划使用三篇文章把相关目录的知识点汇总出来。
并发编程相关知识点汇总:
一、为什么出现多线程并发问题
1.1、Java内存模型介绍
1.2、java对象的组成(对象头,实例数据,填充区域)
二、线程安全常见的关键字使用详解
2.1、synchronized
2.2、lock
2.3、Atomic
2.4、volatile
2.5、Threadlocal详解
三、Java并发编程中常用的类和集合
四、线程间的协作(wait/notify/sleep/yield/join)
五、线程安全的级别
六、并发编程常见问题汇总
七:扩展阅读
一、为什么出现多线程并发问题
并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能。它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰、思维缜密,这样才能写出高效、安全、可靠的多线程并发程序。
图解:
一、共享性
数据共享性是线程安全的主要原因之一。如果所有的数据只是在线程内有效,那就不存在线程安全性问题,这也是我们在编程的时候经常不需要考虑线程安全的主要原因之一。但是,在多线程编程中,数据共享是不可避免的。最典型的场景是数据库中的数据,为了保证数据的一致性,我们通常需要共享同一个数据库中数据,即使是在主从的情况下,访问的也同一份数据,主从只是为了访问的效率和数据安全,而对同一份数据做的副本。
二:互斥性
资源互斥是指同时只允许一个访问者对其进行访问,具有唯一性和排它性。我们通常允许多个线程同时对数据进行读操作,但同一时间内只允许一个线程对数据进行写操作。所以我们通常将锁分为共享锁和排它锁,也叫做读锁和写锁。如果资源不具有互斥性,即使是共享资源,我们也不需要担心线程安全。例如,对于不可变的数据共享,所有线程都只能对其进行读操作,所以不用考虑线程安全问题。但是对共享数据的写操作,一般就需要保证互斥性,上述例子中就是因为没有保证互斥性才导致数据的修改产生问题。Java中提供多种机制来保证互斥性,最简单的方式是使用Synchronized。
三、原子性
原子性就是指对数据的操作是一个独立的、不可分割的整体。换句话说,就是一次操作,是一个连续不可中断的过程,数据不会执行的一半的时候被其他线程所修改。保证原子性的最简单方式是操作系统指令,就是说如果一次操作对应一条操作系统指令,这样肯定可以能保证原子性。但是很多操作不能通过一条指令就完成。例如,对long类型的运算,很多系统就需要分成多条指令分别对高位和低位进行操作才能完成。还比如,我们经常使用的整数i++的操作,其实需要分成三个步骤:(1)读取整数i的值;(2)对i进行加一操作;(3)将结果写回内存。这个过程在多线程下就可能出现如下现象:
这也是代码段一执行的结果为什么不正确的原因。对于这种组合操作,要保证原子性,最常见的方式是加锁,如Java中的Synchronized或Lock都可以实现,代码段二就是通过Synchronized实现的。除了锁以外,还有一种方式就是CAS(Compare And Swap),即修改数据之前先比较与之前读取到的值是否一致,如果一致,则进行修改,如果不一致则重新执行,这也是乐观锁的实现原理。不过CAS在某些场景下不一定有效,比如另一线程先修改了某个值,然后再改回原来值,这种情况下,CAS是无法判断的。
四、可见性
要理解可见性,需要先对JVM的内存模型有一定的了解,JVM的内存模型与操作系统类似,如图所示:
从这个图中我们可以看出,每个线程都有一个自己的工作内存(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),对于共享变量,线程每次读取的是工作内存中共享变量的副本,写入的时候也直接修改工作内存中副本的值,然后在某个时间点上再将工作内存与主内存中的值进行同步。这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。
五、有序性
为了提高性能,编译器和处理器可能会对指令做重排序。重排序可以分为三种:
1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
先看上图中的(1)源码部分,从源码来看,要么指令1先执行要么指令3先执行。如果指令1先执行,r2不应该能看到指令4中写入的值。如果指令3先执行,r1不应该能看到指令2写的值。但是运行结果却可能出现r2==2,r1==1的情况,这就是“重排序”导致的结果。上图(2)即是一种可能出现的合法的编译结果,编译后,指令1和指令2的顺序可能就互换了。因此,才会出现r2==2,r1==1的结果。Java中也可通过Synchronized或Volatile来保证顺序性。
备注:具体深入了解参考本人另外一篇博文(Java内存模型详解)
java对象的组成
Java对象保存在内存中时,由以下三部分组成:
1、对象头
2、实例数据
3、对齐填充字节
1.1、对象头
对象头也由以下三部分组成:
1、Mark Word
2、指向类的指针
3、数组长度(只有数组对象才有)
1、Mark Word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
2、指向类的指针该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Java对象的类数据保存在方法区。
3、数组长度只有数组对象保存了这部分数据。该数据在32位和64位JVM中长度都是32bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
JVM一般是这样使用锁和Mark Word的:
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
Java对象剩余的两个部分:
1、实例数据对象的实例数据就是在java代码中能看到的属性和他们的值。
2、对齐填充字节因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
二、线程安全常见的关键字使用
线程安全涉及的关键字:
1、synchronized
2、lock
3、Atomic
4、volatile
5、threadlocal
Synchronized详解:
基本使用:
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:
1、确保线程互斥的访问同步代码
2、保证共享变量的修改能够及时可见
3、有效解决重排序问题。
从语法上讲,Synchronized总共有三种用法:
1、修饰普通方法(方法锁)
2、修饰静态方法(类锁)
3、修饰代码块(对象锁)
修饰普通方法:
packagecom.paddx.test.concurrent;
public classSynchronizedTest{
public synchronized void method1(){
System.out.println("Method 1 start");
try {
System.out.println("Method 1 execute");
Thread.sleep(3000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public synchronized void method2(){
System.out.println("Method 2 start");
try {
System.out.println("Method 2 execute");
Thread.sleep(1000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public static void main(String[]args) {
finalSynchronizedTesttest = newSynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test.method2();
}
}).start();
}
}
总结:通过在方法声明中加入synchronized关键字来声明synchronized方法。synchronized方法锁控制对类成员变量的访问:每个类实例对应一把锁每个synchronized方法都必须获得调用该方法的类实例的”锁“方能执行,否则所属线程阻塞。方法一旦执行,就会独占该锁,一直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,从而重新进入可执行状态。这种机制确保了同一时刻对于每一个类的实例,其所有声明为synchronized的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。
修饰静态方法(类)同步:
packagecom.paddx.test.concurrent;
public classSynchronizedTest{
public static synchronized void method1(){
System.out.println("Method 1 start");
try {
System.out.println("Method 1 execute");
Thread.sleep(3000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public static synchronized void method2(){
System.out.println("Method 2 start");
try {
System.out.println("Method 2 execute");
Thread.sleep(1000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public static void main(String[]args) {
finalSynchronizedTesttest = newSynchronizedTest();
finalSynchronizedTesttest2 = newSynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test2.method2();
}
}).start();
}
}
总结:对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以即使test和test2属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以也只能顺序的执行method1和method2,不能并发执行。
对象锁是用来控制实例方法之间的同步,而类锁是用来控制静态方法(或者静态变量互斥体)之间的同步的。
修饰代码块:
packagecom.paddx.test.concurrent;
public classSynchronizedTest{
public void method1(){
System.out.println("Method 1 start");
try {
synchronized (this) {
System.out.println("Method 1 execute");
Thread.sleep(3000);
}
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public void method2(){
System.out.println("Method 2 start");
try {
synchronized (this) {
System.out.println("Method 2 execute");
Thread.sleep(1000);
}
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public static void main(String[]args) {
finalSynchronizedTesttest = newSynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test.method2();
}
}).start();
}
}
总结:当一个对象中有synchronized method或synchronized block的时候,调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放。(方法锁也是对象锁)java的所有对象都含有一个互斥锁,这个锁由jvm自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,jvm会自动释放对象锁。这里也体现了用synchronized来加锁的一个好处,即 :方法抛异常的时候,锁仍然可以由jvm来自动释放。
Synchronized原理:
Synchronized是如何实现对代码块进行同步的。
monitorenter:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
1、执行monitorexit的线程必须是objectref所对应的monitor的所有者。
2、指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
Synchronized是如何实现同步方法的:
方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
总结:Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。
Synchronized底层优化:
简介:Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的MutexLock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统MutexLock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
二、轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
1、轻量级锁的加锁过程
1、在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。
2、拷贝对象头中的Mark Word复制到锁记录中。
3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。
5、如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁CAS操作之前堆栈与对象的状态:
轻量级锁CAS操作之后堆栈与对象的状态:
轻量级锁的解锁过程:
1、通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
2、如果替换成功,整个同步过程就完成了。
3、如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
三、偏向锁
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
偏向锁获取过程:
1、访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
2、如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
3、如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
4、如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
5、执行同步代码。
偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
重量级锁、轻量级锁和偏向锁之间转换:
四、其他优化
一、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。
问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。
解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
二、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。
这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
三、锁消除(Lock Elimination):锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
总结:JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。
对比图:
Lock详解:
Lock是一个接口,它主要由下面这几个方法:
public interface Lock {
void lock();
voidlockInterruptibly() throwsInterruptedException;
booleantryLock();
booleantryLock(long time,TimeUnitunit) throwsInterruptedException;
void unlock();
ConditionnewCondition();
}
lock():lock方法可能是平常使用最多的一个方法,就是用来获取锁。如果锁被其他线程获取,则进行等待。
如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。
Locklock= ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock():方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time,TimeUnitunit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
Locklock= ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly() :此方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
一般形式如下:
public void method() throwsInterruptedException{
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被释放,防止死锁的发生。
注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
Java中15种锁的介绍:
1、公平锁/非公平锁
2、可重入锁/不可重入锁
3、独享锁/共享锁
4、互斥锁/读写锁
5、乐观锁/悲观锁
6、分段锁
7、偏向锁/轻量级锁/重量级锁
8、自旋锁
上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。
一:公平锁和非公平锁
公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
解析:
对于JavaReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
备注:在AQS中维护了一个private volatileintstate来计数重入次数,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。
二:可重入锁和不可重入锁
可重入锁:广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁。
例子:
synchronized voidsetA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized voidsetB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
不可重入锁:不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。
三:独享锁和共享锁
独享锁和共享锁在你去读C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就会发现,它俩一个是独享一个是共享锁。
独享锁:该锁每一次只能被一个线程所持有。
共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。
另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享,对于Synchronized而言,当然是独享锁。
四:互斥锁和读写锁
互斥锁:
1、在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
2、如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被变成就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
读写锁:
1、读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。
2、读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态
3、读写锁在Java中的具体实现就是ReadWriteLock
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。
五:乐观锁和悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
六:分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一。
在并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会减低性能。在锁上发生竞争时将导致这两种问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式—-每次只能有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。
我们一般有三种方式降低锁的竞争程度:
1、减少锁的持有时间
2、降低锁的请求频率
3、使用带有协调机制的独占锁,这些机制允许更高的并发性。
在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。
其实说的简单一点就是:
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到原来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。
七:偏向锁、轻量级锁、重量级锁
锁的状态:
1、无锁状态
2、偏向锁状态
3、轻量级锁状态
4、重量级锁状态
锁的状态是通过对象监视器在对象头中的字段来表明的。
四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)。
偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级:轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
八:自旋锁
我们知道CAS算法是乐观锁的一种实现方式,CAS算法中又涉及到自旋锁,所以这里给大家讲一下什么是自旋锁。
简单回顾一下CAS算法:CAS是英文单词Compare and Swap(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数
1、需要读写的内存值V
2、进行比较的值A
3、拟写入的新值B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
Java如何实现自旋锁:
例子:
public classSpinLock{
privateAtomicReference<Thread>cas= newAtomicReference<Thread>();
public void lock() {
Thread current =Thread.currentThread();
//利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current =Thread.currentThread();
cas.compareAndSet(current, null);
}
}
解析:Lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
自旋锁存在的问题:
1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁的优点:
1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
可重入的自旋锁和不可重入的自旋锁:
解析:当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。
而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。
为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。
例子:
public classReentrantSpinLock{
privateAtomicReference<Thread>cas= newAtomicReference<Thread>();
privateintcount;
public void lock() {
Thread current =Thread.currentThread();
if (current ==cas.get()) { //如果当前线程已经获取到了锁,线程数增加一,然后返回
count++;
return;
}
//如果没获取到锁,则通过CAS自旋
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread cur =Thread.currentThread();
if (cur ==cas.get()) {
if (count > 0) {//如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
count--;
} else {//如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
cas.compareAndSet(cur, null);
}
}
}
}
自旋锁与互斥锁
1、自旋锁与互斥锁都是为了实现保护资源共享的机制。
2、无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
3、获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。
自旋锁总结
1、自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
2、自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
3、自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
4、自旋锁本身无法保证公平性,同时也无法保证可重入性。
5、基于自旋锁,可以实现具备公平性和可重入性质的锁。
Synchronzied和Lock的主要区别如下:
1、存在层面:Syncronized是Java中的一个关键字,存在于JVM层面,Lock是Java中的一个接口。
2、锁的释放条件:
获取锁的线程执行完同步代码后,自动释放;
2.线程发生异常时,JVM会让线程释放锁;Lock必须在finally关键字中释放锁,不然容易造成线程死锁。
3、锁的获取:在Syncronized中,假设线程A获得锁,B线程等待。如果A发生阻塞,那么B会一直等待。在Lock中,会分情况而定,Lock中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待。
4、锁的状态:Synchronized无法判断锁的状态,Lock则可以判断。
5、锁的类型:
5.1、Synchronized是可重入,不可中断,非公平锁。
5.2、Lock锁则是可重入,可判断,可公平锁。
6、锁的性能:Synchronized适用于少量同步的情况下,性能开销比较大。
Lock锁适用于大量同步阶段:
6.1、Lock锁可以提高多个线程进行读的效率(使用readWriteLock)
6.2、在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
6.3、ReetrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。
Atomic的使用:
简介:J2SE 5.0提供了一组atomic class来帮助我们简化同步处理。
基本工作原理是使用了同步synchronized的方法实现了对一个long, integer,对象的增、减、赋值(更新)操作. 比如对于++运算符AtomicInteger可以将它持有的integer能够atomic地递增。在需要访问两个或两个以上atomic变量的程序代码(或者是对单一的atomic变量执行两个或两个以上的操作)通常都需要被synchronize以便两者的操作能够被当作是一个atomic的单元。
AtomicInteger使用:
1、多个线程访问同一个整型数值;
2、自动增加/减小值;
3、经常作为流水值使用;
4、线程安全,使用原子锁;
5、包名java.util.concurrent.atomic, 该包名下包含其它同步数值类AtomicBoolean、AtomicLong等;
6、常用方法:get()、set()、getAndIncrement()、getAndDecrement();
下面通过简单的两个例子的对比来看一下AtomicInteger的强大的功能
例子一:
class Counter {
private volatileintcount = 0;
public synchronized void increment() {
count++; //若要线程安全执行count++,需要加锁
}
publicintgetCount() {
return count;
}
}
例子二:
class Counter {
privateAtomicIntegercount = newAtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
publicintgetCount() {
returncount.get();
}
}
总结:那么为什么不使用记数器自加呢,例如count++这样的,因为这种计数是线程不安全的,高并发访问时统计会有误,而AtomicInteger为什么能够达到多而不乱,处理高并发应付自如呢?
这是由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。Java.util.concurrent中实现的原子操作类包括:AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference。
Volatile关键字:
简述:Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
而声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步,底层基于C++的volatile实现,因为volatile自带了编译器屏障的功能,总能拿到内存中的最新值。
当一个变量定义为volatile之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“loadaddl$0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
例子:
public volatileintcount = 0;
public intTestVolatile() {
finalCountDownLatchcountDownLatch= newCountDownLatch(1000);
for (inti= 0;i< 1000;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedExceptione) {
}
increase();
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println("<<<<<" + count);
return count;
}
public void increase() {
count++;
}
出现异常的原因分析:
来看看count++操作的时候内存都做了什么操作:
1、从主内存里面(栈)读取到count的值,到CPU的高速缓存当中。
2、寄存器对count的值进行加一操作。
3、将CPU的高速缓存当中的count值刷新到主内存(栈)当中。
我们可以看到++这个操作非原子,先读count,然后+1, 最后再写count。
如果变量count被使用了volatile修饰,那么在thread1中,当count变为3的时候,就会强制刷新到主存。如果这个时候,thread2已经将count =2从从主存映射到缓存上并且已经做完了自增操作,此时count =3,那么最终主存中count值为3。
所以,如果我们想让count的最终值是4,仅仅保证可见性是不够的,还得保证原子性。也就是对于变量count的自增操作加锁,保证任意一个时刻只有一个线程对count进行自增操作。可以说volatile是一种“轻量级的锁”,它能保证锁的可见性,但不能保证锁的原子性。
例子二:
//线程1
volatilebooleanstop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
没有出现异常的原因:
1、使用volatile关键字会强制将修改的值立即写入主存;
2、使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效,也就是执行线程1的CPU缓存中的stop无效。
3、由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值,那么线程1读取到的就是最新的正确的值。
volatile和synchronized区别:
1、volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2、volatile仅能使用在变量级别,synchronized则可以使用在变量,方法。
3、volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性,《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性。
4、volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
5、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
6、使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。
ThreadLocal关键字:
共享变量一直是并发中的老大难问题,每个线程都对它有操作权,所以线程之间的同步很关键,锁也就应运而生。这里换一个思路,是否可以把共享变量私有化?即每个线程都拥有一份共享变量的本地副本,每个线程对应一个副本,同时对共享变量的操作也改为对属于自己的副本的操作,这样每个线程处理自己的本地变量,形成数据隔离。事实上这就是ThreadLocal了。
使用场景:
1、ThreadLocal最适合的是变量在线程间隔离而在方法或类间共享的场景。
2、每个线程需要自己独立的实例且该实例需要在多个方法中使用。
输出结果:
Thread[Thread-1,5,main]====57
Thread[Thread-0,5,main]====75
创建了两个线程,它们都在threadlocal上面都set了一个随机数,我们看最后的输出结果每个都是不同的值,那么我们如果把threadlocal替换成一个集合会发生什么,由于两个线程时上个线程生成的随机数57会被第二个线程覆盖掉,而在Threadlocal中两个线程都是操作的自己的本地副本,那么两个线程互不影响都无法操控到对方的数据,因此它们存取的都是不同的值。
实现原理:
ThreadLocal.ThreadLocalMapthreadLocals= null;
ThreadLocal.ThreadLocalMapinheritableThreadLocals= null;
我们调用ThreadLocal的set或者get才会真正创建他们,也就是你以为你把变量交给ThreadLocal了,其实这小子转手就给ThreadLocalMap了,ThreadLocal就是套在ThreadLocalMap外面的一层壳而已。
ThreadLocal的组成如下:
解析:可以看出,就跟map基本一样,key是ThreadLocal的引用,value则是由开发者设置,即本地变量。
ThreadLocal函数原理:
Set函数:
public void set(T value) {
Thread t =Thread.currentThread();
ThreadLocalMapmap =getMap(t);
if (map != null)
//这里this是ThreadLocal的实例引用
map.set(this, value);
else
createMap(t, value);
}
voidcreateMap(Thread t, TfirstValue) {
t.threadLocals= newThreadLocalMap(this,firstValue);
}
解析:
1、首先获取当前线程
2、以当前线程为key去查找当前线程的map即threadLocals变量
3、如果threadLocals不为空,就把ThreadLocal引用作为key,value传给map
4、否则创建map,也就是初始化当前线程的threadLocals变量
备注:再次注意值存放的实际位置是Thread中的ThreadLocalMap变量,ThreadLocalMap是一个map,key是ThreadLocal的实例引用,值则是我们要存的变量.
Get函数:
public T get() {
Thread t =Thread.currentThread();
ThreadLocalMapmap =getMap(t);
if (map != null) {
ThreadLocalMap.Entrye =map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
returnsetInitialValue();
}
解析:同样道理,先得到当前线程,然后得到成员变量threadLocals,如果threadLocals不为空,返回本地变量对应值,否则初始化threadLocals。
初始化threadLocals函数:
private TsetInitialValue() {
T value =initialValue();
Thread t =Thread.currentThread();
ThreadLocalMapmap =getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected TinitialValue() {
return null;
}
解析:判断当前threadLocals是否为空,如果不为空,设置当前ThreadLocal的实例引用对应变量为null,否则调用createMap创建threadLocals变量。
remove()函数:
public void remove() {
ThreadLocalMapm =getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
解析:如果threadLocals变量不为空,删掉map中当前ThreadLocal对应实例引用的本地变量。
ThreadLocal的内存溢出问题:
解析:
从上边一路走下来我们应该了解了,每一个线程中都有一个ThreadLocalMap
类型的threadLocals变量,这个map中key为ThreadLocal的实例引用,value为对应的本地变量。如果这个线程不消亡,开发者也没有采用remove操作及时清除掉不再使用的变量,这些变量就会一直存在map中,直到撑爆你的内存,造成内存溢出问题。
总结:
1、ThreadLocal并不解决线程间共享数据的问题。
2、ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题。
3、每个线程持有一个Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。
4、每个线程对应一个ThreadLocalMap,ThreadLocal其实就是套在ThreadLocalMap上的一层壳。
5、ThreadLocalMap的key是ThreadLocal的实例引用,value是我们像设置的本地变量。
6、若是线程一直不停,threadLocalMap中的本地变量就会越来越多,注意及时remove掉不再使用的变量,防止内存溢出。
7、ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题。
8、ThreadLocalMap的set方法通过调用replaceStaleEntry方法回收键为null的Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏。
9、ThreadLocal适用于变量在线程间隔离且在方法间共享的场景。
今天大概就说这么多,后续的关于并发相关的知识点会再分两篇文章总结,一共是三篇文章。
七:扩展阅读
1、https://www.xuebuyuan.com/3253276.html(synchronized的4种用法)
2、一文带你彻底搞懂ThreadLocal(微信公众号)
3、https://blog.csdn.net/javazejian/article/details/72772461(全面理解Java内存模型(JMM)及volatile关键字)
4、https://juejin.im/post/5d2c97bff265da1bc552954b(图解Java线程安全)
5、https://www.cnblogs.com/paddix/p/5367116.html(Java并发编程:Synchronized及其实现原理,系列)
6、https://www.jianshu.com/p/cfac5c131a9b(面试字节跳动Android研发岗,已拿到offer,这些知识点该放出来了)
7、http://www.infoq.com/cn/articles/java-se-16-synchronized(聊聊并发(二)——Java SE1.6中的Synchronized)
8、https://blog.csdn.net/niuwei22007/article/details/51433669(synchronized的JVM底层实现(很详细 很底层))
9、https://blog.csdn.net/qq_22771739/article/details/82529874(Java线程的6种状态及切换(透彻讲解))
10、https://blog.csdn.net/lkforce/article/details/81128115(Java的对象头和对象组成详解)
11、Synchronized和Lock的区别和使用场景(微信公众号)
12、一文带你理解Java中Lock的实现原理(微信公众号)
13、https://www.cnblogs.com/0616--ataozhijia/p/6869657.html(AtomicInteger的用法)
14、面试必问的volatile,你了解多少 (微信公众号)
15、Java中Volatile关键字详解(微信公众号)
16、https://segmentfault.com/a/1190000017766364(Java中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等)
17、ThreadLocal到底是什么?它解决了什么问题(微信公众号)
18、Java并发集合的实现原理(微信公众号)
19、https://blog.csdn.net/jackyrongvip/article/details/89472397(笔记:Collections的synchronized XXX方法)
20、https://blog.csdn.net/qq_34039315/article/details/78549311(Java并发编程75道面试题及答案——稳了)
网友评论