Android 线程安全+volatile

1.数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,那么这两个操作之间就存在数据依赖。
数据依赖分下列三种类型:
①写后读(a = 1;b = a;)
写一个变量之后,再读这个位置。
②写后写 (a = 1;a = 2;)
写一个变量之后,再写这个变量。
③读后写(a = b;b = 1;)
读一个变量之后,再写这个变量。
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会改变,所以编译器和处理器在重排序时会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。也就是说在单线程环境下指令执行的最终效果,应当与其在顺序执行下的效果一致,否则这种优化便会失去意义。

2.线程的安全性问题
线程安全问题不是说线程不安全,而是多个线程之间交错操作有可能导致数据异常。
要实现线程安全就要保证三点:原子性、可见性和有序性。
①原子性Atomic
原子指不可分割,就是访问(读/写)某个共享变量的操作,从执行线程以外的其他线程看来,该操作只有未开始和结束两种状态,不会知道该操作的中间部分,那么该操作就是原子操作。
访问同一组共享变量的原子操作是不能被交错的,这就排除了一个线程执行一个操作的期间,另一个线程读取或更新该操作锁访问的共享变量,导致脏数据和丢失更新。
比如a++,对于共享变量a的操作,实际上会执行三个操作:读取变量a的值、a的值+1、将值赋予变量a。这三个操作中任何一个操作过程中a的值被篡改,都会出现错误的结果。而在多线程中a的值是有可能被其他线程修改的,从而导致线程不安全。所以为了保证线程安全,必须把这三个步骤当成不可分割的一个整体操作,在其他线程看来,该操作只有未开始和结束两种状态,不知道中间发生什么,这就体现了原子性。
在单线程环境下可以认为整个步骤都是原子性操作;但是在多线程环境下Java只保证了基本数据类型的变量和赋值操作才是原子性的(基本数据类型的变量和赋值操作类似i = 0的操作)。
②可见性
可见性是指一个线程对共享变量的更新,对于其他读取该变量的线程是否可见。保证可见性意味着一个线程可以读取到对应共享变量的最新值。
可见性问题与计算机的存储系统有关,程序中的变量可能被分配到寄存器而不是主内存中,每个处理器都有自己的寄存器,一个处理器无法读取另一个处理器的寄存器上的内容。即使共享变量是分配到主内存中存储的,也不能保证可见性,因为处理器不是直接访问主内存,而是通过高速缓存进行的。一个处理器上运行的线程对变量的更新,可能只是更新到该处理器的写缓冲器中,还没有到高速缓存中,更别说处理器了。
从保证线程安全的角度看,光保证原子性还不够,还要保证可见性,同时保证可见性和原子性才能确保一个线程能正确地看到其他线程对共享变量做的更新。
Java提供了volatile关键字保证可见性。通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁,执行同步代码,并且在释放锁之前会将对变量的修改刷新到主内存中,因此可以保证可见性。
③ 有序性
有序性指一个处理器为一个线程执行的内存访问操作,对于另一个处理器上运行的线程来看是乱序的。
现代处理器为了提高指令的执行效率,往往不按程序顺序执行指令,而是哪条指令就绪就先执行哪条指令,这就是处理器的乱序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序。
重排序(Reordering)是处理器和编译器对代码做的一种优化,它可以在不影响单线程程序正确性的情况下提升程序的性能,但是它会对多线程程序的正确性产生影响,导致线程安全问题。
比如:
class ReorderExample {
int a = 0;
boolean flag = false;

public void writer() {
a = 1; // 1
flag = true; // 2
}

public void reader() {
if (flag) { // 3
int i = a * a; // 4
}
}
}
flag变量是个标记,用来标识变量a是否已被写入。假设有两个线程A和B,A首先执行writer()方法,随后B线程执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入?答案是:不一定能看到。
由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。
当操作1和操作2重排序后,执行顺序可能是2 -> 3 -> 4 -> 1 (根据CPU对多个线程进行时间分配的原理来看,这是完全可能存在并且合理的一种顺序),操作3和操作4重排序后,因为操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时会影响指令序列执行的并行度,为此编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲的硬件缓存中。当接下来操作3的条件判断为真时就把该计算结果写入变量i中。所以,猜测执行实质上对操作3和4做了重排序。
在Java中可以通过volatile关键字来保证一定的“有序性”,因为volatile具有禁止代码重排序的功能。另外可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

3.线程锁
常见的实现线程安全的办法是使用锁和原子类型。锁能保证当前只有一个线程对数据操作,直到线程操作完,下个线程接着操作。这就是锁的作用,让多个线程更好地协作,避免多个线程的操作交错导致数据异常的问题。

锁的五个特点:
(1)临界区:持有锁的线程获得锁后和释放锁前执行的代码叫做临界区。
(2)排他性:锁具有排他性,保证一个共享变量在任一时刻只能被一个线程访问,这就保证了临界区代码一次只能够被一个线程执行,临界区的操作具有不可分割性,也就保证了原子性。
(3)串行:在没有锁的时候,多线程下并发对数据进行操作,这是很危险的。而在有锁的情况下,只能一个线程访问数据,锁相当于是把多个线程对共享变量的操作从并发改为串行。
(4)三种保障:锁能够保护共享变量实现线程安全,它的作用包括保障原子性、可见性和有序性。
(5)调度策略:锁的调度策略分为公平策略和非公平策略,对应的锁就叫公平锁和非公平锁。
公平锁会在加锁前查看是否有排队等待的线程,有的话会优先处理排在前面的线程。公平锁以增加上下文切换为代价,保障了锁调度的公平性,增加了线程暂停和唤醒的可能性。
公平锁的开销比非公平锁大,所以ReentrantLock 的默认调度策略是非公平策略。

锁的两个问题:
(1)泄漏锁
锁泄漏是指一个线程获得锁后,由于程序的错误导致锁一直无法被释放,导致其他线程一直无法获得该锁。
(2)活跃性问题
锁泄漏会导致活跃性问题,这些问题包括死锁和锁死等。

锁的类型可分为内部锁synchronized、显式锁ReentrantLock、读写锁ReentrantReadWriteLock、轻量级锁volatile四种。

主要介绍轻量级锁volatile:
volatile关键字可用于修饰共享变量,对应的变量就叫volatile变量。
volatile变量的特点:
1)volatile的字面意思是“不稳定的”,也就是volatile用于修饰容易发生变化的变量。这里的不稳定指的是对这种变量的读写操作要从高速缓存或主内存中读取,而不会分配到寄存器中。
2)volatile的开销比锁低,volatile变量的读写操作不会导致上下文切换,所以volatile关键字也叫轻量级锁。
3)volatile变量读操作的开销比普通变量要高,因为volatile变量的值每次都要从高速缓存或主内存中读取,无法被暂存到寄存器中。
4)对于volatile变量的写操作,JVM会在该操作前插入一个释放屏障,并在该操作后插入一个存储屏障。存储屏障具有冲刷处理器缓存的作用,所以在volatile变量写操作后插入一个存储屏障,能让该存储屏障前的所有操作结果对其他处理器来说是同步的。
5)对于volatile变量的读操作,JVM会在该操作前插入一个加载屏障,并在操作后插入一个获取屏障。加载屏障通过冲刷处理器缓存,使线程所在的处理器将其他处理器对该共享变量做的更新同步到该处理器的高速缓存中。
6)保证有序性:volatile能禁止指令重排序,也就是使用volatile能保证操作的有序性。
7)保证可见性:读线程执行的加载屏障和写线程执行的存储屏障配合在一起,能让写线程对volatile变量的写操作对读线程可见,从而保证了可见性。
8)原子性:在原子性方面,对于long/double型变量,volatile能保证读写操作的原子型;对于非 long/double型变量,volatile只能保证写操作的原子性。如果volatile变量写操作前涉及共享变量,竞态仍然可能发生,因为共享变量赋值给volatile变量时,其他线程可能已经更新了该共享变量的值。
注:volatile不能保证原子性,因为当一个变量进行x++的操作时实际是分为temp = x,temp = x + 1,x = temp这三个操作,那么在中途另一个线程插进来了,值就不正确了。

当一个共享变量被volatile修饰后, 就具备了两个含义:
①线程修改该变量的值时, 变量的新值对其他线程是立即可见的。 换句话说, 就是不同线程对这个变量进行操作时具有可见性,即该关键字保证了可见性。
②禁止使用指令重排序。 volatile关键字禁止指令重排序有两个含义: 一个是当程序执行到volatile变量的操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还没有进行;在进行指令优化时,在volatile变量之前的语句不能在volatile变量后面执行;同样在volatile变量之后的语句也不能在volatile变量前面执行,即该关键字保证了有序性。

通常来说使用volatile必须具备以下两个条件:
①对变量的写操作不会依赖于当前值。 例如自增自减。
②该变量没有包含在具有其他变量的不变式中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值