写点什么

Java 中的线程到底有哪些安全策略

  • 2022 年 5 月 17 日
  • 本文字数:6551 字

    阅读完需:约 21 分钟

本文分享自华为云社区《【高并发】线程安全策略》,作者:冰 河 。

一、不可变对象

不可变对象需要满足的条件

(1)对象创建以后其状态就不能修改

(2)对象所有域都是 final 类型

(3)对象是正确创建的(在对象创建期间,this 引用没有溢出)


对于不可变对象,可以参见 JDK 中的 String 类

final 关键字:类、方法、变量

(1)修饰类:该类不能被继承,String 类,基础类型的包装类(比如 Integer、Long 等)都是 final 类型。final 类中的成员变量可以根据需要设置为 final 类型,但是 final 类中的所有成员方法,都会被隐式的指定为 final 方法。

(2)修饰方法:锁定方法不被继承类修改;效率。注意:一个类的 private 方法会被隐式的指定为 final 方法(3)修饰变量:基本数据类型变量(数值被初始化后不能再修改)、引用类型变量(初始化之后则不能再指向其他的对象)


在 JDK 中提供了一个 Collections 类,这个类中提供了很多以 unmodifiable 开头的方法,如下:

Collections.unmodifiableXXX: Collection、List、Set、Map…


其中 Collections.unmodifiableXXX 方法中的 XXX 可以是 Collection、List、Set、Map…


此时,将我们自己创建的 Collection、List、Set、Map,传递到 Collections.unmodifiableXXX 方法中,就变为不可变的了。此时,如果修改 Collection、List、Set、Map 中的元素就会抛出 java.lang.UnsupportedOperationException 异常。


在 Google 的 Guava 中,包含了很多以 Immutable 开头的类,如下:


ImmutableXXX,XXX 可以是 Collection、List、Set、Map…


注意:使用 Google 的 Guava,需要在 Maven 中添加如下依赖包:

<dependency>	<groupId>com.google.guava</groupId>	<artifactId>guava</artifactId>	<version>23.0</version></dependency>
复制代码

二、线程封闭

(1)Ad-hoc 线程封闭:程序控制实现,最糟糕,忽略(2)堆栈封闭:局部变量,无并发问题(3)ThreadLocal 线程封闭:特别好的封闭方法

三、线程不安全类与写法

1. StringBuilder -> StringBuffer

StringBuilder:线程不安全;StringBuffer:线程不安全;


字符串拼接涉及到多线程操作时,使用 StringBuffer 实现

在一个具体的方法中,定义一个字符串拼接对象,此时可以使用 StringBuilder 实现。因为在一个方法内部定义局部变量进行使用时,属于堆栈封闭,只有一个线程会使用变量,不涉及多线程对变量的操作,使用 StringBuilder 即可。

2. SimpleDateFormat -> JodaTime

SimpleDateFormat:线程不安全,可以将其对象的实例化放入到具体的时间格式化方法中,实现线程安全 JodaTime:线程安全


SimpleDateFormat 线程不安全的代码示例如下:

package io.binghe.concurrency.example.commonunsafe;import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;@Slf4jpublic class DateFormatExample { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); //请求总数 public static int clientTotal = 5000; //同时并发执行的线程数 public static int threadTotal = 200;
public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for(int i = 0; i < clientTotal; i++){ executorService.execute(() -> { try{ semaphore.acquire(); update(); semaphore.release(); }catch (Exception e){ log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); } public static void update(){ try { simpleDateFormat.parse("20191024"); } catch (ParseException e) { log.error("parse exception", e); } }}
复制代码

修改成如下代码即可。

package io.binghe.concurrency.example.commonunsafe;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;@Slf4jpublic class DateFormatExample2 { //请求总数 public static int clientTotal = 5000; //同时并发执行的线程数 public static int threadTotal = 200;
public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for(int i = 0; i < clientTotal; i++){ executorService.execute(() -> { try{ semaphore.acquire(); update(); semaphore.release(); }catch (Exception e){ log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); }
public static void update(){ try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); simpleDateFormat.parse("20191024"); } catch (ParseException e) { log.error("parse exception", e); } }}
复制代码

对于 JodaTime 需要在 Maven 中添加如下依赖包:

<dependency>	<groupId>joda-time</groupId>	<artifactId>joda-time</artifactId>	<version>2.9</version></dependency>
复制代码

示例代码如下:

package io.binghe.concurrency.example.commonunsafe;import lombok.extern.slf4j.Slf4j;import org.joda.time.DateTime;import org.joda.time.format.DateTimeFormat;import org.joda.time.format.DateTimeFormatter;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
@Slf4jpublic class DateFormatExample3 { //请求总数 public static int clientTotal = 5000; //同时并发执行的线程数 public static int threadTotal = 200;
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");
public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for(int i = 0; i < clientTotal; i++){ final int count = i; executorService.execute(() -> { try{ semaphore.acquire(); update(count); semaphore.release(); }catch (Exception e){ log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); }
public static void update(int i){ log.info("{} - {}", i, DateTime.parse("20191024", dateTimeFormatter)); }}
复制代码

3. ArrayList、HashSet、HashMap 等 Collections 集合类为线程不安全类

4. 先检查再执行:if(condition(a)){handle(a);}

注意:这种写法是线程不安全的!!!!!


两个线程同时执行这种操作,同时对 if 条件进行判断,并且 a 变量是线程共享的,如果两个线程均满足 if 条件,则两个线程会同时执行 handle(a)语句,此时,handle(a)语句就可能不是线程安全的。


不安全的点在于两个操作中,即使前面的执行过程是线程安全的,后面的过程也是线程安全的,但是前后执行过程的间隙不是原子性的,因此,也会引发线程不安全的问题。


实际过程中,遇到 if(condition(a)){handle(a);}类的处理时,考虑 a 是否是线程共享的,如果是线程共享的,则需要在整个执行方法上加锁,或者保证 if(condition(a)){handle(a);}的前后两个操作(if 判断和代码执行)是原子性的。

四、线程安全-同步容器

1. ArrayList -> Vector, Stack

ArrayList:线程不安全;

Vector:同步操作,但是可能会出现线程不安全的情况,线程不安全的代码示例如下:

public class VectorExample {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) throws InterruptedException { while (true){ for(int i = 0; i < 10; i++){ vector.add(i); } Thread thread1 = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < vector.size(); i++){ vector.remove(i); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < vector.size(); i++){ vector.get(i); } } }); thread1.start(); thread2.start(); } }}
复制代码

Stack:继承自 Vector,先进后出。

2. HashMap -> HashTable(Key, Value 都不能为 null)

HashMap:线程不安全;HashTable:线程安全,注意使用 HashTable 时,Key, Value 都不能为 null;

3. Collections.synchronizedXXX(List、Set、Map)

注意:在遍历集合的时候,不要对集合进行更新操作。当需要对集合中的元素进行删除操作时,可以遍历集合,先对需要删除的元素进行标记,集合遍历结束后,再进行删除操作。例如,下面的示例代码:

public class VectorExample3 {
//此方法抛出:java.util.ConcurrentModificationException private static void test1(Vector<Integer> v1){ for(Integer i : v1){ if(i == 3){ v1.remove(i); } } } //此方法抛出:java.util.ConcurrentModificationException private static void test2(Vector<Integer> v1){ Iterator<Integer> iterator = v1.iterator(); while (iterator.hasNext()){ Integer i = iterator.next(); if(i == 3){ v1.remove(i); } } } //正常 private static void test3(Vector<Integer> v1){ for(int i = 0; i < v1.size(); i++){ if(i == 3){ v1.remove(i); } } } public static void main(String[] args) throws InterruptedException { Vector<Integer> vector = new Vector<>(); vector.add(1); vector.add(2); vector.add(3);
//test1(vector); //test2(vector); test3(vector); }}
复制代码

五、线程安全-并发容器 J.U.C

J.U.C 表示的是 java.util.concurrent 报名的缩写。

1. ArrayList -> CopyOnWriteArrayList

ArrayList:线程不安全;CopyOnWriteArrayList:线程安全;


写操作时复制,当有新元素添加到 CopyOnWriteArrayList 数组时,先从原有的数组中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。

CopyOnWriteArrayList 缺点:(1)每次写操作都需要复制一份,消耗内存,如果元素特别多,可能导致 GC;(2)不能用于实时读的场景,适合读多写少的场景;


CopyOnWriteArrayList 设计思想:

(1)读写分离

(2)最终一致性

(3)使用时另外开辟空间,解决并发冲突


注意:CopyOnWriteArrayList 读操作时,都是在原数组上进行的,不需要加锁,写操作时复制,当有新元素添加到 CopyOnWriteArrayList 数组时,先从原有的集合中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。

2.HashSet、TreeSet -> CopyOnWriteArraySet、ConcurrentSkipListSet

CopyOnWriteArraySet:线程安全的,底层实现使用了 CopyOnWriteArrayList。


ConcurrentSkipListSet:JDK6 新增的类,支持排序。可以在构造时,自定义比较器,基于 Map 集合。在多线程环境下,ConcurrentSkipListSet 中的 contains()方法、add()、remove()、retain()等操作,都是线程安全的。但是,批量操作,比如:containsAll()、addAll()、removeAll()、retainAll()等操作,并不保证整体一定是原子操作,只能保证批量操作中的每次操作是原子性的,因为批量操作中是以循环的形式调用的单步操作,比如 removeAll()操作下以循环的方式调用 remove()操作。如下代码所示:

//ConcurrentSkipListSet类型中的removeAll()方法的源码public boolean removeAll(Collection<?> c) {	// Override AbstractSet version to avoid unnecessary call to size()	boolean modified = false;	for (Object e : c)		if (remove(e))			modified = true;	return modified;}
复制代码

所以,在执行 ConcurrentSkipListSet 中的批量操作时,需要考虑加锁问题。


注意:ConcurrentSkipListSet 类不允许使用空元素(null)。

3. HashMap、TreeMap -> ConcurrentHashMap、ConcurrentSkipListMap

ConcurrentHashMap:线程安全,不允许空值


ConcurrentSkipListMap:是 TreeMap 的线程安全版本,内部是使用 SkipList 跳表结构实现

4.ConcurrentSkipListMap 与 ConcurrentHashMap 对比如下

(1)ConcurrentSkipListMap 中的 Key 是有序的,ConcurrentHashMap 中的 Key 是无序的;(2)ConcurrentSkipListMap 支持更高的并发,对数据的存取时间和线程数几乎无关,也就是说,在数据量一定的情况下,并发的线程数越多,ConcurrentSkipListMap 越能体现出它的优势。


注意:在非对线程下尽量使用 TreeMap,另外,对于并发数相对较低的并行程序,可以使用 Collections.synchronizedSortedMap,将 TreeMap 进行包装;对于高并发程序,使用 ConcurrentSkipListMap 提供更高的并发度;在多线程高并发环境中,需要对 Map 的键值对进行排序,尽量使用 ConcurrentSkipListMap。

六、安全共享对象的策略-总结

(1)线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改

(2)共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它。

(3)线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它

(4)被守护对象:被守护对象只能通过获取特定的锁来访问


点击关注,第一时间了解华为云新鲜技术~​

发布于: 刚刚阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
Java中的线程到底有哪些安全策略_Java_华为云开发者社区_InfoQ写作社区