Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说Java中的线程到底有哪些安全策略,希望能够帮助你!!!。
本文分享自华为云社区《【高并发】线程安全策略-云社区-华为云》,作者:冰河。
不可变对象需要满足的条件
final关键字:类、方法、变量
在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>
StringBuilder:线程不安全;
StringBuffer:线程不安全;
字符串拼接涉及到多线程操作时,使用StringBuffer实现。
在一个具体的方法中,定义一个字符串拼接对象,此时可以使用StringBuilder实现。因为在一个方法内部定义局部变量进行使用时,属于堆栈封闭,只有一个线程会使用变量,不涉及多线程对变量的操作,使用StringBuilder即可。
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));
}
}
注意:这种写法是线程不安全的!!!!!
两个线程同时执行这种操作,同时对if条件进行判断,并且a变量是线程共享的,如果两个线程均满足if条件,则两个线程会同时执行handle(a)语句,此时,handle(a)语句就可能不是线程安全的。
不安全的点在于两个操作中,即使前面的执行过程是线程安全的,后面的过程也是线程安全的,但是前后执行过程的间隙不是原子性的,因此,也会引发线程不安全的问题。
实际过程中,遇到if(condition(a)){handle(a);}类的处理时,考虑a是否是线程共享的,如果是线程共享的,则需要在整个执行方法上加锁,或者保证if(condition(a)){handle(a);}的前后两个操作(if判断和代码执行)是原子性的。
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,先进后出。
HashMap:线程不安全;
HashTable:线程安全,注意使用HashTable时,Key, Value都不能为null;
注意:在遍历集合的时候,不要对集合进行更新操作。当需要对集合中的元素进行删除操作时,可以遍历集合,先对需要删除的元素进行标记,集合遍历结束后,再进行删除操作。例如,下面的示例代码:
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表示的是java.util.concurrent报名的缩写。
ArrayList:线程不安全;
CopyOnWriteArrayList:线程安全;
写操作时复制,当有新元素添加到CopyOnWriteArrayList数组时,先从原有的数组中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。
CopyOnWriteArrayList缺点:
注意:CopyOnWriteArrayList读操作时,都是在原数组上进行的,不需要加锁,写操作时复制,当有新元素添加到CopyOnWriteArrayList数组时,先从原有的集合中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。
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)。
ConcurrentHashMap:线程安全,不允许空值
ConcurrentSkipListMap:是TreeMap的线程安全版本,内部是使用SkipList跳表结构实现
注意:在非对线程下尽量使用TreeMap,另外,对于并发数相对较低的并行程序,可以使用Collections.synchronizedSortedMap,将TreeMap进行包装;对于高并发程序,使用ConcurrentSkipListMap提供更高的并发度;在多线程高并发环境中,需要对Map的键值对进行排序,尽量使用ConcurrentSkipListMap。
点击下方,第一时间了解华为云新鲜技术~
华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。