变量线程安全分析

145 阅读6分钟

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

  • 如果没有被线程共享,则是线程安全的。
  • 如果被共享:
    • 只有读操作,则线程安全。
    • 一旦有写操作,则这段代码是临界区,需要考虑线程安全。

局部变量是否线程安全

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

局部变量的线程安全分析

public static void test() {
   int i = 10;
   i++;
}

每个线程调用该方法时局部变量i,会在每个线程的栈帧内存中被创建多分,因此不存在共享。如图: 未命名文件.jpg

当局部变量的引用有所不同,先来看一个成员变量的例子:

public class ThreadUnsafeDemo {

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

//可能会发生一种情况:线程1和线程2都去执行method2,但是由于并发执行导致最后只有一个元素添加成功,当执行了两次移除操作,所以就会报错。
//执行结果:
Exception in thread "Thread1" 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.juc.ThreadUnsafe.method3(ThreadUnsafeDemo.java:39)
	at org.example.juc.ThreadUnsafe.method1(ThreadUnsafeDemo.java:30)
	at org.example.juc.ThreadUnsafeDemo.lambda$main$0(ThreadUnsafeDemo.java:17)
	at java.lang.Thread.run(Thread.java:750)

进程已结束,退出代码0

分析

  • 无论哪个线程中的 method2 引用的都是同一个对象中的list成员变量。
  • method2 和 method3 分析相同。 1.jpg

更改为局部变量

public class ThreadUnsafeDemo {

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

class Threadsafe {
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            ArrayList<String> list = new ArrayList<>();
            // 临界区,会产生竞态条件
            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 中引用通过一个对象。
  • menthod3 的参数分析与 method2 相同。 2.jpg

分析

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

答案:

1.如果访问权限修饰符为 private ,则 method2 和 method3 的形参肯定是 method1 的方法传递过去的,但是 method1 每个线程调用的时候都会创建新的list集合,因此不会存在线程安全问题。

2.如果访问权限修饰符为 public ,线程一去调用 method1 时创建一个list对象,然后去传递 method2 和 method3, 此时线程二去单独调用 method2 或 method3,线程二传递的 list 是另外一个list对象了,因此也不会存在线程安全问题。

但是为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法。此时会出现线程安全问题,因为比如一个线程去访问 method1 创建了 list ,然后去访问 method2 或 method3 方法,由于子类重写了 method2 或 method3 方法,但是子类的 method2 或 method3 方法内部又创建了一个线程去操作传递过来的 list 集合,因此满足了2个线程操作同一个共享变量,因此存在线程安全问题。

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(避免子类重写) 提供【安全】的意义所在,请体会开闭原则中的【闭】。

常见的线程安全类

Integer、Short、Float、Double、Long、Boolean、Byte、Character、StringBuffer、Random等以及java.util.concurrent包下的类。这里的线程安全指的是多个线程调用他们同一个实例的某一个方法时,是线程安全的。可以理解为:

    Hashtable table = new Hashtable();
    
    new Thread(()->{
        table.put("key", "value1");
    }).start();
    
    new Thread(()->{
        table.put("key", "value2");
    }).start();

他们的每一个方法都是原子性的,但是多个方法组合在一起就不再是线程安全的。比如:

    Hashtable table = new Hashtable();
    // 线程1,线程2
    if( table.get("key") == null) {
        table.put("key", value);
    }
    
//这段代码原本的意思只是,如果这个key为null,则put一个值,但是线程一和线程二同时执行该方法,此时线程一得到这个key为null,正打算put的时候,线程二进来也判断这个key为null,因此此时线程二进来会将线程一put的值进行覆盖,因此得不到预期的结果,存在线程安全问题。

image.png

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。 但String 有 replace,substring 等方法是可以改变值的,那么这些方法又是如何保证线程安全的呢?其实从String的源码可以看到其实它并没有再原始对象上进行修改,而是重新创建了新的String进行赋值。

三层架构判断线程是否安全

线程不安全:由于 Servlet 只有一份,因此成员变量 userService 也只有一份并可以被共享,由于在 userService 中有成员变量 count ,此时多个线程就可以对同一个 count 进行操作,因此不是线程安全的。

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

线程安全

public class MyServlet extends HttpServlet {

    // UserServiceImpl中虽然有一个成员变量userDao,但是它是私有的,也没有其它地方可以去修改它,因此也属于线程安全的。
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}


public class UserServiceImpl implements UserService {

    // 因为UserDaoImpl是线程安全的,虽然UserDaoImpl是成员变量也可以被共享,但是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 = ?";
        // Connection作为局部变量,即使有多个线程进行访问,也不能修改它的属性以及状态,因此线程是安全的。
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}

线程不安全

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 {
    // 是否安全
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

线程安全

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

外星方法

public abstract class Test {

    public static void main(String[] args) {
        new Test().bar();
    }

    public void bar() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }
    public abstract foo(SimpleDateFormat sdf);

}

由于 foo 是一个抽象类,因此 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();
    }
}

卖票案例

public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(2000);
        List<Thread> list = new ArrayList<>();
        // 用来存储买出去多少张票
        List<Integer> sellCount = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件
                int count = ticketWindow.sell(randomAmount());
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }
        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 买出去的票求和
        log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

class TicketWindow {
    private int count;
    public TicketWindow(int count) {
        this.count = count;
    }
    public int getCount() {
        return count;
    }
    //临界区,添加 synchronized
    public int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

转账案例

public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        // 查看转账2000次后的总金额
        log.debug("total:{}",(a.getMoney() + b.getMoney()));
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
}
class Account {
    private int money;
    public Account(int money) {
        this.money = money;
    }
    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    public void transfer(Account target, int amount) {
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}
//使用Account.class不能使用this是为什么?
public synchronized void transfer(Account target, int amount) {
    synchronized(Account.class){
        if (this.money > amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
        }
    }  
}