java并发编程 笔记五

变量的线程安全分析

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

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

局部变量是否线程安全?

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

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的。

直接看重头戏吧。

实例分析

例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) {
  		// 使用上述变量
	 }
}
Map不安全
String安全
final String 安全
 Date 不安全 因为可修改
 final Date d2 不安全 
 	1final 固定了Data的引用值
 	2、Data其他属性 比如:年月日可变

例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 int count = 0;

 	public void update() {
	 // ...
	 count++;
 }
}
private UserService userService = new UserServiceImpl(); 线程不安全
1、userService 是UserServiceImpl中的成员变量,所以会有多个线程共享使用userService 
2、UserServiceImpl中的count只有一份,所以是共享资源
3、UserServiceImpl中的update()属于临界区,多个线程可能对update()进行修改操作

例3:

@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));
 }
}
这段代码有线程安全问题
1、没有加@Scope(),所以他是单例
2、因为是单例,需要被共享,所以成员变量需要被共享
3、使用多例无法解决问题,因为使用前置通知可以是一个对象,然后后置通知有可能是另一个对象,无法统计时间
解决方法
1、使用环绕通知,可以把获取时间的变量做成局部变量

例4:

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) {
		 // ...
	 }
 }
}
1、UserDaoImpl没有成员变量,线程安全
2、 Connection conn也是安全的,因为是局部变量。有多个线程同时访问,线程一创建conn1,线程二创建conn2,互不干扰
3、UserServiceImpl 使用userDao ,线程安全   因为UserDao虽然被共享,但是没有可修改的属性(无状态,没有成员变量)
4、MyServlet 使用userService 线程安全  因为userService 虽然有userDao 成员变量,但是他是private,而且userDao不能被修改

例5:

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();
 	}
}
1、跟例4一样的不分析
2private Connection conn = null;作成了UserDaoImpl 的成员变量
3、然而UserDaol ,UserService,MyServlet 都只有一份,所以UserDaoImpl 会被多个线程共享,所以conn被多个线程共享
4、所以private Connection conn = null;要做成私有的局部变量,而不是共享的成员变量
  • 一份的意思是会在每个线程的栈帧内存中被创建一份。
  • 如果在每个线程的栈帧内存中被创建多份,因此不存在共享

例6:

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();
	 }
}
1、在UserServiceImpl每次都会创建新的userDao 作为方法内的局部变量,没有线程安全问题
2、线程一调用Service中的update,创建一个新的userDao,于是Connection也是新的;线程二调用后也是新的。
3、不推荐这种做法,在此方法没有问题,其他例子里可能有隐患。把Connection做成线程内的局部变量最佳

例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();
	 }
}
1、sdf 是局部变量,传递给抽象方法,子类可能做不恰当的事情
2、子类中的父方法在一个新线程被使用,造成并发访问同一个对象

请比较 JDK 中 String 类的实现

private和final一定程度能保护线程安全。开闭原则中的闭,不让子类来影响父类的行为
发布了93 篇原创文章 · 获赞 31 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_43866567/article/details/104544087