变量的线程安全分析

1、成员变量和局部变量

  • 成员变量和静态变量的线程安全性
    • 如果它们没有共享,则线程安全。
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。
      • 如果只有读操作,则线程安全。
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全。
  • 局部变量的线程安全性
    • 局部变量是线程安全的。
    • 但局部变量引用的对象则未必。
      • 如果该对象没有逃离方法的作用访问,它是线程安全的。
      • 如果该对象逃离方法的作用范围,需要考虑线程安全。

2、变量的线程安全分析

  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class TestThreadSafe {
    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+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);
    }
    }
    • 由于不同线程调用method2()和method3()时是对同一个成员变量list进行操作,这样就有可能存在当集合元素未添加时就先进行remove操作从而报错如下:

      1
      2
      3
      4
      5
      6
      7
      Exception in thread "Thread2" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
      at java.util.ArrayList.rangeCheck(ArrayList.java:659)
      at java.util.ArrayList.remove(ArrayList.java:498)
      at org.example.ThreadUnsafe.method3(TestThreadSafe.java:37)
      at org.example.ThreadUnsafe.method1(TestThreadSafe.java:28)
      at org.example.TestThreadSafe.lambda$main$0(TestThreadSafe.java:17)
      at java.lang.Thread.run(Thread.java:748)
    • 而当把list集合调至局部变量位置时,线程就安全了,因为此时不同线程调用的是各自栈帧中的list对象,不存在共享,代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      public class TestThreadSafe {
      static final int THREAD_NUMBER = 2;
      static final int LOOP_NUMBER = 200;
      public static void main(String[] args) {
      ThreadSafe test = new ThreadSafe();
      for (int i = 0; i < THREAD_NUMBER; i++) {
      new Thread(() -> {
      test.method1(LOOP_NUMBER);
      }, "Thread" + (i+1)).start();
      }
      }
      }

      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");
      }

      private void method3(ArrayList<String> list) {
      list.remove(0);
      }
      }
  • 如果在上面的基础上,为ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
    ThreadSafe test = new ThreadSafeSubClass();
    for (int i = 0; i < THREAD_NUMBER; i++) {
    new Thread(() -> {
    test.method1(LOOP_NUMBER);
    }, "Thread" + (i+1)).start();
    }
    }
    }

    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{
    @Override
    public void method3(ArrayList<String> list) {
    new Thread(() -> {
    list.remove(0);
    }).start();
    }
    }

    由于重写后的method3方法又开启了一个线程来访问局部变量list,导致局部变量list能被不同线程共享,因此也是线程不安全的。

3、常见的线程安全类

  • 常见的线程安全类有:

    • String
    • Integer
    • StringBuffer
    • Random
    • Vector
    • Hashtable
    • java.util.concurrent 包下的类
  • 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:

    1
    2
    3
    4
    5
    6
    7
    Hashtable table = new Hashtable();
    new Thread(()->{
    table.put("key", "value1");
    }).start();
    new Thread(()->{
    table.put("key", "value2");
    }).start();
  • 它们的每个方法是原子的,但注意它们多个方法的组合不是原子的,会带来线程安全问题,如下例子:

    1
    2
    3
    4
    5
    Hashtable table = new Hashtable();
    // 线程1,线程2
    if( table.get("key") == null) {
    table.put("key", value);
    }
  • 分析下面实例的线程安全性:

    • 例子1

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //Servlet是运行在tomcat中的且只有一份实例,此实例会被多个线程共享,所以会带来线程安全问题
      public class MyServlet extends HttpServlet {
      //线程不安全
      Map<String,Object> map = new HashMap<>();
      //线程安全
      String S1 = "...";
      //线程安全
      final String S2 = "...";
      //线程不安全
      Date D1 = new Date();
      //线程不安全,使用final的修饰的引用数据类型只表明引用地址不可变,但是该对象的属性值可以被改变
      final Date D2 = new Date();

      public void doGet(HttpServletRequest request, HttpServletResponse response) {
      // 使用上述变量
      }
      }
    • 例子2

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class MyServlet extends HttpServlet {
      // Servlet只有一份实例导致userService也只有一份实例,且会被不同线程共享,所以线程不安全
      private UserService userService = new UserServiceImpl();

      public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
      }
      }
      public class UserServiceImpl implements UserService {
      // 记录调用次数,线程不安全
      private int count = 0;

      public void update() {
      count++;
      }
      }
    • 例子3

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      @Aspect
      @Component
      public class MyAspect {
      // Spring容器中的对象默认是单例的,意味着该成员变量会被共享,所以会出现线程安全问题,改进方法是做成环绕通知,该变量变成局部变量来使用
      private long start = 0L;

      @Before("execution(* *(..))")
      public void before() {
      start = System.nanoTime();
      }

      @After("execution(* *(..))")
      public void after() {
      long end = System.nanoTime();
      System.out.println("cost time:" + (end-start));
      }
      }
    • 例子4

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      public class MyServlet extends HttpServlet {
      // 线程安全的,因为即使userService里面有成员变量,但由于此变量是私有的,其它地方无法对其进行修改
      private UserService userService = new UserServiceImpl();

      public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
      }
      }
      public class UserServiceImpl implements UserService {
      // 线程安全的,因为虽然userDao作为成员变量,但里面没有可以更改的成员属性
      private UserDao userDao = new UserDaoImpl();

      public void update() {
      userDao.update();
      }
      }
      public class UserDaoImpl implements UserDao {
      public void update() {
      String sql = "update user set password = ? where username = ?";
      // conn是线程安全的
      try (Connection conn = DriverManager.getConnection("","","")){
      // ...
      } catch (Exception e) {
      // ...
      }
      }
      }
    • 例子5

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      public class MyServlet extends HttpServlet {
      // 线程安全
      private UserService userService = new UserServiceImpl();

      public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
      }
      }
      public class UserServiceImpl implements UserService {
      // 线程安全
      private UserDao userDao = new UserDaoImpl();

      public void update() {
      userDao.update();
      }
      }
      public class UserDaoImpl implements UserDao {
      // 线程不安全,因为conn只会有一份且可以同时被不同线程更改
      private Connection conn = null;
      public void update() throws SQLException {
      String sql = "update user set password = ? where username = ?";
      conn = DriverManager.getConnection("","","");
      // ...
      conn.close();
      }
      }
    • 例子6

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      public class MyServlet extends HttpServlet {
      // 线程安全
      private UserService userService = new UserServiceImpl();

      public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
      }
      }
      public class UserServiceImpl implements UserService {
      //线程安全,每创建一个线程调用此方法时都会创建一个不同的userDao对象
      public void update() {
      UserDao userDao = new UserDaoImpl();
      userDao.update();
      }
      }
      public class UserDaoImpl implements UserDao {
      // 线程安全
      private Connection = null;
      public void update() throws SQLException {
      String sql = "update user set password = ? where username = ?";
      conn = DriverManager.getConnection("","","");
      // ...
      conn.close();
      }
      }
    • 例子7

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public abstract class Test {

      public void bar() {
      // 线程不安全
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      foo(sdf);
      }

      public abstract foo(SimpleDateFormat sdf);


      public static void main(String[] args) {
      new Test().bar();
      }
      }
      • 其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public void foo(SimpleDateFormat sdf) {
      String dateStr = "1999-10-11 00:00:00";
      for (int i = 0; i < 20; i++) {
      new Thread(() -> {
      try {
      sdf.parse(dateStr);
      } catch (ParseException e) {
      e.printStackTrace();
      }
      }).start();
      }
      }
    • 例子8

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      private static Integer i = 0;
      public static void main(String[] args) throws InterruptedException {
      List<Thread> list = new ArrayList<>();
      for (int j = 0; j < 2; j++) {
      Thread thread = new Thread(() -> {
      for (int k = 0; k < 5000; k++) {
      synchronized (i) {
      i++;
      }
      }
      }, "" + j);
      list.add(thread);
      }
      list.stream().forEach(t -> t.start());
      list.stream().forEach(t -> {
      try {
      t.join();
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      });
      log.debug("{}", i);
      }