关于变量的线程安全问题

目录

成员变量与静态变量

2、局部变量的线程安全

3、常见的线程安全类

4、案例分析


成员变量与静态变量

有三个条件:

  1. 多线程环境
  2. 存在共享
  3. 同时存在读写操作

如果都满足,则肯定有线程安全问题。

2、局部变量的线程安全

局部变量是线程安全的,因为它是线程私有的,不满足共享条件。

原理是,每次方法调用对应着一个栈帧的创建,局部变量保存在栈帧的局部变量表中,而栈是线程私有的。

但,局部变量引用的对象则不一定:

  • 如果该对象没有跨越方法的作用范围,那么它是线程安全的
  • 如果该对象跨越了方法的作用范围,它就不是线程安全的。

为什么局部变量是线程安全的

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

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

关于局部变量的引用

局部变量的作用范围,只是它所在的方法内部,如果要在其他方法中使用这个变量,那么只能通过在本方法内传参来获得。

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对象,不同的线程不会操作同一个list对象,不存在共享
  • 一个线程中,三个方法的list引用都指向同一个对象

但是,如果出现这两种情况:

  • 把method2()修改成public的方法
  • 为上面这个类创建一个子类,并且在子类中重写method2()方法
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

这样,还是涉及到了多个线程操作一份共享变量,那肯定有线程安全问题。

这里体现出了访问权限修饰符的重要性,应该阻止局部变量的引用暴露给其他线程!合理使用private、final。

3、常见的线程安全类

  • String
  • Integer等包装类
  • StringBuffer
  • Random
  • java.util.concurrent包下的类(JUC)

替换关系

线程安全线程不安全
StringBufferStringBuilder
VectorArrayList
HashTableHashMap

需要线程安全时使用左边的,不需要时使用右边的,同一行的用法是一样的

线程安全的类,里面的方法都是同步方法。

如果多线程下使用集合,除了上面的Vector和HashTable,还可以使用Collections的下列方法,把普通的集合类变成线程安全的集合类

方法名说明
synchronizedList(List list)返回由指定列表支持的同步(线程安全)列表。
synchronizedMap(Map<K,V> m)返回由指定地图支持的同步(线程安全)映射。
synchronizedSet(Set s)返回由指定集合支持的同步(线程安全)集。

线程安全的解释

  • 线程安全的类,它们的每个方法都是原子的。多个线程调用同一个实例的某个方法,不会存在线程安全问题
  • 但是,它们多个方法的组合操作,并不是原子的!

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

两个线程执行这段代码:

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

Hashtable中,单独的get和put都是线程安全的,但组合起来使用,则不保证线程安全,因为get和put之间不是原子的,可能发生上下文切换。

注意不要误解:在执行get时,线程1持有锁,但执行完后就会释放锁。此时发生上下文切换,那么线程2还是可以持有锁,执行get方法的。

2、不可变类的线程安全性

String、Integer类都是不可变类,因此它们的方法都是线程安全的,修改内容后会返回一个新的对象,而不会修改原始对象的值。

相当于它是一个只读的常量。

4、案例分析

虽然有很多具体情形,但统一的宗旨是:

  • 有没有涉及到对共享变量的修改
  • 如果有,有没有专门考虑线程安全

具体问题具体分析。

问题一

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) {
        // 使用上述变量
    }
}

servlet对象会被tomcat的多个线程共享使用,所以存在多线程环境

  • HashMap不是线程安全的,多个线程访问一个HashMap,不安全
  • s1安全
  • s2安全
  • d1不安全,因为它是共享的,且可以被修改
  • d2安全,因为它是final的,不可被修改

问题二

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++;
    }
}
  • 多线程环境,只有一个userService对象,存在共享。
  • 它的update方法属于临界区,涉及到共享变量(count)的修改,而且不是同步的。所以不安全

问题三

@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));
    }
}
  • 这个对象是单例的,它的成员变量存在共享,存在并发修改问题,不安全。

如果要实现对执行时间的统计,怎么做?

  • 改成多例的话,避免了线程安全问题,但开始的对象和结束的对象不能保证是同一个,无法实现业务
  • 应该使用环绕通知,把start写成局部变量来解决

问题四

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) {
            // ...
        }
    }
}
  • dao没有成员变量,所以它是线程安全的
  • service也是安全的,因为虽然它共享了dao,但dao中不存在共享。
  • 上层的servlet也是安全的

问题五

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();
    }
}
  • dao中,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 { 
    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();
    }
}
  • service每次都会在方法内部创建一个新的dao,ao本身是一个局部变量,是线程私有的,那dao的成员变量就不存在共享了,所以安全

问题七

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是抽象方法,没有给出具体实现,把这种不可知的方法称为“外星方法”。

很有可能,foo中新开了线程,这就存在并发修改问题了。所以可能不是线程安全的。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值