JUC学习笔记——共享模型之管程( 三 )

线程安全分析这小节我们将会介绍线程安全分析
变量线程安全问题首先我们来思考成员变量和静态变量的安全性:

  • 如果它们没有共享 , 则线程安全
  • 如果它们被共享了:如果只有读操作,则线程安全
  • 如果它们被共享了:如果有读写操作,则这段代码是临界区,需要考虑线程安全
我们再来思索一下局部变量的安全性:
  • 局部变量是线程安全的
  • 局部变量引用的对象:如果该对象没有逃离方法的作用访问,它是线程安全的
  • 局部变量引用的对象:如果该对象逃离方法的作用范围,需要考虑线程安全
我们通过简单代码进行测试:
/*局部变量*/ // 源代码展示:public static void test1() {int i = 10;i++;}// 我们查看底层代码:每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享public static void test1(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=0 0: bipush 10 2: istore_0 3: iinc 0, 1 6: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 LocalVariableTable: Start Length Slot Name Signature 340iI/*成员变量*/// 源代码展示:static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {// 一个test对象ThreadUnsafe test = new ThreadUnsafe();// 两个线程去操纵for (int i = 0; i < THREAD_NUMBER; i++) {// 均执行method1方法new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}class ThreadUnsafe {// 这里的list是属于对象的,创建在堆中,属于线程共同操纵对象ArrayList<String> list = new ArrayList<>();// 不断调用method2,3方法200次public void method1(int loopNumber) {for (int i = 0; i < loopNumber; i++) {// { 临界区, 会产生竞态条件method2();method3();// } 临界区}}private void method2() {list.add("1");}private void method3() {list.remove(0);}}// 运行结果的其中一种:如果线程2 还未 add,线程1 remove 就会报错Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748) /*成员变量局部优化*/// 源代码展示static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {// 一个test对象ThreadUnsafe test = new ThreadUnsafe();// 两个线程去操纵for (int i = 0; i < THREAD_NUMBER; i++) {// 均执行method1方法new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}class ThreadSafe {public final void method1(int loopNumber) {// 这里将list变为局部变量,每个线程独自在自己的栈中创建,就不会产生安全问题ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list);method3(list);}}private void method2(ArrayList<String> list) {list.add("1");}private void method3(ArrayList<String> list) {list.remove(0);}}/*方法public化缺陷*/// 源代码展示:class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list);method3(list);}}public void method2(ArrayList<String> list) {list.add("1");}public void method3(ArrayList<String> list) {list.remove(0);}}class ThreadSafeSubClass extends ThreadSafe{@Overridepublic void method3(ArrayList<String> list) {new Thread(() -> {list.remove(0);}).start();}}// 如果我们将method2,3改为public,就可能会导致其他线程直接调用method2,3导致安全性问题// 同时甚至可能出现其他子类继承父类导致修改原方法,同时创建一个线程导致多线程问题出现常见线程安全类我们在下面介绍一下我们常用的线程安全类:
  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类
同时我们需要知道无法改变的类型也是线程安全的:
  • String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
  • 这里需要注意:String的各类修改方法都是直接创建一个新的String类型而不是在String本体进行增删
我们再进行简单解释:
  • 线程安全类的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的!
我们采用代码进行简单解释:
/*下述的单个方法都是原子且安全性的*/ Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");}).start();new Thread(()->{table.put("key", "value2");}).start();/*但当这些方法组合起来,就无法保证其线程安全性*/ Hashtable table = new Hashtable();// 线程1,线程2都执行时,可能出现下述情况// 线程1get==null,线程2get==null , 线程2put,线程1put;导致线程安全性错误出现if( table.get("key") == null) { table.put("key", value);}

推荐阅读