返回

那些年踩过的线程不安全陷阱:层出不穷的真实案例

Android

对象引用的逸出

假设你有一个发布任务的系统,其中包含一个 Task 类来封装任务信息。出于性能考虑,你在 Task 类中保存了一个对任务执行者的引用。当任务被发布时,系统会创建一个新的线程来执行任务。在执行过程中,任务执行者可能会引用 Task 类中的变量。

public class Task {
    private Executor executor;

    public Task(Executor executor) {
        this.executor = executor;
    }

    public void execute() {
        executor.execute();
    }
}

现在,问题来了。如果在执行任务时,任务执行者被其他线程修改了,可能会导致线程不安全问题。例如,如果任务执行者被另一个线程设置为 null,那么在执行任务时就会抛出 NullPointerException 异常。

隐藏的逸出

隐藏的逸出是指,虽然变量本身没有被共享,但由于其他变量的共享,导致该变量也被共享了。

举个例子,假设你在 Task 类中有一个私有变量 count,用来统计任务执行的次数。你认为,由于 count 是私有的,因此它是线程安全的。但实际上,由于 Task 类是可变的,因此其他线程可以通过修改 Task 类来修改 count 的值。

public class Task {
    private int count;

    public void execute() {
        count++;
    }
}

多个线程操作同一非线程安全对象

假设你有一个 Counter 类,用来统计一个数字。Counter 类包含一个私有变量 count,用来存储数字的值。Counter 类还提供了一个 increment() 方法,用于增加 count 的值。

public class Counter {
    private int count;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

现在,假设你创建了两个线程,同时调用 Counter 类的 increment() 方法。由于 Counter 类是非线程安全的,因此两个线程可能会同时修改 count 的值,导致最终的 count 值不正确。

使用不同的锁锁定同一对象

假设你有一个 Account 类,用来管理一个银行账户。Account 类包含一个私有变量 balance,用来存储账户的余额。Account 类还提供了一个 withdraw() 方法,用于从账户中提取钱。

public class Account {
    private int balance;

    public void withdraw(int amount) {
        synchronized (this) {
            if (balance >= amount) {
                balance -= amount;
            }
        }
    }
}

现在,假设你创建了两个线程,同时调用 Account 类的 withdraw() 方法。由于两个线程使用了不同的锁来锁定 Account 类,因此可能会出现线程不安全问题。例如,如果两个线程同时调用 withdraw() 方法,并且提取的金额之和大于账户的余额,那么其中一个线程可能会提取到比账户余额更多的钱。

同时操作多个关联的线程安全对象

假设你有一个 Order 类,用来管理一个订单。Order 类包含一个私有变量 items,用来存储订单中的商品。Order 类还提供了一个 addItem() 方法,用于向订单中添加商品。

public class Order {
    private List<Item> items;

    public void addItem(Item item) {
        synchronized (this) {
            items.add(item);
        }
    }
}

现在,假设你创建了两个线程,同时调用 Order 类的 addItem() 方法。由于两个线程使用了同一个锁来锁定 Order 类,因此不会出现线程不安全问题。但是,如果两个线程同时调用 Order 类的 getItems() 方法,那么可能会出现线程不安全问题。因为 getItems() 方法并没有对 Order 类加锁,因此两个线程可能会同时修改 items 列表,导致最终的 items 列表不正确。

总结

线程不安全是一个非常复杂的问题,而且在实际开发中经常遇到。因此,在开发多线程程序时,一定要注意线程安全问题。要做到这一点,你可以使用以下几种方法:

  • 使用同步机制来保护共享数据。
  • 使用不变对象来避免共享数据。
  • 使用线程安全的类库。

只有这样,才能避免线程不安全问题,保证程序的正确性和可靠性。