暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Java并发与高并发解决方案(四): 线程安全策略

程序员雨衣 2019-05-21
394

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
          @ThreadUnsafe
          public 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() {
          @Override
          public 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();
          }
          }

          结果:

            11111
            11111
            20:13:17.068 [main] INFO com.itmuch.concurrenttest.examples.atomic.ConcurrentTest5 - count = 5000


            3.同步容器


            3.1.同步容器分类


            3.2.Vector ⽰例 :

              @Slf4j
              @ThreadSafe
              public 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() {
              @Override
              public 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.使⽤同步容器不代表线程安全

              示例:

                @ThreadUnsafe
                public 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: 14
                  at 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: 21
                  at 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
                    // foreach
                    private static void test1(Vector<Integer> v1) {
                    for (Integer i : v1) {
                    if (i.equals(3)) {
                    v1.remove(i);
                    }
                         }
                    }
                    // java.util.ConcurrentModificationException
                    // iterator
                    private 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
                    // for
                    private 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#putjava.util.TreeMap#put


                    5.4 ConcurrentSkipListSet

                    1. TreeSet 一样,支持自然排序
                    2. 可以在构造时,自己指定比较器
                    3. 和其他 Set 集合一样,也是基于 Map

                    4. 在多线程中, containsaddremove 操作都是线程安全的,多个线程可以并发进行以上几个操作,但对于 containsAll
                    removeAlladdAll 等批量操作无法保证原子方式执行,因此底层还是在调用 containsaddremove 等方法。批量操作时,只能保证每一次的 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. 被守护对象:被守护对象只能通过获取特定的锁来访问

                    以上几点其实是从线程封闭、不可变对象、同步容器、并发容器总结出来的 。

                    文章转载自程序员雨衣,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论