点击关注公众号,实用技术文章及时了解

来源: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类型。

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() {@Overridepublic void run() {for (int i = 0; i < vector.size(); i++) {System.out.println("获取vector的第" + i + "个元素: " + vector.get(i));}}}).start();new Thread(new Runnable() {@Overridepublic 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: 1at 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操作。学习资料:Java进阶视频资源

(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() {@Overridepublic 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() {@Overridepublic 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中创建了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() {@Overridepublic 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() {@Overridepublic void run() {threadLocal1.set("thread2");threadLocal2.set(2);System.out.println(threadLocal1.get() + ": " + threadLocal2.get());threadLocal1.remove();threadLocal2.remove();}});thread1.start();thread2.start();// 没有通过ThreadLocal为主线程添加过本地线程变量,获取到的内容都是nullSystem.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引用无法回收,造成内存泄露。

推荐:

主流Java进阶技术(学习资料分享)

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

Java并发:五种线程安全类型、线程安全的实现、枚举类型相关推荐

  1. Java并发(二十一):线程池实现原理

    一.总览 线程池类ThreadPoolExecutor的相关类需要先了解: (图片来自:https://javadoop.com/post/java-thread-pool#%E6%80%BB%E8% ...

  2. 【Java并发编程】之二:线程中断

    [Java并发编程]之二:线程中断 使用interrupt()中断线程 ​ 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一 ...

  3. Java swing五种常见的布局方式【转载】

    Java swing五种常见的布局方式 1. 边界布局(BorderLayout) 2.流式布局(FlowLayout) 3.网格布局(GridLayout) 4.盒子布局(BoxLaYout) 5. ...

  4. php枚举属于什么类型,python枚举类型是什么?python枚举类型的简单介绍

    本篇文章给大家带来的内容是关于python枚举类型是什么?python枚举类型的简单介绍,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 一:枚举类型的定义 二:枚举类和普通类的区别 ...

  5. java并发编程基础—生命周期与线程控制

    一.线程生命周期 线程被创建启动以后,他既不是一启动就进入执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New).就绪(Runnable).运行(Running).阻塞(Bloc ...

  6. 线程池的五种状态及创建线程池的几种方式

    上篇<Java线程的6种状态详解及创建线程的4种方式> 前言:我们都知道,线程是稀有资源,系统频繁创建会很大程度上影响服务器的使用效率,如果不加以限制,很容易就会把服务器资源耗尽.所以,我 ...

  7. 【Java 并发编程】【05】线程安全问题与线程同步

    5. 线程安全问题与线程同步 多线程编程是有趣且复杂的事情,它常常容易突然出现"错误情况",这是由于系统的线程调度具有一定的随机性.即使程序在运行过程中偶尔会出现问题,那也是由于我 ...

  8. 【Java并发编程】面试必备之线程池

    什么是线程池 是一种基于池化思想管理线程的工具.池化技术:池化技术简单点来说,就是提前保存大量的资源,以备不时之需.比如我们的对象池,数据库连接池等. 线程池好处 我们为什么要使用线程池,直接new ...

  9. Java并发编程:面试必备之线程池

    什么是线程池 是一种基于池化思想管理线程的工具.池化技术:池化技术简单点来说,就是提前保存大量的资源,以备不时之需.比如我们的对象池,数据库连接池等. 线程池好处 我们为什么要使用线程池,直接new ...

  10. Java并发编程(2):线程中断(含代码)

    使用interrupt()中断线程 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回.这 ...

最新文章

  1. php清除账号登录,php实现账号登录/上传/下载/删除文件
  2. 中部四省会打造人才信息云平台
  3. 将 Firefox 浏览器的书签导出
  4. hosts文件 端口_中望软件:中望3D网络版服务端如何固定端口
  5. C++面试宝典(纯干货)
  6. linux每周2 4 6执行定时任务,linux计划任务crontab例子
  7. cookie登陆qq空间的两种方式
  8. Java网络爬虫Spider
  9. C语言-B树(B-树)的完整实现
  10. 【深度】广告流量分配HWM算法
  11. 360悬浮加速小火箭,内存清理,加速缓存
  12. Jmeter beanshell语法
  13. 一次项目开发中,收获的经验和教训
  14. 怎样正确配置OmniPlan预置
  15. H3C 二层链路聚合
  16. 【水滴石穿】mobx-todos
  17. 海康威视错误代码0xf_海康威视设备网络SDK编程指南(报 警主机).pdf
  18. onclick 与 click 的区别
  19. 把资料放在网盘的风险有多大?MEGA网盘87GB数据遭泄露
  20. 有关于PC的扫码登录

热门文章

  1. 老用户们,苹果没有忘了你们!
  2. 华为Mate X国行售价曝光,5G网速实测,强悍!
  3. 走,去抖音上发财!抖音承诺未来一年要帮一千万创作者赚到钱
  4. 用千元左右的手机你会感到没面子吗?网友回答令人赞叹
  5. 魅族回应“手机无法拨打120”一事:已安排负责人与客户沟通处理
  6. 阿里影业正式成为阿里集团子公司 俞永福辞任执行董事
  7. 拳王公社:知识付费项目!个体操作可年入千万!简单可复制!
  8. 二维数组和指针(包含交换二维数组行列)
  9. Promise【面试】
  10. 晨哥真有料丨女生眼里的满分男生!