线程安全分析

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

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

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

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

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

案例1

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

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。

案例2

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

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1() {
        for (int i = 0; i < 20000; i++) {
        // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }
    public void method2() {
        list.add("1");
    }
    public void method3() {
        list.remove(0);
    }
}

出现 IndexOutOfBoundsException ,索引越界异常。

具体分析 

ArrayList 的 add 方法源码如下,remove 方法类似

  public boolean add(E e) {
    /**
     * 添加一个元素时,做了如下两步操作
     * 1.判断列表的capacity容量是否足够,是否需要扩容
     * 2.真正将元素放在列表的元素数组里面
     */
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
  }

add元素时,实际做了两个大的步骤:

  • 判断 elementData 数组容量是否满足需求
  • 在 elementData 对应位置上设置值

ensureCapacityInternal(size + 1) 多线程情况下会出现索引越界异常,这里主要分析elementData[size++],它不是一个原子操作,是分两步执行的。 

elementData[size] = e;
size++;

size++的字节码 :

getfield #284 <java/util/ArrayList.size>
dup_x1
iconst_1
iadd
putfield #284 <java/util/ArrayList.size>

思考这样一种情况:

  1. 列表为空 size = 0。
  2. 线程 A 执行完 elementData[size] = e 之后挂起。A 把 "a" 放在了下标为 0 的位置,此时 size = 0。
  3. 线程 B 执行 elementData[size] = e。因为此时 size = 0,所以 B 把 "b" 放在了下标为 0 的位置,于是刚好把 A 的数据给覆盖掉了。
  4. 线程 B 将 size 的值增加为 1, 线程 A将 size 的值增加为 1。

结果:size 为 1,elementData 下标为 0 的位置变成了 B,下标 1 的位置上什么都没有。此时再执行两次删除操作,就会出现异常。 

解决办法:将 list 修改为局部变量

如果为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,是否有线程安全问题?

class ThreadSafe {
    public void method1() {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 20000; 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{
    /**
     *多个线程同时操作list会存在线程安全问题,
     * 可以打印size查看
     */
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

多个线程同时操作list,依然存在线程安全问题,如上源码,ThreadSafeSubClass 类重写了 method3 方法,导致 method1 执行时依然是多个线程操作 list。

解决办法:private 或 final 修饰方法,让子类不能重写。

从这个例子可以看出 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();

 1)线程安全类方法的组合 

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

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

2)不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。

5、实例分析

例1

public class MyServlet extends HttpServlet {
    // 是否安全? 否
    Map<String, Object> map = new HashMap<>();
    // 是否安全? 是
    String S1 = "...";
    // 是否安全? 是
    final String S2 = "...";
    // 是否安全? 否
    Date D1 = new Date();
    // 是否安全? 是
    final Date D2 = new Date();

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

例2

否,userService的成员变量的修改操作是线程不安全的

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

否,Spring中的对象没有加额外说明的话都是单例模式,需要被共享,就会有线程安全问题。

解决办法:环绕通知,环绕通知可以把start,end做成环绕通知的局部变量。

注意:不能改成多例模式,如果进入前置通知和后置通知的对象不一样,也会产生问题。

@Aspect
@Component
public class MyAspect {
    // 是否安全?
    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 

相比于例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 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

否,conn对象被多个线程共享,例如:一个线程拿到conn对象,此时另一个线程将conn对象关闭,就会出现线程安全问题。

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

 例6

相比于例5,UserServiceImpl将userDao变为局部变量,这样虽然不存在线程安全问题,但不推荐这样写。

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

对比String类,由于被设计成final,子类不能继承,不会有线程安全问题。 

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

Integer属于不可变对象,一旦被创建,就不可能被修改。  

 i++的字节码

  8 getstatic #4 <p4_4/Test4.i>
 11 astore_2
 12 getstatic #4 <p4_4/Test4.i>
 15 invokevirtual #5 <java/lang/Integer.intValue>
 18 iconst_1
 19 iadd
 20 invokestatic #6 <java/lang/Integer.valueOf>
 23 dup
 24 putstatic #4 <p4_4/Test4.i>

 相当于执行

i = Integer.valueOf(i.intValue()+1)

进一步查看valueOf方法

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

i++的本质是创建一个新的对象,由于在多线程间,并不一定能看到同一个对象(因为i对象一直在变),因此,两个线程加锁可能加在了不同的对象实例上。

6、习题

1、卖票练习

测试下面代码是否存在线程安全问题,并尝试改正

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    // Random为 线程安全
    static Random random = new Random();
    // 随机 1~5
    public static int randomCount(){
        return random.nextInt(5) + 1;
    }
    public static void main(String[] args) {
        TicketWindow window = new TicketWindow(1000);
        List<Thread> threadList = new ArrayList();
        //用来存储卖出去多少钱,线程安全
        List<Integer> sellCount = new Vector();
        for (int i = 0; i < 500; i++){
            Thread thread = new Thread(() -> {
                int count = window.sell(randomCount());
                sellCount.add(count);
            });
            threadList.add(thread);
            thread.start();
        }
        //主线程等待执行完毕
        threadList.forEach((t) ->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        log.debug("剩余票:{}",window.getCount());
        log.debug("卖出去的票:{}",sellCount.stream().mapToInt(i -> i).sum());
  }
}
class TicketWindow{
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    public int sell(int amount){
        if(this.count >= amount){
            this.count -= amount;
            return amount;
        }
        return 0;
    }
}

只需要在sell方法加synchronized即可

注意这两段代码

    int count = window.sell(randomCount());
    sellCount.add(count);

前面说过每个方法是原子的,但是组合到一起会有线程安全问题。

    if( table.get("key") == null) {
        table.put("key", value);
    }

注意两者区别:table是对一个共享变量读和写,现在的例子中window是一个共享变量, sellCount是另一个共享变量,我们只要求每个共享变量的临界区被保护就可以了。

2、转账练习

测试下面代码是否存在线程安全问题,并尝试改正

public class ExerciseTransfer {
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
    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()));
    }
}
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);
        }
    }
}

这样改正行不行,为什么?

    public synchronized void transfer(Account target, int amount) {
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }

不行,只会保护this对象的money,不能保护target 的money。

可以这样改,但性能低下

    public void transfer(Account target, int amount) {
        synchronized (Account.class){
            if (this.money > amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小鲁蛋儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值