并发之线程安全

1 线程安全

1 成员变量和静态变量

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

2 局部变量

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

局部变量线程安全分析

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

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

成员变量

class ThreadUnsafe {
 ArrayList<String> list = new ArrayList<>();
 public void method1(int loopNumber) {
 for (int i = 0; i < loopNumber; i++) {
 // { 临界区, 会产生竞态条件
 method2();
 method3();
 // } 临界区
 }
 }
 private void method2() {
 list.add("1");
 }
 private void method3() {
 list.remove(0);
 }
}    
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
 ThreadUnsafe test = new ThreadUnsafe();
 for (int i = 0; i < THREAD_NUMBER; i++) {
 new Thread(() -> {
 test.method1(LOOP_NUMBER);
 }, "Thread" + i).start();
 }
}
/*
运行报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

*/

说明:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 运行相同

解决方法

将list改为局部变量

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 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

在这里插入图片描述

3 常见线程安全类

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

说明:

  • 它们的每个方法是原子的
  • 多个方法的组合不是原子的

线程安全类方法的组合

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

1

不可变类线程安全

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的. 但其部分方法,如replace可以改变值,怎么处理的?

下面案列,给该类添加一个方法,可以添加对象的内容信息.

public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
}

改造后

public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
 
 public Immutable add(int v){
 return new Immutable(this.value + v);
 } 
}

说明:

  • 通过重新创建了一个新对象,来改变了不可变类信息

4 实例说明

案列1
// HttpServlet是Web容器访问中是单例, 即多个线程访问同一个对象
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
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
@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
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
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
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();
 }
}
案列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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值