(四)Java并发学习笔记--线程不安全类与写法

简介: 常见线程不安全的类有哪些呢下图中,我们只画出了最常见的几种情况,我们常见的Collections集合都是线程不安全的StringBuilder-demo:@Slf4jpublic class StringExample1 {...

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

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

img_7b4cfca9014827d8915693666ddf8a5b.png
  • 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();

    }
}

运行结果如下:


img_7dc5e8eba8918d5f0c199b3679620ed4.png

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

目录
相关文章
|
7月前
|
安全 Java 数据建模
Java记录类:简化数据载体的新选择
Java记录类:简化数据载体的新选择
402 101
|
7月前
|
安全 Java 开发者
Java记录类:简化数据载体的新方式
Java记录类:简化数据载体的新方式
346 100
|
8月前
|
安全 IDE Java
Java记录类型(Record):简化数据载体类
Java记录类型(Record):简化数据载体类
588 143
|
6月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
187 4
|
6月前
|
存储 Java 索引
用Java语言实现一个自定义的ArrayList类
自定义MyArrayList类模拟Java ArrayList核心功能,支持泛型、动态扩容(1.5倍)、增删改查及越界检查,底层用Object数组实现,适合学习动态数组原理。
264 4
|
6月前
|
缓存 安全 Java
如何理解Java中的并发?
Java并发指多任务交替执行,提升资源利用率与响应速度。通过线程实现,涉及线程安全、可见性、原子性等问题,需用synchronized、volatile、线程池及并发工具类解决,是高并发系统开发的关键基础。(238字)
354 5
|
6月前
|
IDE JavaScript Java
在Java 11中,如何处理被弃用的类或接口?
在Java 11中,如何处理被弃用的类或接口?
315 5
|
6月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
377 2
|
6月前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
393 5
|
6月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
320 1
下一篇
开通oss服务