首页IT科技java中的线程安全(万字详解 Java 线程安全,面试必备!)

java中的线程安全(万字详解 Java 线程安全,面试必备!)

时间2025-08-03 21:08:04分类IT科技浏览7695
导读:来源:blog.csdn.net/u014454538/article/details/98515807...

来源:blog.csdn.net/u014454538/article/details/98515807

1. Java中的线程安全

Java线程安全:狭义地认为是多线程之间共享数据的访问                。 Java语言中各种操作共享的数据有5种类型:不可变                、绝对线程安全                        、相对线程安全        、线程兼容        、线程独立

① 不可变

不可变(Immutable) 的对象一定是线程安全的                ,不需要再采取任何的线程安全保障措施                        。 只要能正确构建一个不可变对象                        ,该对象永远不会在多个线程之间出现不一致的状态        。 多线程环境下        ,应当尽量使对象成为不可变                ,来满足线程安全        。

如何实现不可变?

如果共享数据是基本数据类型                        ,使用final关键字对其进行修饰        ,就可以保证它是不可变的                        。 如果共享数据是一个对象        ,要保证对象的行为不会对其状态产生任何影响                。 String是不可变的                        ,对其进行substring()                        、replace()                、concat()等操作                ,返回的是新的String对象        ,原始的String对象的值不受影响        。而如果对StringBuffer或者StringBuilder对象进行substring()        、replace()                        、append()等操作                        ,直接对原对象的值进行改变                        。 要构建不可变对象                ,需要将内部状态变量定义为final类型                。如java.lang.Integer类中将value定义为final类型。

Java 面试题最全整理:https://www.javastack.cn/mst/

private final int value;

常见的不可变的类型:

final关键字修饰的基本数据类型 枚举类型                、String类型 常见的包装类型:Short、Integer                        、Long                        、Float、Double                、Byte                        、Character等 大数据类型:BigInteger        、BigDecimal

注意:原子类 AtomicInteger 和 AtomicLong 则是可变的                        。

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合                        。

通过Collections.unmodifiableMap(map)获的一个不可变的Map类型。 Collections.unmodifiableXXX() 先对原始的集合进行拷贝                        ,需要对集合进行修改的方法都直接抛出异常                。

例如                        ,如果获得的不可变map对象进行put()                、remove()                        、clear()操作,则会抛出UnsupportedOperationException异常                        。

② 绝对线程安全

绝对线程安全的实现                ,通常需要付出很大的        、甚至不切实际的代价        。

Java API中提供的线程安全                        ,大多数都不是绝对线程安全                。

例如        ,对于数组集合Vector的操作                ,如get()        、add()                        、remove()都是有synchronized关键字修饰                        。有时调用时也需要手动添加同步手段                        ,保证多线程的安全        。

下面的代码看似不需要同步        ,实际运行过程中会报错        。

import java.util.Vector; /** * @Author: lucy * @Version 1.0 */ public class VectorTest { public static void main(String[] args) { Vector<Integer> vector = new Vector<>(); while(true){ for (int i = 0; i < 10; i++) { vector.add(i); } new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < vector.size(); i++) { System.out.println("获取vector的第" + i + "个元素: " + vector.get(i)); } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i=0;i<vector.size();i++){ System.out.println("删除vector中的第" + i+"个元素"); vector.remove(i); } } }).start(); while (Thread.activeCount()>20) return; } } }

出现ArrayIndexOutOfBoundsException异常        ,原因:某个线程恰好删除了元素i                        ,使得当前线程无法访问元素i                        。

Exception in thread "Thread-1109" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1 at java.util.Vector.remove(Vector.java:831) at VectorTest$2.run(VectorTest.java:28) at java.lang.Thread.run(Thread.java:745)

需要将对元素的get和remove构造成同步代码块:

synchronized (vector){ for (int i = 0; i < vector.size(); i++) { System.out.println("获取vector的第" + i + "个元素: " + vector.get(i)); } } synchronized (vector){ for (int i=0;i<vector.size();i++){ System.out.println("删除vector中的第" + i+"个元素"); vector.remove(i); } }

③ 相对线程安全

相对线程安全需要保证对该对象的单个操作是线程安全的                ,在必要的时候可以使用同步措施实现线程安全                。 大部分的线程安全类都属于相对线程安全        ,如Java容器中的Vector                、HashTable        、通过Collections.synchronizedXXX()方法包装的集合        。

④ 线程兼容

Java中大部分的类都是线程兼容的                        ,通过添加同步措施                ,可以保证在多线程环境中安全使用这些类的对象                        。 如常见的ArrayList                        、HashTableMap都是线程兼容的                。

⑤ 线程对立

线程对立是指:无法通过添加同步措施,实现多线程中的安全使用。 线程对立的常见操作有:Thread类的suspend()和resume()(已经被JDK声明废除)                        ,System.setIn()和System.setOut()等                        。

2. Java的枚举类型

通过enum关键字修饰的数据类型                        ,叫枚举类型                        。

枚举类型的每个元素都有自己的序号,通常从0开始编号。 可以通过values()方法遍历枚举类型                ,通过name()或者toString()获取枚举类型的名称 通过ordinal()方法获取枚举类型中元素的序号 public class EnumData { public static void main(String[] args) { for (Family family : Family.values()) { System.out.println(family.name() + ":" + family.ordinal()); } } } enum Family { GRADMOTHER, GRANDFATHER, MOTHER, FATHER, DAUGHTER, SON; }

可以将枚举类型看做普通的class                        ,在里面定义final类型的成员变量        ,便可以为枚举类型中的元素赋初值                。

要想获取枚举类型中元素实际值                ,需要为成员变量添加getter方法                        。

虽然枚举类型的元素有了自己的实际值                        ,但是通过ordinal()方法获取的元素序号不会发生改变        。

public class EnumData { public static void main(String[] args) { for (Family family : Family.values()) { System.out.println(family.name() + ":实际值" + family.getValue() + ", 实际序号" + family.ordinal()); } } } enum Family { GRADMOTHER(3), GRANDFATHER(4), MOTHER(1), FATHER(2), DAUGHTER(5), SON(6); private final int value; Family(int value) { this.value = value; } public int getValue() { return value; } }

3. Java线程安全的实现

① 互斥同步

互斥同步(Mutex Exclusion & Synchronization)是一种常见的并发正确性保障手段                。

同步:多个线程并发访问共享数据        ,保证共享数据同一时刻只被一个(或者一些        ,使用信号量)线程使用                        。 互斥:互斥是实现同步的一种手段                        ,主要的互斥实现方式:临界区(Critical Section)                、互斥量(Mutex)、信号量(Semaphore)        。

同步与互斥的关系:

互斥是原因                ,同步是结果        。 同步是目的        ,互斥是方法                        。

Java中                        ,最基本的实现互斥同步的手段是synchronized关键字                ,其次是JUC包中的ReentrantLock                。

关于synchronized关键字:

编译后的同步块,开始处会添加monitorenter指令                        ,结束处或异常处会添加monitorexit指令        。 monitorenter和monitorexit指令中都包含一个引用类型的参数                        ,分别指向加锁或解锁的对象                        。如果是同步代码块,则为synchronized括号中明确指定的对象;如果为普通方法                ,则为当前实例对象;如果为静态方法                        ,则为类对应的class对象                。 JVM执行monitorenter指令时        ,要先尝试获取锁:如果对象没被锁定或者当前线程已经拥有该对象的锁                ,则锁计数器加1;否则获取锁失败                        ,进入阻塞状态        ,等待持有锁的线程释放锁。 JVM执行monitorexit指令时        ,锁计数器减1                        ,直到计数器的值为0                ,锁被释放                        。(synchronized是支持重进入的) 由于阻塞或者唤醒线程都需要从用户态(User Mode)切换到核心态(Kernel Mode)        ,有时锁只会被持有很短的时间                        ,没有必要进行状态转换                        。可以让线程在阻塞之前先自旋等待一段时间                ,超时未获取到锁才进入阻塞状态,这样可以避免频繁的切入到核心态。其实                        ,就是后面自旋锁的思想                。

关于ReentrantLock:

与synchronized关键字相比                        ,它是API层面的互斥锁(lock()                        、unlock()                        、try...finally)                        。 与synchronized关键字相比,具有可中断、支持公平与非公平性                、可绑定多个Condition对象的高级功能        。 由于synchronized关键字被优化                ,二者的性能差异并不是很大                        ,如果不是想使用ReentrantLock的高级功能        ,优先考虑使用synchronized关键字                。

② 非阻塞同步

(1)CAS概述

互斥同步最大的性能问题是线程的阻塞和唤醒                ,因此又叫阻塞同步                        。

互斥同步采用悲观并发策略:

多线程并发访问共享数据时                        ,总是认为只要不加正确的同步措施        ,肯定会出现问题        。 无论共享数据是否存在竞争        ,都会执行加锁                        、用户态和心态的切换        、维护锁计数器                、检查是否有被阻塞的线程需要唤醒等操作        。

随着硬件指令集的发展                        ,我们可以采用基于冲突检测的乐观并发策略:

先进行操作                ,如果不存在冲突(即没有其他线程争用共享数据)        ,则操作成功                        。 如果有其他线程争用共享数据                        ,产生了冲突                ,使用其他的补偿措施                。 常见的补偿措施:不断尝试,直到成功为止                        ,比如循环的CAS操作        。

乐观并发策略的许多实现都不需要将线程阻塞                        ,这种同步操作叫做非阻塞同步                        。

非阻塞同步依靠的硬件指令集:前三条是比较久远的指令,后两条是现代处理器新增的                。

测试和设置(Test and Set) 获取并增加(Fetch and Increment) 交换(Swap) 比较并交换(Compare and Swap                ,即CAS) 加载链接/条件存储(Load Linked/ Store Conditional                        ,即LL/SC)

什么是CAS?

CAS        ,即Compare and Swap                ,需要借助处理器的cmpxchg指令完成。 CAS指令需要三个操作数:内存位置V(Java中可以简单的理解为变量的内存地址)                        、旧的期待值A        、新值B                        。 CAS指令执行时                        ,当且仅当V符合旧的预期值A        ,处理器才用新值B更新V的值;否则        ,不执行更新                        。 不管是否更新V的值                        ,都返回V的旧值                ,整个处理过程是一个原子操作。

原子操作:所谓的原子操作是指一个或一系列不可被中断的操作                。

Java中的CAS操作:

Java中的CAS操作由sun.misc.Unsafe中的compareAndSwapInt()        、compareAndSwapLong()等几个方法包装提供                        。实际无法调用这些方法        ,需要采用反射机制才能使用        。 在实际的开发过程中                        ,一般通过其他的Java API调用它们                ,如JUC包原子类中的compareAndSet(expect, update)                         、getAndIncrement()等方法                。这些方法内部都使用了Unsafe类的CAS操作                        。 Unsafe类的CAS操作,通过JVM的即时编译器编译后                        ,是一条与平台相关的CAS指令        。

除了偏向锁                        ,Java中其他锁的实现方式都是用了循环的CAS操作        。

(2)通过循环的CAS实现原子操作

通过++i或者i++可以实现计数器的自增,在多线程环境下                ,这样使用是非线程安全的                        。

public class UnsafeCount { private int i = 0; private static final int THREADS_COUNT = 200; public static void main(String[] args) { Thread[] threads = new Thread[THREADS_COUNT]; UnsafeCount counter = new UnsafeCount(); for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { counter.count(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println("多线程调用计数器i                        ,运行后的值为: " + counter.i); } public void count() { i++; } }

运行以上的代码发现:当线程数量增加        ,每个线程调用计数器的次数变大时                ,每次运行的结果是错误且不固定的                。

为了实现实在一个多线程环境下                、线程安全的计数器                        ,需要使用AtomicInteger的原子自增运算        。

import java.util.concurrent.atomic.AtomicInteger; public class SafeCount { private AtomicInteger atomic = new AtomicInteger(0); private static final int THREAD_COUNT = 200; public static void main(String[] args) { SafeCount counter = new SafeCount(); Thread[] threads = new Thread[THREAD_COUNT]; for (int i = 0; i < THREAD_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int j=0;j<10000;j++){ counter.count(); } } }); threads[i].start(); } while (Thread.activeCount()>1){ Thread.yield(); } System.out.println("多线程调用线程安全的计数器atomic:"+counter.atomic); } public void count() { // 调用compareAnSet方法        ,使用循环的CAS操作实现计数器的原子自增 for (; ; ) { int expect = atomic.get(); int curVal = expect + 1; if (atomic.compareAndSet(expect, curVal)) { break; } } } }

与非线程安全的计数器相比        ,线程安全的计数器有以下特点:

将int类型的计数器变量i                        ,更换成具有CAS操作的AtomicInteger类型的计数器变量atomic                        。 进行自增运算时                ,通过循环的CAS操作实现atomic的原子自增                。 先通过atomic.get()获取expect的值        ,将expect加一得到新值                        ,然后通过atomic.compareAndSet(expect, curVal)这一方法实现CAS操作。 其中compareAndSet()返回的true或者false                ,表示此次CAS操作是否成功                        。如果返回false,则不停地重复执行CAS操作                        ,直到操作成功                        。

上面的count方法实现的AtomicInteger原子自增                        ,可以只需要调用incrementAndGet()一个方法就能实现。

public void count() { // 调用incrementAndGet方法,实现AtomicInteger的原子自增 atomic.incrementAndGet(); }

因为incrementAndGet()方法                ,封装了通过循环的CAS操作实现AtomicInteger原子自增的代码                。

public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } 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; } (3)CAS操作存在的问题

1. ABA问题

在执行CAS操作更新共享变量的值时                        ,如果一个值原来是A        ,被其他线程改成了B                ,然后又改回成了A                        。对于该CAS操作来说                        ,它完全感受不到共享变量值的变化        。这种操作漏洞称为CAS操作的ABA问题                。 解决该问题的思路是        ,为变量添加版本号        ,每次更新时版本号递增                        。这种场景下就成了1A --> 2B --> 3A        。CAS操作就能检测到共享变量的ABA问题了        。 JUC包中                        ,也提供了相应的带标记的原子引用类AtomicStampedReference来解决ABA问题                        。 AtomicStampedReference的compareAndSet()方法会首先比较期待的引用是否等于当前引用                ,然后检查期待的标记是否等于当前标记                。如果全部相等        ,则以原子操作的方式将新的引用和新的标记更新到当前值中        。 但是AtomicStampedReference目前比较鸡肋                        ,如果想解决AB问题                ,可以使用锁                        。

2. 循环时间过长,开销大

循环的CAS操作如果长时间不成功                        ,会给CPU带来非常大的执行开销                。

3. 只能保证一个共享变量的原子操作

只对一个共享变量执行操作时                        ,可以通过循环的CAS操作实现。如果是多个共享变量,循环的CAS操作无法保证操作的原子性                        。 取巧的操作:将多个共享变量合为一个变量进行CAS操作                        。JDK1.5开始                ,提供了AtomicReference类保证引用对象之间的原子性                        ,可以将多个变量放在一个对象中进行CAS操作。

③ 无同步方案

同步只是保证共享数据争用时正确性的一种手段        ,如果不存在共享数据                ,自然无须任何同步措施                。

(1)栈封闭

多个线程访问同一个方法的局部变量时                        ,不会出现线程安全问题                        。

因为方法中的局部变量不会逃出该方法而被其他线程访问        ,因此可以看做JVM栈中数据        ,属于线程私有        。

(2)可重入代码(Reentrant Code)

可重入代码又叫纯代码(Pure Code)                        ,可在代码执行的任何时候中断他它                ,转去执行另外一段代码(包括递归调用它本身)        ,控制权返回后                        ,原来的程序不会出现任何错误                。

所有可重入的代码都是线程安全                ,并非所有线程安全的代码都是可重入的                        。

可重入代码的共同特征:

不依赖存储在堆上的数据和公用的系统资源 用到的状态量都由参数中传入 不调用非可重用的方法

如何判断代码是否具备可重入性?如果一个方法,它的返回结果是可预测的        。只要输入了相同的数据                        ,就都能返回相同的结果                        ,那它就满足可重入性,当然也就是线程安全的        。

(3)线程本地存储(TLS)

线程本地存储(Thread Local Storage):

如果一段代码中所需要的数据必须与其他代码共享                ,那就看看这些共享数据的代码是否能保证在同一个线程中执行                        。 如果能保证                        ,我们就可以把共享数据的可见范围限制在同一个线程内                。 这样        ,无须同步也能保证线程之间不出现数据争用的问题        。

TLS的重要应用实例:经典的Web交互模型中                ,一个请求对应一个服务器线程                        ,使得Web服务器应用可以使用                        。

Java中没有关键字可以将一个变量定义为线程所独享        ,但是Java中创建了java.lang.ThreadLocal类提供线程本地存储功能                。

每一个线程内部都包含一个ThreadLocalMap对象        ,该对象将ThreadLocal对象的hashCode值作为key                        ,即ThreadLocal.threadLocalHashCode                ,将本地线程变量作为value        ,构成键值对。 ThreadLocal对象是当前线程ThreadLocalMap对象的访问入口                        ,通过threadLocal.set()为本地线程添加独享变量;通过threadLocal.get()获取本地线程独享变量的值                        。 ThreadLocal        、ThreadLocalMap                        、Thread的关系:Thread对象中包含ThreadLocalMap对象                ,ThreadLocalMap对象中包含多个键值对,每个键值对的key是ThreadLocal对象的hashCode                        ,value是本地线程变量                        。

ThreadLocal的编程实例:

想为某个线程添加本地线程变量                        ,必须通过ThreadLocal对象在该线程中进行添加,构造出的键值对自动存入该线程的map中; 想要获取某个线程的本地线程变量                ,必须在该线程中获取                        ,会自动查询该线程的map        ,获得ThreadLocal对象对应的value。 通过ThreadLocal对象重复为某个线程添加键值对                ,会覆盖之前的value                。 public class TLS { public static void main(String[] args) { ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { // 设置当前线程的本地线程变量 threadLocal1.set("thread1"); threadLocal2.set(1); System.out.println(threadLocal1.get() + ": " + threadLocal2.get()); // 使用完毕后要删除                        ,避免内存泄露 threadLocal1.remove(); threadLocal2.remove(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { threadLocal1.set("thread2"); threadLocal2.set(2); System.out.println(threadLocal1.get() + ": " + threadLocal2.get()); threadLocal1.remove(); threadLocal2.remove(); } }); thread1.start(); thread2.start(); // 没有通过ThreadLocal为主线程添加过本地线程变量        ,获取到的内容都是null System.out.println(threadLocal1.get()+": "+threadLocal2.get()); } }

对ThreadLocal的正确理解:

ThreadLocal适用于线程需要有自己的实例变量        ,该实例变量可以在多个方法中被使用                        ,但是不能被其他线程共享的场景                        。 由于不存在数据共享                ,何谈同步?因此ThreadLocal 从理论上讲        ,不是用来解决多线程并发问题的        。

ThreadLocal的实现:

最原始的想法:ThreadLocal维护线程与实例的映射                。既然通过ThreadLocal对象为线程添加本地线程变量                        ,那就将ThreadLocalMap放在ThreadLocal中                        。

原始想法存在的缺陷:多线程并发访问ThreadLocal中的Map                ,需要添加锁        。这是, JDK 未采用该方案的一个原因        。

优化后的方法:Thread维护ThreadLocal与实例的映射                        。Map是每个线程所私有                        ,只能在当前线程通过ThreadLocal对象访问自身的Map                。不存在多线程并发访问同一个Map的情况                        ,也就不需要锁        。

优化后存在内存泄露的情况:JDK1.8中,ThreadLocalMap每个Entry对ThreadLocal对象是弱引用                ,对每个实例是强引用                        。当ThreadLocal对象被回收后                        ,该Entry的键变成null        ,但Entry无法被移除                。使得实例被Entry引用无法回收                ,造成内存泄露。

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2022最新版)

2.劲爆!Java 协程要来了                        。                        。。

3.Spring Boot 2.x 教程                        ,太全了!

4.别再写满屏的爆爆爆炸类了        ,试试装饰器模式        ,这才是优雅的方式!!

5.《Java开发手册(嵩山版)》最新发布                        ,速速下载!

觉得不错                ,别忘了随手点赞+转发哦!

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
双十一office降价吗(2022年Office办公双11有优惠活动吗_Office双11有哪些优惠) steam哪些游戏赚钱(哪些游戏平台可以赚钱-Steam平台竞争加剧 独立游戏开发者的赚钱梦几近破灭)