目录
成员变量与静态变量
有三个条件:
- 多线程环境
- 存在共享
- 同时存在读写操作
如果都满足,则肯定有线程安全问题。
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)
替换关系
线程安全 | 线程不安全 |
---|---|
StringBuffer | StringBuilder |
Vector | ArrayList |
HashTable | HashMap |
需要线程安全时使用左边的,不需要时使用右边的,同一行的用法是一样的
线程安全的类,里面的方法都是同步方法。
如果多线程下使用集合,除了上面的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中新开了线程,这就存在并发修改问题了。所以可能不是线程安全的。