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

一、不可变对象

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

(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中添加如下依赖包:

com.google.guava guava 23.0
二、线程封闭

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

三、线程不安全类与写法

  1. StringBuilder -> StringBuffer

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

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

  1. 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中添加如下依赖包:

joda-time joda-time 2.9
示例代码如下:

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集合类为线程不安全类

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

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

两个线程同时执行这种操作,同时对if条件进行判断,并且a变量是线程共享的,如果两个线程均满足if条件,则两个线程会同时执行handle(a)语句,此时,handle(a)语句就可能不是线程安全的。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

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

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

四、线程安全-同步容器

  1. ArrayList -> Vector, Stack

ArrayList:线程不安全;

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

public class VectorExample {private static Vector 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 {@Overridepublic void run {for(int i = 0; i < vector.size; i++){vector.remove(i);}}});Thread thread2 = new Thread(new Runnable {@Overridepublic void run {for(int i = 0; i < vector.size; i++){vector.get(i);}}});thread1.start;thread2.start;}}}
Stack:继承自Vector,先进后出。

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

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

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

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

public class VectorExample3 {//此方法抛出:java.util.ConcurrentModificationExceptionprivate static void test1(Vector v1){for(Integer i : v1){if(i == 3){v1.remove(i);}}}//此方法抛出:java.util.ConcurrentModificationExceptionprivate static void test2(Vector v1){iterator iterator = v1.iterator;while (iterator.hasNext){Integer i = iterator.next;if(i == 3){v1.remove(i);}}}//正常private static void test3(Vector v1){for(int i = 0; i < v1.size; i++){if(i == 3){v1.remove(i);}}}public static void main(String args) throws InterruptedException {Vector 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)。

  1. 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)被守护对象:被守护对象只能通过获取特定的锁来访问

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值