并发编程(九):线程不安全的类与写法

内容预览:
  •   什么是线程不安全的类呢?   如果一个类的对象同时被多个线...~
  • 我导入的包如下: <dependency> <groupId>joda-time</gr...~
  •     ArrayList-demo @Slf4jpublic class ArrayListExample { ...~

  什么是线程不安全的类呢?

  如果一个类的对象同时被多个线程访问,如果不做特殊的同步或并发处理,很容易表现出线程不安全的现象,比如抛出异常、逻辑处理错误等,这种类我们就称为线程不安全的类

 

  常见线程不安全的类有哪些呢

  下图中,我们只画出了最常见的几种情况,我们常见的Collections集合都是线程不安全的

 

  StringBuilder-demo

@Slf4j
public class StringExample1 {

//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;

public static StringBuilder stringBuilder = new StringBuilder();

private static void update() {
stringBuilder.append(
"1");
}

public static void main(String[] args)throws Exception {

//定义线程池
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();
log.info(
"size:{}",stringBuilder.length());

}

}

  我测试的时候输出为,4985(因为线程不安全,所以每次的输出可能是不同的),如果StringBuilder类为线程安全的话,输出应该为5000

 

  StringBuffer-demo

@Slf4j
public class StringExample2 {

//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;

public static StringBuffer stringBuffer = new StringBuffer();

private static void update() {
stringBuffer.append(
"1");
}

public static void main(String[] args)throws Exception {

//定义线程池
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();
log.info(
"size:{}",stringBuffer.length());

}

}

  输出为5000,且多次测试结果均为5000,证明StringBuffer类是线程安全的,通过看StringBuffer的实现可发现,其所有的实现都是加了synchronized关键字的,虽然可以保证线程安全但是性能是有损耗的,这也证明了StringBuilder的存在价值,如果定义StringBuilder为局部变量时是没有线程安全问题的,这就用到了上篇博客我们讲的堆栈封闭原理

 

  simpleDateFormat-demo1

@Slf4j
public class DateFormatExample1 {

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;


private static void update() {
try {
simpleDateFormat.parse(
"20180208");
}
catch (ParseException e) {
log.error(
"parse exception",e);
}
}

public static void main(String[] args)throws Exception {

//定义线程池
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();

}
}

  运行结果如下:

  因为simpleDateFormat为线程不安全的类,所以在多线程访问的时候出现了异常

 

   simpleDateFormat-demo2

@Slf4j
public class DateFormatExample2 {


//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;


private static void update() {
try {
//用堆栈封闭的方式
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
simpleDateFormat.parse(
"20180208");
}
catch (ParseException e) {
log.error(
"parse exception",e);
}
}

public static void main(String[] args)throws Exception {

//定义线程池
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();

}
}

  此demo为demo1的改进版,将SimpleDateFormat声明为局部变量,运用了堆栈封闭的方式保证了线程安全,运行此demo是没有异常抛出的

 

  jodatime-demo

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

@Slf4j
public class DateFormatExample3 {


//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;

private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");


private static void update(int i) {
log.info(
"{},{}",i,DateTime.parse("20180208", dateTimeFormatter).toDate());
}

public static void main(String[] args)throws Exception {

//定义线程池
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();

}
}

  此demo引用了joda.time包,保证了线程安全,在实际的开发中,我们更推荐做日期转换的时候使用此包,这种处理方法不仅能保证线程安全,而且还有其它的优势。我导入的包如下:

        <dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9</version>
</dependency>

 

  以下我们做ArrayList,HashMap,HashSet的实例演示,它们都是线程不安全的,还好我们一般都将它们定义为局部变量(堆栈封闭),如果我们将它们定义为成员变量或static修饰的变量,在多个线程同时访问的时候就很容易出问题。

 

  ArrayList-demo

@Slf4j
public class ArrayListExample {

//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;

//arraylist是线程不安全的
private static List<Integer> list = new ArrayList<>();


private static void update(int i) {
list.add(i);
}

public static void main(String[] args)throws Exception {

//定义线程池
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();
log.info(
"size:{}", list.size());

}
}

  如果是线程安全的输出应该为5000,实际输出为4945,且每次运行输出的值可能不一样,所以它是线程不安全的

 

  HashSet-demo

@Slf4j
public class HashSetExample {

//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;

//HashSet是线程不安全的
private static Set<Integer> set = new HashSet<>();


private static void update(int i) {
set.add(i);
}

public static void main(String[] args)throws Exception {

//定义线程池
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();
log.info(
"size:{}", set.size());

}
}

  输出为4985,是线程不安全的(线程安全的话输出为5000)

 

  HashMap-demo

@Slf4j
public class HashMapExample {

//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;

//HashMap是线程不安全的
private static Map<Integer,Integer> map = new HashMap<>();


private static void update(int i) {
map.put(i,i);
}

public static void main(String[] args)throws Exception {

//定义线程池
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();
log.info(
"size:{}", map.size());

}
}

  输出为4886(且每次运行输出值可能不同),是线程不安全的(线程安全的话输出为5000)

 

  线程不安全的写法

  典型的线程不安全的写法是:先检查,再执行

  if(condition(a)) 就算a是一个线程安全的类所对应的对象,对a的处理handle(a)也是原子性的,但由于这两步之间的不是原子性的也会引发线程安全问题,如A、B两个线程都通过了a的判断条件,A线程执行handle(a)之后,a已经不符合condition(a)的判断条件了,可是此时B线程仍然要执行handle(a),这就引发了线程安全问题。

 

以上就是:并发编程(九):线程不安全的类与写法 的全部内容。

本站部分内容来源于互联网和用户投稿,如有侵权请联系我们删除,谢谢。
Email:[email protected]


0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论