java多线程学习笔记 --四.线程安全策略

不可变对象

概念

通过在某些情况下,将不会修改的类对象设计成不可变对象,来让对象在多个线程间是线程安全的,其实就是相当于躲避了并发的问题。

不可变对象满足的条件

  • 对象创建后其状态不能修改
  • 对象的所有域都是final类型(这样只能对每个域赋值一次,通过构造器初始化所有成员,进行深度拷贝,在get方法中不直接返回对象本身,而是克隆对象并返回克隆)
  • 对象是正确创建的(在对象创建期间,this引用没有逸出)

final

可以作用于类 方法 变量

  • 修饰类:不能被继承
    • 基础类型的包装类都是final类型的类。final类中的成员变量可以根据需要设置为final,但是要注意的是,final类中的所有成员方法都会被隐式的指定为final方法
  • 修饰方法
    • 把方法锁定,以防任何继承类修改它的含义
    • 效率:在早期的java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不见内嵌调用带来的性能提升。一个private方法会被隐式的指定为final方法
  • 修饰变量
    • 基本数据类型变量,在初始化之后,它的值就不能被修改了。如果是引用类型变量,在它初始化之后便不能再指向另外的对象。

注意 被final修饰的引用类型变量,虽然不能重新指向另外一个对象,但是可以修改里边的值。

Java:unmodifiable相关方法

使用Java的Collection类的unmodifiable相关方法,可以创建不可变对象。unmodifiable相关方法包含:Collection、List、Map、Set….

原理:Collections.unmodifiableMap在执行时,将参数中的map对象进行了转换,转换为Collection类中的内部类 UnmodifiableMap对象。而 UnmodifiableMap对map的更新方法(比如put、remove等)进行了重写,均返回UnsupportedOperationException异常,这样就做到了map对象的不可变

Guava:Immutable相关类

使用Guava的Immutable相关类也可以创建不可变对象。同样包含很多类型:Collection、List、Map、Set….

线程封闭

概念

也是躲避并发,其实就是把对象封装到一个线程里,只有这一个线程能看到这个对象,虽然这个对象不是线程安全的,但也不会出现任何线程安全方面的问题。

Ad-hoc线程封闭

程序控制实现,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象(例如,GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中

堆栈封闭

局部变量,无并发问题

堆栈封闭其实就是方法中定义局部变量。不存在并发问题。多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份到线程的栈中(Java内存模型),所以局部变量是不会被多个线程所共享的。

ThreadLocal线程封闭(重点)

在实际的项目中,比如登录用户的信息,可以存在ThreadLocal中在加上filter,这样可以避免每次都从request中取值,然后从controller层传下去

目的:实现一个线程内的共享变量

特别好的封闭方法,在代码中有具体实现

它是一个特别好的封闭方法,其实ThreadLocal内部维护了一个map,map的key是每个线程的名称,而map的value就是我们要封闭的对象。ThreadLocal提供了get、set、remove方法,每个操作都是基于当前线程的,所以它是线程安全的

线程隔离的秘密就在于ThreadLocalMap这个类,ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

线程不安全的类与写法

StringBuffer和StringBuilder

StringBuilder是线程不安全的,而StringBuffer是线程安全的。分析源码:StringBuffer的方法使用了synchronized关键字修饰

当在一个方法内部本来是线程安全的,所以使用StringBuilder性能更好。推荐在方法内使用StringBuilder。

SimpleDateFormat和JodaTime

SimpleDateFormat 类在处理时间的时候,如下写法是线程不安全的

但是我们可以变换其为线程安全的写法:在每次转换的时候使用线程封闭,新建变量

另外我们也可以使用jodatime插件来转换时间:其可以保证线程安全性
Joda 类具有不可变性,因此它们的实例无法被修改。(不可变类的一个优点就是它们是线程安全的)

Arraylist HashSet HashMap等Collections

都是线程不安全的,以后会有单独的章节。

线程安全 - 同步容器

综合的讲:同步容器存在很多问题,所以说不是线程安全

  • 复合操作是线程不安全的(代码有演示)需要人为干预
  • 在迭代的过程如果被其他线程修改 会出现ConcurrentModificationException
  • 对容器状态进行串行化访问,导致性能降低

采用synchornized,缺点会影响性能,有时还会出现一些小问题

在集合中 foreach和iterator中尽量不要做集合数据的更新操作,会出现java.util.ConcurrentModificationException异常,在单线程会抛出这个异常,在高并发抛出几率会更大,实在要更新,把需要更新的数据标记,循环完后进行更新。推荐使用普通的for循环。

ArraryList -> Vector,Stack

使用vector线程安全性好,但并不是线程安全

stack继承于vector

vector虽然能保证每次只有一个线程进去,两个同步方法因为执行顺序的差异导致不同的线程里边触发线程不安全问题,线程是随机的 他进入某个方法也是随机的,有可能出现数组越界异常。

代码有演示

HashMap -> HashTable

key , value 不能为 null

代码有演示

Collections.synchronizedXXX(List,Set,Map)

代码演示

线程安全 - 并发容器 J.U.C

解决的同步容器可能产生的问题:

  • 并发容器自带有大量的复合操作 不需要人为干预
  • 对容器进行迭代时,不会出错,但不是实时一致性。(CopyOnWriteArraryList的读是读的原数据,写是在复制的容器上进行)
  • 根据某些特定的场景,使用相应的并发容器达到避免加锁,提高性能(CopyOnWriteArraryList没给读加锁)

ArraryList -> CopyOnWriteArraryList

解决了同步容器Vector的两个关键性问题,并发容器定义了一系列

CopyOnWriteArrayList 写操作时复制,当有新元素添加到集合中时,从原有的数组中拷贝一份出来,然后在新的数组上作写操作,将原来的数组指向新的数组。整个数组的add操作都是在锁的保护下进行的,防止并发时复制多份副本。读操作是在原数组中进行,不需要加锁

缺点:
1.写操作时复制消耗内存,如果元素比较多时候,容易导致young gc 和full gc。
2.不能用于实时读的场景.由于复制和add操作等需要时间,故读取时可能读到旧值。
能做到最终一致性,但无法满足实时性的要求,更适合读多写少的场景。
如果无法知道数组有多大,或者add,set操作有多少,慎用此类,在大量的复制副本的过程中很容易出错。

设计思想:
1.读写分离
2.最终一致性

HashSet,TreeSet –> CopyOnWriteArraySet ,ConcurrentSkipListSet

它是线程安全的,底层实现使用的是CopyOnWriteArrayList,因此它也适用于大小很小的set集合,只读操作远大于可变操作。因为他需要copy整个数组,所以包括add、remove、set它的开销相对于大一些。

迭代器不支持可变的remove操作。使用迭代器遍历的时候速度很快,而且不会与其他线程发生冲突


它是JDK6新增的类,同TreeSet一样支持自然排序,并且可以在构造的时候自己定义比较器。

同其他set集合,是基于map集合的(基于ConcurrentSkipListMap),在多线程环境下,里面的contains、add、remove操作都是线程安全的。
多个线程可以安全的并发的执行插入、移除、和访问操作。但是对于批量操作addAll、removeAll、retainAll和containsAll并不能保证以原子方式执行,原因是addAll、removeAll、retainAll底层调用的还是contains、add、remove方法,只能保证每一次的执行是原子性的,代表在单一执行操纵时不会被打断,但是不能保证每一次批量操作都不会被打断。在使用批量操作时,还是需要手动加上同步操作的。

不允许使用null元素的,它无法可靠的将参数及返回值与不存在的元素区分开来

HashMap,TreeMap –> ConcurrentHashMap,ConcurrentSkipListMap

不允许空值,在实际的应用中除了少数的插入操作和删除操作外,绝大多数我们使用map都是读取操作。而且读操作大多数都是成功的。基于这个前提,它针对读操作做了大量的优化。因此这个类在高并发环境下有特别好的表现。
ConcurrentHashMap作为Concurrent一族,其有着高效地并发操作,相比Hashtable的笨重,ConcurrentHashMap则更胜一筹了。

在1.8版本以前,ConcurrentHashMap采用分段锁的概念,使锁更加细化,但是1.8已经改变了这种思路,而是利用CAS+Synchronized来保证并发更新的安全,当然底层采用数组+链表+红黑树的存储结构。


底层实现采用SkipList跳表
曾经有人用ConcurrentHashMap与ConcurrentSkipListMap做性能测试,在4个线程1.6W的数据条件下,前者的数据存取速度是后者的4倍左右。但是后者有几个前者不能比拟的优点:
1、Key是有序的

2、支持更高的并发,存储时间与线程数无关

安全共享对象策略 - 总结

  • 线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改

  • 共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它

  • 线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保障线程安全,多以其他线程无需额外的同步就可以通过公共接口随意访问他

  • 被守护对象:被守护对象只能通过获取特定的锁来访问

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值