1不可变对象
1.1.不可变对象需要满⾜的条件
1. 对象创建以后其状态就不能修改
2. 对象所有域都是 final 类型
3. 对象是正确创建的(在对象创建期间, this 引用没有逸出)
可以采用的方式:
1. 将类声明为 final,这样它就不能被继承
2. 将所有成员声明为 private,这样就不允许直接访问这些成员
3. 对变量不提供 set 方法,将所有可变的成员声明为 final,这样只能赋值一次
4. 通过构造器初始化所有成员,进行深度拷贝
5. 在 get 方法中不直接返回对象本身,而是克隆对象,并返回对象的拷贝
1.2.final
1. 修饰类:不能被继承
a) final 类中成员变量可以根据需要设置为 final
b) final 类的所有方法都会被隐式地指定为 final
c) 使用 final 修饰类时,需要谨慎,除非该类不会被继承;或者从安全的角度考虑,将类设为 final。
2. 修饰方法:
a) 锁定方法不被继承类修改
b) 效率
i. 早期的 Java 版本中,会将 final 方法转为内嵌调用来提升性能,但如果方法过于庞大,提升也非常有限。在新版本 Java中,不再需要将方法设为 final 来提升性能。
c) 一个类的 private 方法会隐式地被指定为 final 方法
3. 修饰变量
a) 对于基本类型的变量:那么数值一旦初始化就不能修改
b) 对于引用类型的对象:一旦初始化结束,就不允许指向另外一个对象
public class FinalTest {private static final Map<String, String> map = Maps.newHashMap();static {map.put("k","2");map.put("k","3");}public static void main(String[] args) {System.out.println(map);}}
结果:
{k=3}//分析://final 作用于引用类型,只是让 map 不能修改所引用的地址。//而并非不允许修改其值。 而对于基础类型(包括 String,如果使用 final修饰,则值也不允许修改)
1.3.内置的不可变对象
1. Collections.unmodifiableXXX
a) 是将 put 等方法直接置空,调用时直接抛异常来实现的,例如:

2. Guava 中有 ImmutableXXX 类,实现不可变的玩法类似
1.4.线程封闭
1. ad-hoc 线程封闭:程序控制实现,最糟糕,忽略
2. 堆栈封闭:其实就是使用局部变量,无并发问题
3. ThreadLocal 线程封闭:特别好的封闭方法
个人理解: ad-hoc 线程封闭: 维护线程封闭性的职责完全由程序实现来承担。
参考文档: http://tyrion.iteye.com/blog/1976457
线程不安全类与写法
1.什么是线程不安全的类?
如果一个类的对象同时被多个线程访问, 如果不做特殊的同步或者并发处理,就会很容易表现出线程不安全的现象,例如抛异常、逻辑处理错误等等。这种类就被称为线程不安全类。
2.常⻅的线程安全/不安全类

3.线程不安全的写法
if(condition(a)) {handle(a);}
如上,如果 a 是一个共享变量,那么就必须在方法上加 synchronized,或者保证两个操作是原子性的,否则将可能导致并发问题。
示例:
@Slf4j@ThreadUnsafepublic class ConcurrentTest5 {private static int threadCount = 200;private static int clientTotal = 5000;private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newCachedThreadPool();Semaphore semaphore = new Semaphore(threadCount);CountDownLatch countDownLatch = new CountDownLatch(clientTotal);for (int i = 0; i < clientTotal; i++) {executorService.execute(new Runnable() {@Overridepublic void run() {try {semaphore.acquire();add();semaphore.release();countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}}});}countDownLatch.await();executorService.shutdown();log.info("count = {}", count);}private static void add() {if (count.get() == 100) {System.out.println(11111);}count.incrementAndGet();}}
结果:
111111111120:13:17.068 [main] INFO com.itmuch.concurrenttest.examples.atomic.ConcurrentTest5 - count = 5000
3.同步容器
3.1.同步容器分类

3.2.Vector ⽰例 :
@Slf4j@ThreadSafepublic class VectorTest1 {private static int threadCount = 200;private static int clientTotal = 5000;private static List<Integer> list = new Vector<>();public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newCachedThreadPool();Semaphore semaphore = new Semaphore(threadCount);CountDownLatch countDownLatch = new CountDownLatch(clientTotal);for (int i = 0; i < clientTotal; i++) {executorService.execute(new Runnable() {@Overridepublic void run() {try {semaphore.acquire();add();semaphore.release();countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}}});}countDownLatch.await();executorService.shutdown();log.info("count = {}", list.size());}private static void add() {list.add(1);}}
如上,假如用的不是 Vector,那么最终打印的 count 很可能不是 5000,线程不安全;而用 Vector 是线程安全的。
3.3.使⽤同步容器不代表线程安全
示例:
@ThreadUnsafepublic class VectorTest2 {private static Vector<Integer> vector = new Vector<>();public static void main(String[] args) {while (true) {for (int i = 0; i < 10; i++) {vector.add(i);}Thread thread1 = new Thread() {public void run() {for (int i = 0; i < vector.size(); i++) {vector.remove(i);}}};Thread thread2 = new Thread() {public void run() {for (int i = 0; i < vector.size(); i++) {vector.get(i);}}};thread1.start();thread2.start();}}}
结果:
Exception in thread "Thread-295" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 14at java.util.Vector.get(Vector.java:748)at com.itmuch.concurrenttest.examples.list.VectorTest2$2.run(VectorTest2.java:27)Exception in thread "Thread-313" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 21at java.util.Vector.get(Vector.java:748)at com.itmuch.concurrenttest.examples.list.VectorTest2$2.run(VectorTest2.java:27)
4.⽰例: ConcurrentModificationException 异常
public class VectorTest3 {// java.util.ConcurrentModificationException// foreachprivate static void test1(Vector<Integer> v1) {for (Integer i : v1) {if (i.equals(3)) {v1.remove(i);}}}// java.util.ConcurrentModificationException// iteratorprivate static void test2(Vector<Integer> v1) {Iterator<Integer> iterator = v1.iterator();while (iterator.hasNext()) {Integer i = iterator.next();if (i.equals(3)) {v1.remove(i);}}}// success// forprivate static void test3(Vector<Integer> v1) {for (int i = 0; i < v1.size(); i++) {if (v1.get(i).equals(3)) {v1.remove(i);}}}public static void main(String[] args) {Vector<Integer> vector = new Vector<>();vector.add(1);vector.add(2);vector.add(3);test2(vector);}}
原因分析:
https://www.cnblogs.com/dolphin0520/p/3933551.html ,即使把本例中的 Vector 换成 List,或者将其换成 Map/Set 或者对应同步容器,
也会出现该问题。
解决方案:
1. 如果使用了 foreach 或迭代器循环集合时,尽量不要在操作过程中做 remove 等更新操作。如果要进行 remove,可以在循环时进行
标记,循环完成后再删除。
2. 使用 COW 容器
5.并发容器及安全共享策略总结

5.1.CopyOnWriteArrayList
写操作时复制,当有新元素添加到 CopyOnWriteArrayList 时,会先从原有的数组中拷贝一份出来,然后在新的数组中做写操作,写完后再将原来的数组指向到新的数组。 COW 整个 add 操作,都是在锁的保护下运行的,这主要是为了避免在多线程并发做 add 操作时,复制出多个副本出来,把数据搞乱,导致最终的数组数据不是期望的。
缺点及使⽤场景:
1. 写时需要拷贝数组,因此会消耗内存,如果原数据数据比较多时,就会导致 Young GC,甚至是 Full GC。
2. 不适用实时读的场景,因为 Copy 需要时间,所以当调用 size 操作时,读取的数据可能是旧的(不准)。虽然能做到最终一致性,但无法满足实时性的要求。
3. 更适合读多、写少的场景。 如果无法保证要放置多少数据,也不知道到底要做 add/set 多少次,那么建议慎用该类。因此如果数据比较多,每次更新都要重新复制,代价会非常高。在高并发场景下可能会引起故障。
4. 通常来说,在实际项目中,多个线程共享的 List 不会很大,修改操作也会比较少。因此在大多数场景下 CopyOnWriteArrayList 都可以很好地代替 ArrayList,满足线程安全。
5. 读操作都是在原数组上读,无需加锁;而写操作为了避免多个线程复制出多个副本出来,把数据搞乱,所以需要加锁
设计思想
1. 读写分离
2. 最终一致性
3. 使用时另外开辟空间,通过这种方式来解决并发冲突
源码
自己看了下源码,发现写操作时,都是用的 ReentrantLock 加锁,然后在里面复制现有数组,并进行写操作,最后将新数组赋值给旧数组,由于加了锁,所以同时只有一个线程可以进行写操作。
读的时候,直接读原数组。
5.2CopyOnWriteArraySet
底层用到了 CopyOnWriteArrayList, 做了 add 等操作时的去重而已。
5.3TreeSet
拓展阅读: https://www.cnblogs.com/yzssoft/p/7127894.html
自己看了下源码, TreeSet 里面用到了 XXXMap,在调用 TreeSet.add(E)时,会调用 XXXMap.put(E, 常量),在这个 put 方法中,会调用 Comparble.compareTo(),和上一个插入的元素进行比较,并重新生成二叉树结构。
核心源码在: java.util.TreeMap.NavigableSubMap#put、 java.util.TreeMap#put
5.4 ConcurrentSkipListSet
1. 和 TreeSet 一样,支持自然排序
2. 可以在构造时,自己指定比较器
3. 和其他 Set 集合一样,也是基于 Map 的
4. 在多线程中, contains、 add、 remove 操作都是线程安全的,多个线程可以并发进行以上几个操作,但对于 containsAll、
removeAll、 addAll 等批量操作无法保证原子方式执行,因此底层还是在调用 contains、 add、 remove 等方法。批量操作时,只能保证每一次的 contains 等操作是原子性的,但无法保证每次批量操作不被其他线程打断。因此并发场景下,如果需要用批量操作,那需要自己手动进行同步,例如加锁
5. 不允许使用 null 元素,因为无法将参数及返回值与不存在的元素区分开来
5.5 ConcurrentHashMap
1. 性能强劲
2. 读取操作进行了大量优化
3. 后面会详细讲,目前视频里就简单提了一下
5.6 ConcurrentHashMap 与 ConcurrentSkipListMap
1. ConcurrentHashMap 性能大致是 ConcurrentSkipListMap 的四倍
2. ConcurrentSkipListMap 的 Key 是有序的, ConcurrentHashMap 不行
3. ConcurrentSkipListMap 支持更高的并发,它的存取时间和线程数几乎没有关系;也就是说,在数据量一定时,并发的线程越多,越能体现出其优势
4. ConcurrentSkipListMap 使用跳表的方式实现
5. 非多线程情况下课尽量使用 TreeMap 代替 ConcurrentSkipListMap
6. 在并发性相对较低的程序,可使用 Collections.synchronizedSortedMap(),是将 TreeMap 进行包装,也可以提供较好的效率
7. 对于高并发程序,应使用 ConcurrentSkipListMap 提供更好的并发度
6.JUC 的构成

7.安全共享对象的策略
1. 线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改
2. 共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它
3. 线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步即可通过公共接口随意访问它
4. 被守护对象:被守护对象只能通过获取特定的锁来访问
以上几点其实是从线程封闭、不可变对象、同步容器、并发容器总结出来的 。




