android 多线程学习3:synchronized与volatile与线程安全对象

android 多线程学习1:一些基础
android 多线程学习2:线程的创建与方法分析
android 多线程学习3:synchronized与volatile与线程安全对象
android 多线程学习4:线程池ThreadPoolExecutor
android 多线程学习5:AsyncTask
android 多线程学习6:HandlerThread
android 多线程学习7:Handler消息处理机制


一、java内存模型(JMM)

Java采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。


内存模型JMM示意图

Java中所有的变量都存储在主内存(Main Memory)中。每个线程有自己的工作内存(Working Memory,工作内存往往是CPU内的高速缓存),线程的工作内存中保存了该线程使用到的变量在的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间数据的传递都需要通过主内存来完成。

总结一下就是,一个线程对一个共享变量的操作大致分为三步
  1.当前线程首先从主内存拷贝共享变量到自己的工作内存
  2.然后对工作内存里的变量进行处理
  3.处理完后更新变量值到主内存
上述三步操作是不具原子性的,即:当线程A在修改了共享变量1的值后,还未将共享变量1的值从工作内存刷新到主内存,此时线程B开始读取共享变量1,线程B读取到的值就是共享变量1未被线程A修改之前的值。所以会造成一定程度的数据错乱,而关键字synchronized就具有使每个线程依次排队操作共享变量的功能。

并发的三种特性:
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行。

二、Synchronized 阻塞式同步

使得同一时刻只有一个线程能够获得对象监视器。任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

作用:释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。

使用位置

三、volatile 轻量级同步机制

特性:
1. 保证可见性,不保证原子性;
当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;这个写会操作会导致其他线程中的缓存无效。

当线程A将volatile修饰的共享变量修改写入之后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。


volatile机制

线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值已失效,然后线程B读这个volatile变量时就从主内存读取。

2. 禁止指令重排
重排序是为了优化性能。重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序。

比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

使用volatile关键字修饰共享变量便可以禁止这种重排序。即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
a. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b. 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

声明一个引用变量为volatile,不能保证通过该引用变量访问到的非volatile变量的可见性。同理,声明一个数组变量为volatile不能确保数组内元素的可见性。volatile的特性不能在数组内传递,因为数组里的元素不能被声明为volatile。

volatile不适合复合操作
例如,inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以下例结果并不能达到30000。

volatile复合操作

解决办法:
采用synchronized来保证原子性

单例模式的双重锁为什么要加volatile

双锁单例

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码
a.memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。

volatile使用场景
volatile最适合使用的地方是一个线程写、其它线程读的场合,如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。

四、java中一些线程安全的对象

线程安全:Vector、HashTable、StringBuffer
非线程安全:ArrayList 、LinkedList、HashMap、HashSet、TreeMap、TreeSet、StringBulider

Vector、ArrayList、LinkedList
  1、Vector:与ArrayList一样,也是通过数组实现的。它支持线程的同步,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
  2、ArrayList: 由于通过数组实现,它允许对元素进行快速随机访问。缺点是当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
  3、LinkedList: 用链表结构存储数据。适合数据的动态插入和删除,随机访问和遍历速度比较慢。它还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

HashTable、HashMap、HashSet
HashTable和HashMap采用的存储机制是一样的,不同的是:
  1、HashMap:
     a. 采用数组方式存储key-value构成的Entry对象,无容量限制;
     b. 基于key hash查找Entry对象存放到数组的位置,对于hash冲突采用链表的方式去解决;
     c. 在插入元素时,可能会扩大数组的容量,在扩大容量时须要重新计算hash,并复制对象到新的数组中;
     d. 是非线程安全的;
     e. 遍历使用的是Iterator迭代器;
  2、HashTable:
     a. 是线程安全的;
     b. 无论是key还是value都不允许有null值的存在;在HashTable中调用Put方法时,如果key为null,直接抛出NullPointerException异常;
     c. 遍历使用的是Enumeration列举;
  3、HashSet:
     a. 基于HashMap实现,无容量限制;
     b. 是非线程安全的;
     c. 不保证数据的有序;

TreeSet、TreeMap
TreeSet和TreeMap都是完全基于Map来实现的,并且都不支持get(index)来获取指定位置的元素,需要遍历来获取。另外,TreeSet还提供了一些排序方面的支持,例如传入Comparator实现、descendingSet以及descendingIterator等。
  1、TreeSet: 基于TreeMap实现的,支持排序;是非线程安全的;
  2、TreeMap: 典型的基于红黑树的Map实现,因此它要求一定要有key比较的方法,要么传入Comparator比较器实现,要么key对象实Comparator接口;是非线程安全的;

StringBuffer和StringBulider
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串。
  1、在执行速度方面的比较:StringBuilder > StringBuffer ;
  2、他们都是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,不像String一样创建一些对象进行操作,所以速度快;
  3、StringBuilder:线程非安全的;
  4、StringBuffer:线程安全的;

对于String、StringBuffer和StringBulider三者使用的总结:
   1.操作少量数据 : String
   2.操作大量数据&单线程操作 : StringBuilder
   3.操作大量数据&多线程操作 : StringBuffer



(部分内容参考于网络,如有不妥,请联系删除~)

你可能感兴趣的:(android 多线程学习3:synchronized与volatile与线程安全对象)