聊聊项目中的MVC分层构架中的一些线程安全问题

 

变量的线程安全分析

 

 

 

 

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

  如果它们没有共享,则线程安全
  如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
 

 

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

局部变量是否线程安全?

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

局部变量线程安全分析

局部变量如果引用的对象没有逃离整个方法的作用范围,哪它就是线程安全的,每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享,因此不存在线程安全问题

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

javac 后使用javap -v  查看字节码

  public static void test1();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: bipush        10
         2: istore_0
         3: iinc          0, 1
         6: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3       4     0     i   I

由此我们可以看出这里的i++,转化成字节点是incr,他是原子的,不像静态的变量中的i++操作

 

 

 

成员变量线程安全问题分析

成员变量在多线程环境下访问是存在共享 ,因此会引起线程安全问题

public class ThreadSafeTest {

    public static void main(String[] args){
        UserService userService = new UserService();
        for (int i = 0; i < 2; i++){
            new Thread(()->{
                userService.method(200);
            }, "Thread" + (i+1)).start();
        }
    }
}


class UserService{
    // 成员变量在多线程环境中线程不安全
    List<String> list = new ArrayList<>();

    private void addUser(){
        list.add("test");
    }

    private void deleteUser(){
        list.remove(0);
    }

    public void method(int times){
        for (int i = 0; i < times; i++){
            addUser();
            deleteUser();
        }
    }
}

运行结果异常:

内存分析:

修改为局部变量:

public class ThreadSafeTest {

    public static void main(String[] args){
        UserService userService = new UserService();
        for (int i = 0; i < 2; i++){
            new Thread(()->{
                userService.method(200);
            }, "Thread" + (i+1)).start();
        }
    }
}


class UserService{


    private void addUser(List<String> list){
        list.add("test");
    }

    private void deleteUser(List<String> list){
        list.remove(0);
    }

    public void method(int times){
        List<String> list = new ArrayList<>();
        for (int i = 0; i < times; i++){
            addUser(list);
            deleteUser(list);
        }
    }
}

运行结果正常

内存分析:list是局部变量,每个线程调用时会生成不同的线程栈,每个线程栈创建不同的实例,没有共享,因此不存在线程安全问题

 

 

局部变量-暴露引用:

情况一:如果将以上方法 addUser、deleteUser修改为public意味着可以在外部调用,这时候如果是线程1调用addUser,线程2调用deleteUser,他们是传的list参数是在不同的线程栈的,因此也是安全的

情况二:

public class ThreadSafeTest {

    public static void main(String[] args){
        UserService userService = new UserServiceSubClass();
        for (int i = 0; i < 2; i++){
            new Thread(()->{
                userService.method(200);
            }, "Thread" + (i+1)).start();
        }
    }
}


class UserService{


    private void addUser(List<String> list){
        list.add("test");
    }

    public void deleteUser(List<String> list){
        list.remove(0);
    }

    public void method(int times){
        List<String> list = new ArrayList<>();
        for (int i = 0; i < times; i++){
            addUser(list);
            deleteUser(list);
        }
    }
}

class UserServiceSubClass extends UserService{

    public void deleteUser(List<String> list){
        new Thread(()->{
            list.remove(0);
        }).start();
    }
}

因此,private在一定程序上保护方法的线程安全问题,限制子类重写父类方法无效,如果彻底不想让子类重写还可以直接加一个final修饰

再论关键字private、 final 安全意义所在

 

不想被子类重写的方法请记用final修饰

 

 

不想给外面访问的方法请记用private修饰

常见线程安全类

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

分析:

这时想要线程安全,就要在这两个操作之外使用synchronized包裹

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

常见不可变类:

String、Integer 等都是不可变类,因为其内部的成员变量不可以改变,因此它们的方法都是线程安全的,每次都是在重新操作后返回一个新的对象

 

 

实例分析

示例一

// Servlet只有一个实例,会被tomcat的多个线程访问
public class UserServlet extends HttpServlet {

    Map<String, Object> map = new HashMap<>();  // 不安全
    String s1 = "....";                         // 安全,String是不可变类
    final  String s2 = "....";                  // 安全,String是不可变类
    Date d1 = new Date();                       // 不安全
    final  Date d2 = new Date();                // 不安全, final只是表示d2不能变,但是Date实例中的属性是可以改变的,因此在多线程环境下不安全

    public void doGet(HttpServletRequest req, HttpServletResponse resp) {

    }
}

示例二:

// Servlet只有一个实例,会被tomcat的多个线程访问
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 不是线程安全的,servlet只有一份,因为userService是成员变量所以也只有一份

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {
    // 记录调用数次
    private int count = 0;
    @Override
    public void update() {
        // ....
        count ++;
    }
}

示例三:

 

在Spring中默认每个对象都是单例的,意味着是被多个线程共享的,因此也里面的成员变量也是补共享的,所在before、after方法对成员变量的修改会存在线程安全问题,在这里可以使用around环绕通知,使用把开始时间,结束时间定义成局部变量。如果将LogAspect定义成多例bean,也是不行的,有可能进入before时是一个实例,进入after时是另一个实例,计算出的时间有问题。

示例四:

// Servlet只有一个实例,会被tomcat的多个线程访问
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 虽然userService是成员变量,但是由于userService的成员变量userDao,没有地方可以修改它,所以也是线程安全的

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();        // 虽然userDao是成员变量,但是由于UserDao没有可更改的成员变量(无状态),所以也是线程安全的

    @Override
    public void update() {

    }
}

interface UserDao{

    void update();
}

// Dao没有成员变量,它们是线程安全的
class UserDaoImpl implements UserDao{

    @Override
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // Connection是线程安全的,因为属于局部变量,在多线程环境中,每个线程栈中会创建一份
        try(Connection conn = DriverManager.getConnection("localhost", "admin", "123456")){

        }catch (Exception e){

        }
    }
}

因此MVC模式中的这种贫血模型架构是线程安全的

 

示例五:

// Servlet只有一个实例,会被tomcat的多个线程访问
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 虽然userService是成员变量,但是由于userService的成员变量userDao,没有地方可以修改它,所以也是线程安全的

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();        // 虽然userDao是成员变量,但是由于UserDao没有可更改的成员变量(无状态),所以也是线程安全的

    @Override
    public void update() {

    }
}

interface UserDao{

    void update() throws SQLException;
}

// useDao会被多个线程共享,它的成员变量conn也会被多个线程共享,因此会发生线程安全
class UserDaoImpl implements UserDao{
    private Connection conn = null;
    @Override
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        // Connection是线程安全的,因为属于局部变量,在多线程环境中,每个线程栈中会创建一份
        conn = DriverManager.getConnection("localhost", "admin", "123456");
        conn.close();
    }
}

 

示例六:

// Servlet只有一个实例,会被tomcat的多个线程访问
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 虽然userService是成员变量,但是由于userService的成员变量userDao,没有地方可以修改它,所以也是线程安全的

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {


    @Override
    public void update() {
        try {
            // UserDao是局部变量,每个线程栈中存在一份userDao,哪它内部的conn成员变量也是各存在一份,所以不会有线程安全问题
            UserDao userDao = new UserDaoImpl();
            userDao.update();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

interface UserDao{

    void update() throws SQLException;
}


class UserDaoImpl implements UserDao{
    private Connection conn = null;
    @Override
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        // Connection是线程安全的,因为属于局部变量,在多线程环境中,每个线程栈中会创建一份
        conn = DriverManager.getConnection("localhost", "admin", "123456");
        conn.close();
    }
}

每一个请求,userService中的userDao会创建一份实例,它不存在线程安全问题,但是性能有一些隐患

 

示例七:

public class  ThroughVariableTest{
    public static void main(String[] args){
        new DateFormatUtil().getDate();
    }
}
abstract class BaseDateFormat {

    public void getDate(){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
        parse(simpleDateFormat);
    }

    // 这时子类的形为不确定,可能导致不安全的发生,被称之为外星方法
    abstract void parse(SimpleDateFormat simpleDateFormat);

}
class DateFormatUtil extends BaseDateFormat{

    @Override
    void parse(SimpleDateFormat simpleDateFormat) {
        String datastr = "1990-05-25 00:00:00";
        for (int i = 0; i < 20; i++){
            new Thread(()->{
                try {
                    simpleDateFormat.parse(datastr);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

这时发生了多个线程(包括main线程)访问同一个simpleDateFormat对象 simpleDateForma对象本身是线程不安全的,所以整个代码造成线程不安全问题,

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值