局部变量的引用

先看一个成员变量的例子
class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();

    /**
     * 两个线程同时执行,其中一个线程还没有往list中添加数据,
     * 另一个线程已经从list中取出了数据
     * 就会产生线程安全问题
     *
     * @param loopNumber
     */
    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);
    }

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;


    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }
}

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" Exception in thread "Thread0" java.lang.ArrayIndexOutOfBoundsException: -2
	at java.util.ArrayList.remove(ArrayList.java:507)
	at cn.itcast.test.ThreadUnsafe.method3(ThreadUnsafe.java:29)
	at cn.itcast.test.ThreadUnsafe.method1(ThreadUnsafe.java:19)
	at cn.itcast.test.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:40)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: -1
	at java.util.ArrayList.remove(ArrayList.java:507)
	at cn.itcast.test.ThreadUnsafe.method3(ThreadUnsafe.java:29)
	at cn.itcast.test.ThreadUnsafe.method1(ThreadUnsafe.java:19)
	at cn.itcast.test.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:40)
	at java.lang.Thread.run(Thread.java:748)
分析:

无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量

method3 与 method2 分析相同

变量的安全分析二(局部变量的引用)_多线程

将 list 修改为局部变量
class ThreadUnsafe {
    public final void method1(int loopNumber) {
        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);
    }
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;


    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }

}

那么就不会有上述问题了

分析:

list 是局部变量,每个线程调用时会创建其不同实例,没有共享

而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象

method3 的参数分析与 method2 相同

变量的安全分析二(局部变量的引用)_线程同步_02

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
public class TestThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadSafeSubClass test = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}
class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    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);
    }
}

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) {
        System.out.println(1);
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        System.out.println(2);
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

从这个例子可以看出 private 或 final 提供【安全】的意义所在,开闭原则中的【闭】

1.如果将method2和method3的访问修饰符改为public,不会出现线程安全问题,因为传参与上述代码中的list不是同一个对象

2.如果重现method3,则会出现线程安全问题,多个线程并发修改list


将方法设置为private可以防止子类重新method2和method3

将method1设置为final,可以防止public方法被重写