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

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

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

Java中的线程到底有哪些安全策略的更多相关文章

  1. java中的线程安全

    在Java中,线程的安全实际上指的是内存的安全,这是由操作系统决定的. 目前主流的操作系统都是多任务的,即多个进程同时运行.为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的.分配给别 ...

  2. Java中的线程

    http://hi.baidu.com/ochzqvztdbabcir/item/ab9758f9cfab6a5ac9f337d4 相濡以沫 Java语法总结 - 线程 一 提到线程好像是件很麻烦很复 ...

  3. [译]线程生命周期-理解Java中的线程状态

    线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...

  4. Java中的线程Thread总结

    首先来看一张图,下面这张图很清晰的说明了线程的状态与Thread中的各个方法之间的关系,很经典的! 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口. 要注意的是Threa ...

  5. JAVA中创建线程的三种方法及比较

    JAVA中创建线程的方式有三种,各有优缺点,具体如下: 一.继承Thread类来创建线程 1.创建一个任务类,继承Thread线程类,因为Thread类已经实现了Runnable接口,然后重写run( ...

  6. 浅谈利用同步机制解决Java中的线程安全问题

    我们知道大多数程序都不会是单线程程序,单线程程序的功能非常有限,我们假设一下所有的程序都是单线程程序,那么会带来怎样的结果呢?假如淘宝是单线程程序,一直都只能一个一个用户去访问,你要在网上买东西还得等 ...

  7. 第9章 Java中的线程池 第10章 Exector框架

    与新建线程池相比线程池的优点 线程池的分类 ThreadPoolExector参数.执行过程.存储方式 阻塞队列 拒绝策略 10.1 Exector框架简介 10.1.1 Executor框架的两级调 ...

  8. Java中一个线程只有六个状态。至于阻塞、可运行、挂起状态都是人们为了便于理解,自己加上去的。

    java中,线程的状态使用一个枚举类型来描述的.这个枚举一共有6个值: NEW(新建).RUNNABLE(运行).BLOCKED(锁池).TIMED_WAITING(定时等待).WAITING(等待) ...

  9. Java中创建线程的三种方式以及区别

    在java中如果要创建线程的话,一般有3种方法: 继承Thread类: 实现Runnable接口: 使用Callable和Future创建线程. 1. 继承Thread类 继承Thread类的话,必须 ...

随机推荐

  1. numpy入门—Numpy的核心array对象以及创建array的方法

    Numpy的核心array对象以及创建array的方法 array对象的背景: Numpy的核心数据结构,就叫做array就是数组,array对象可以是一维数组,也可以是多维数组: Python的Li ...

  2. 【精】多层PCB层叠结构

    在设计多层PCB电路板之前,设计者需要首先根据电路的规模.电路板的尺寸和电磁兼容(EMC)的要求来确定所采用的电路板结构,也就是决定采用4层,6层,还是更多层数的电路板.确定层数之后,再确定内电层的放 ...

  3. DIV 上下左右居中黑科技

    <style> #info{height:0px; width:0px;top:50%; left:50%;position:absolute;} #center{background:# ...

  4. 编写大型项目web页面 从写web登陆页面开始

    web页面搭建需要准备什么工具 首先我们会和设计师沟通 我们需要一些检验设计的工具 ps 自动裁图 自动测量工具 (我这里安利一下一个工具 我用的cutterman) sketch 可以使用阿里的工具 ...

  5. 前端每日实战:144# 视频演示如何用 D3 和 GSAP 创作一个集体舞动画

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/gdVObN 可交互视频 此视频是可 ...

  6. Java/C++实现模板方法模式---数据库操作

    对数据库的操作一般包括连接.打开.使用.关闭等步骤,在数据库操作模板类中我们定义了connDB().openDB().useDB().closeDB()四个方法分别对应这四个步骤.对于不同类型的数据库 ...

  7. Thread中,run方法和start方法的区别

    1. 通过调用Thread类中的start()方法可以启动一个线程,但是线程并不是立刻运行,而是处于就绪态,一旦获取cpu时间片,则会立即运行run()方法 2. start()方法实现了多线程运行, ...

  8. 面向对象编程-终结篇 es6新增语法

    各位,各位,终于把js完成了一个段落了,这次的章节一过我还没确定下面要学的内容可能是vue也可能是前后端交互,但无论是哪个都挺兴奋的,因为面临着终于可以做点看得过去的大点的案例项目了,先憋住激动地情绪 ...

  9. python---运算符和编码

    格式化输出 在输出数据时,会有用户需要输入的信息被输出,没办法预先知道,这就需要使用格式化输出.先在相应位置放置一个占位符,再把字符串里的占位符与外部的变量做个映射关系. name = input(& ...

  10. Vue3 + Echarts 5 绘制带有立体感流线中国地图,建议收藏

    本文绘制的地图效果图如下: 一.Echarts 使用五部曲 1.下载并引入 echarts Echarts 已更新到了 5.0 版本,安装完记得检查下自己的版本是否是 5.0 . npm instal ...