Java-线程安全

Java-线程安全

在这里插入图片描述

1 什么是线程安全

线程安全是针对某个对象来说,如果当多线程访问此对象时,不用考虑这些线程在运行时环境下的调度和交替执行,也不用再用额外方式如同步锁等、不用调用方进行任何其他协调操作,总能运行获得正确结果,那就可以说这个对象代码线程安全。

也就是说,被调用的线程安全代码已经封装了必要的线程安全保证手段(如互斥同步等),调用者无需关心多线程调用问题、无需是线程任何线程安全措施。但以上定义其实并不容易做到,很多时候需要弱化一些。

实际上,在多线程编程中我们需要同时关注可见性、顺序性和原子性问题。

本篇文章将讲线程安全各个级别,以及从这三个问题出发,结合实例详解volatile如何保证可见性及一定程序上保证顺序性以及synchronized如何同时保证可见性和原子性,最后对比volatile和synchronized的适用场景。

2 Java中的线程安全

Java中线程安全强度由强到弱是:
不可变 -> 绝对线程安全 -> 相对线程安全 -> 线程兼容 -> 线程对立。

2.1 不可变

就是指那些一旦初始化就不可改变的对象,这当然是线程安全的:

  • final修饰的基本数据类型
  • final修饰的不可变对象如String, 基本类型的封装类型(Long,Integer等,因为他们的状态变量都为final,而AtomicLong等对象内部value只用了volatile修饰,并非不可变对象)(注意this指针不可逃逸)

这类对象永远不会改变,多线程状态下也永远处于一致的状态。

这种方式实现不可变最简洁,但使用场景较少。

2.2 绝对线程安全

不用考虑运行时环境,调用者不用加任何同步措施,就能保证多线程情况下拥有正确结果。

这很难达到,Java中很多所谓线程安全类都不是绝对线程安全。

比如Vector,他每个方法都是synchronized的,但如果多线程同时读写,可能会刚刚某线程remove了某个序号上的元素,另一个线程去访问这个序号的元素就可能导致报错。

解决方案就是读写方法都对这个vector实例使用synchronized加上同步锁,实现排他性。

如果要实现我们前面定义中提到的绝对线程安全,就必须在Vector内部加上快照,每次有元素改动时就产生新的快照。读取时,就读那一刻的不可变快照。但是这样开销很大,维护麻烦。

以下实例可说明Vector非绝对线程安全:

@Test
    public void test3 () {
        while (true) {
            for (int i = 0; i < 10; i++)
                vector.add(i);
 
            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        Thread.yield();
                        try{
                            vector.remove(i);
                        } catch (Exception e){
                            e.printStackTrace();
                            System.exit(-1);
                        }
                    }
                }
            });
 
            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        Thread.yield();
                        try{
                            System.out.println(vector.get(i));
                        } catch (Exception e){
                            e.printStackTrace();
                            System.exit(-1);
                        }
                    }
                }
            });
 
            removeThread.start();
            printThread.start();
 
            while (Thread.activeCount() > 20) ;//防止线程太多计算机卡死
        }
    }

以上代码很有可能出现删除线程下标越界的问题:

java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 9
	at java.util.Vector.remove(Vector.java:836)
	at demos.collection.TestVectorNoSafe$3.run(TestVectorNoSafe.java:74)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 14
	at java.util.Vector.remove(Vector.java:836)
	at demos.collection.TestVectorNoSafe$3.run(TestVectorNoSafe.java:74)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 13
	at java.util.Vector.remove(Vector.java:836)
	at demos.collection.TestVectorNoSafe$3.run(TestVectorNoSafe.java:74)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 11
	at java.util.Vector.remove(Vector.java:836)

以上报错是因为别的线程已经remove了某个下标的元素,而报错的线程在失去cpu时间片、重新获取时间片后还试图去移除这个不存在元素,导致报错。

Vector所谓的线程安全是指调用Vector类的所有成员方法时加入了synchronized修饰,所以其他线程不能再访问该Vector对象,这是相对线程安全。

但在调用两个Vector成员方法时,当前线程有可能在完成第一个方法后时间片到期,这时其他线程可以访问该Vector对象,造成调用第二个成员方法的结果可能与预想结果不同。这时为保证线程安全,需要加synchronized

2.3 相对线程安全

相对线程安全保证了对这个对象单独的操作是线程安全的,即在调用的时候不需要做额外的保障措施。但对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

我们上面例子中的Vector、ConcurrentHashMap就是相对线程安全。

2.4 线程兼容

就是说本身并不线程安全,需要额外的如同步锁之类的手段,就可以确保线程安全。

常用的容器都是此类,如HashMap, HashSet等。

2.5 线程对立

指那些无法通过手段达到线程安全目的的代码。

suspend的时候加同步锁,那他会一直持有锁直到resume。而当对这个线程调用resume之前也申请同步锁,那就肯定发生死锁。

3 线程安全实现

3.1 互斥方式同步(悲观同步)

相关概念可参考

常见实现方法如下:

  • synchronized
  • java.util.concurrentReentrantLock

本方式无论如何都是先加锁,所以也称为悲观同步。

3.2 非阻塞同步(乐观同步)

3.2.1 概念

上述互斥同步方式最大的开销是线程阻塞和唤醒,而且属于是悲观策略。

乐观同步常见的就是CAS(offset, expectVal, newVal)

该方法思想是,如果当前内存offset的值还是为expectVal,那就设为newVal;如果值不是我们期望的,就不改变。

而且在这个过程是原子的,且这个原子性是现代处理器新增的硬件指令支持的

3.2.2 实现

  • java.util.concurrent.atomic包内的一些类如AtomicInteger就有如compareAndSet这样封装好了的cas方法。

  • Unsafe类也有许多cas的方法,并在jdk源码中大量采用,但最好不要直接去用这个类。

3.2.3 问题

CAS有个问题就是ABA问题,详见Java-并发-CAS

3.3 无同步实现线程安全

主要是一些不会访问共享变量、不依赖全局变量的情况,他们是线程安全的。

3.3.1 可重入代码

不依赖全局变量、一个方法的结果只依赖参数,也就是说结果完全可以依据传入参数预测。(但并不能说线程安全代码一定可重入)

3.3.2 线程本地存储

ThreadLocal, 即线程独有变量。详见Java-多线程-ThreadLocal全解析

4 关于锁

关于锁的内容,可以参考另一篇文章Java-并发-LockLike

5 关于可见性、顺序性和原子性问题

请参考:

6 线程安全实例

参考文档

《深入理解Java虚拟机-第二版》 作者周志明

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值