一、不可变对象
1.不可变对象需要满足的条件
1)对象创建以后其状态不可修改。 2)对象所有域都是final类型。 3)对象是正确创建的(在对象创建期间,this引用没有逸出)。
2.定义不可变对象
创建不可变对象方法:将类声明为final(该类不可以被继承),将类中所有变量声明为私有变量(不允许直接访问成员),所有变量不提供set方法,所有可变成员声明为final(保证只可以赋值一次),在get方法中不直接返回对象本事而是返回对象的拷贝。 1)final关键字:用于修饰类、方法、变量 修饰类:不能被继承如String等,final类中的成员方法都会被隐式的声明为final类型。 修饰方法:锁定方法不能被继承类修改。 修饰变量:基本数据类型变量(初始化后值不能修改)、引用类型变量(指向一个对象后就不能再指向其他对象,但对象内的内容还可以修改) 2)Collections.unmodifiableXXX:Collection、List、Set、Map....
Collections.unmodifiableMap()方法的核心是,生成了一个新的unmodifiablemap,这个map中的所有更新相关方法中没有任何操作而是直接抛出异常,这样就保证了map的不可变,多个线程之间的数据不会被污染。 3)Guava:ImmutableXXX:Collection、List、Set、Map... Immutable 类型直接声明更新方法不可用,调用后会抛出异常。
二、线程封闭
什么是线程封闭:线程封闭就是把对象封装到一个线程中去,只有这一个线程能看到此对象,那么这个对象就算不是线程安全的也不会出现任何安全问题。
1.实现线程封闭的方法
1)Ad-hoc线程封闭:程序控制实现,最糟糕,可直接忽略。 2)堆栈封闭:局部变量,无并发问题。在我们的日常开发过程中遇到的最多的其实就是堆栈封闭,多个线程访问一个方法时方法中的局部变量都会被拷贝一份到线程的栈中,所以局部变量不会被多线程共享,也就不会出现并发问题,所以能用局部变量就不要用全局变量,全局变量很容易出现并发问题。 3)ThreadLocal线程封闭:特别好的封闭方法(有时间应该研究一下其源码)。ThreadLocal内部维护了一个map,key是线程的名称,value是要封闭的对象,每一个线程中的对象都对应着一个map中的value,也就是说ThreadLocal利用map实现了线程封闭。 举个例子,利用ThreadLocal+Filter+Interceptor获取用户登陆时的用户信息,用户登陆时其实每一个线程就是一个请求,我们先将所有请求在Filter中进行过滤直接取出当前用户,把信息存入ThreadLocal中,当这个线程在Service中需要处理信息时,再将信息从ThreadLocal中取出。这样做的好处是可以统一管理请求,并且不需要将用户信息一直做为参数一层一层进行传递而是随用随取,使代码更便捷易于管理。最后在Interceptor中将ThreadLocal中的信息移除, 注意ThreadLocal在使用完数据后需要进行移除操作,不然会有内存逸出问题,因为只有项目重新启动ThreadLocal中存放的信息才会被释放 a.首先定义一个RequestHolder类用于存储需要的信息,在这个类中维护一个ThreadLocal对象,ThreadLocal对象的set()方法默认是在map中插入一个entry,key是线程名称,value是传入的参数,get()方法可以获取到当前线程对应的保存的对象,remove()会将该对象从ThreadLocal中删除。
b.Filter中拦截request中的信息,例子中没有涉及到登陆信息,所以直接调用 RequestHolder中的add()方法存入线程的id,Filter中如果不想拦截住请求而只是进行数据处理,记得加上filterChain.doFilter(servletRequest, servletResponse);让请
c.定义 Interceptor,复写其中的方法,interceptor中请求之前调用preHandle(),请求之后调用afterCompletion(),这里我们在请求之后调用RequestHolder中的remove()方法移除信息。
方法执行顺序:chain.doFilter()之前 -> chain.doFilter() -> preHandle() -> 实际方法 -> afterCompletion() -> chain.doFilter()之后
三、线程不安全类与写法
1.StringBuilder(线程不安全)-->StringBuffer(线程安全),StringBuffer底层的方法是由Synchronized关键标记的方法,性能上会有损耗,所以在局部变量中应该尽可能使用StringBuilder,在性能上会有所提升。 2.SimpleDateFormat(线程不安全)-->JodaTime(线程安全),SimpleDateFormate和DateFormate在多线程环境下都会报错,所以应尽量将SimpleDateFormate设置为局部变量,或改用JodaTime(注意是第三方库,import时需要注意)。 3.ArrayList、HashSet、HashMap等Collections(线程不安全) 4.先检查再执行的代码写法是线程不安全的,比如先判断一个对象是否满足某个条件,然后再对这个对象做某种处理,如果这个对象是多线程共享的,那么“判断”和“后续处理”这两个有关联的操作之间就会出现线程不安全的问题,需要把方法整体加锁。
四、同步容器
Java中提供了同步容器,解决ArrayList、HashSet、HashMap等集合类线程不安全问题
1.ArrayList-->Vector、Stack
Vector实现了List中方法,其中的方法都是Synchronized处理的方法。Stack是继承与Vector类,其实就是数据结构中的栈,其中的方法也都是由Synchronized处理的。 注:同步容器并不一定就是线程安全的,以vector举例,当一个线程某一时刻执行get()方法,另一个线程调用remove()方法,这时就会出现报错问题,所以同步容器的单一操作是线程安全的,但复合操作是不能保证线程安全的,这时应该加锁进行处理保证同一时刻不会有其他线程对Vector进行处理。
2.HashMap-->HashTable(key和value都不可为null)、ConcurrentHashMap
HashTable实现了Map接口,相应方法被Synchronized处理。
3.Collections.SynchronizedXXX(List、Set、Map)
Collections类中提供了许多静态构造方法创建同步容器类。如Collections.SynchronizedList(new ArrayList);这样就可以把一个ArrayList变为线程安全的。 补充:在我们的日常操作时,对集合进行遍历,如果使用foreach或迭代器iterator进行循环不可以进行集合的remove()操作,会抛出java.util.ConcurrentModificationException异常,如果一定要进行remove()操作,可以在循环过程中记录位置,循环结束后再进行remove()操作,或者可以在for循环中进行remove()操作。foreach和iterator进行循环集合,foreach实际上内部使用的是iterator,iterator进行集合遍历时会生成一个变量expectedModCount等于ArrayList/Vector的modCount,使用ArrayList/vector的remove()方法不会同步的更新 expectedModCount ,所以当Iterator迭代时可能会出现找不到对象的情况而报错,Iterator的next()和remove()都有一个checkForComodification()判断当ArrayList/Vector的modCount和IteratorexpectedModCount 不相等时就会抛出ConcurrentModificationException。
五、并发容器
1.并发容器(J.U.C-Java.util.concurrent)
1)ArrayList-->CopyOnWriteArrayList(线程安全),CopyOnWriteArrayList原理就是拷贝一份原来的数组,进行完操作后指向新的操作,所有操作都是Synchronized方法,缺点就是拷贝时会消耗内存,可能会导致youngGC和fullGC,并且不能用于实时读的场景,适合读多写少的场景。内部底层更新相关方法是通过Lock锁保证同一时刻只有一个线程操作数组,读操作没有加锁操作。 CopyOnWrite的三个思想:1.读写分离,读操作时在原数组上操作,写操作时在拷贝的新数组上操作需要加锁。2.最终一致性,因为在拷贝过程中会消耗一段时间,所以保证的是最终结果的正确性。3.使用时另外开辟空间,避免并发问题。 2)HashSet、TreeSet-->CopyOnWriteArraySet、ConcurrentSkipListSet(线程安全) 3)HashMap、TreeMap-->ConcurrentHashMap、ConcurrentSkipListMap(线程安全 )