JUC(七):变量的线程安全分析

1. 成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

2. 局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全 (如:return)

3. 局部变量线程安全分析

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 16: 0
        line 17: 3
        line 18: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3       4     0     i   I

如图
在这里插入图片描述

局部变量的引用稍有不同

先看一个成员变量的例子

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

执行

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" 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.xiaozheng.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.xiaozheng.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.xiaozheng.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748) 

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同
    在这里插入图片描述

将 list 修改为局部变量

class ThreadSafe {
    
    
    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);
    }
}

那么就不会有上述问题了?

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同
    在这里插入图片描述

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

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,
class ThreadSafe {
    
    
    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);
    }
}
class ThreadSafeSubClass extends ThreadSafe{
    
    
    @Override
    public void method3(ArrayList<String> list) {
    
    
        // 出现共享资源,不安全
        new Thread(() -> {
    
    
            list.remove(0);
        }).start();
    }
}

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

4. 常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable();
new Thread(()->{
    
    
    table.put("key", "value1");
}).start();
new Thread(()->{
    
    
    table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的,见后面分析

4.1 线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
    
    
	table.put("key", value);
}
线程1 线程2 table table get("key") == null get("key") == null put("key",v2) put("key",v1) 线程1 线程2 table table

4.2 不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢?

public class Immutable{
    
    
    private int value = 0;
    public Immutable(int value){
    
    
        this.value = value;
    }
    public int getValue(){
    
    
        return this.value;
    }
}

如果想增加一个增加的方法呢?

public class Immutable{
    
    
    private int value = 0;
    public Immutable(int value){
    
    
        this.value = value;
    }
    public int getValue(){
    
    
        return this.value;
    }

    public Immutable add(int v){
    
    
        return new Immutable(this.value + v);
    }
}

5. 实例分析

例1:

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:

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 int count = 0; // 共享资源

    public void update() {
    
    
        // ...
        count++;
    }
}

例3:

@Aspect
@Component
public class MyAspect {
    
    
    // 是否安全?--> spring中默认Bean是单例的,所以资源共享,不安全(解决方法:环绕通知)
    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:

public class MyServlet extends HttpServlet {
    
    
    // 是否安全-->不可变(UserServiceImpl 有私有成员),安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
    
    
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    
    
    // 是否安全-->无状态(UserDaoImpl 没有成员变量),安全
    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 = ?";
        // 是否安全--方法内局部变量,安全
        try (Connection conn = DriverManager.getConnection("","","")){
    
    
            // ...
        } catch (Exception e) {
    
    
            // ...
        }
    }
}

例5:

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 {
    
    
    // 是否安全-->不安全,t线程创建链接还未使用,t2线程可能就将其关闭了
    private Connection conn = null;
    public void update() throws SQLException {
    
    
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

例6:

public class MyServlet extends HttpServlet {
    
    
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
    
    
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    
    
    public void update() {
    
    
        // 每个线程 都会有一个新的Dao对象 
        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:

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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

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

请比较 JDK 中 String 类的实现

例8:

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

6. 一点点面试相关

6.1 为什么用final修饰String

  • 实现字符串池
  • 线程安全
  • 实现String可以创建HashCode不可变性

​ final可以修饰类,方法和变量,并且被修饰的类或方法,被final修饰的类不能被继承,即它不能拥有自己的子类,被final修饰的方法不能被重写, final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。

在了解final的用途后,在看String为什么要被final修饰:主要是为了”安全性“和”效率“的缘故。

​ final修饰的String,代表了String的不可继承性,final修饰的char[]代表了被存储的数据不可更改性。但是:虽然final代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变,。

​ final也可以将数组本身改变的,这个时候,起作用的还有private,正是因为两者保证了String的不可变性。

  • 不可变

    ​ 只有当String是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果String是可变的,那么String interning将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

  • 线程安全

    ​ 如果String是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为String是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

    因为String是不可变的,所以是多线程安全的,同一个String实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。String自己便是线程安全的。

  • HashCode 缓存

    ​ 因为String是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这使得String很适合作为Map中的键,String的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

猜你喜欢

转载自blog.csdn.net/u013494827/article/details/126066557