短信登录和登录校验(线程安全、ThreadLocal、进程调度)
在做黑马点评项目的登录验证时,老师说用户信息可能会有线程安全问题,需要使用ThreadLocal,我当时就不太理解了,用户信息存储在session中怎么会有线程安全问题呢?
1.什么是线程:
每次客户端向服务器发送request请求,一个HTTP请求到达服务器,Tomcat会从线程池中分配一个线程来执行这个请求,这个线程会经过拦截器、控制器、服务层、数据访问层等,直到返回响应。
2.session获取用户信息的缺点:
首先,使用session并不会出现线程安全问题,因为session是用户级别,不同用户之间隔离,但是使用session有缺点:
Class Service1{void method11(user){}void method12(user){}Class Service2{void method21(user){}void method22(user){}	
}
如上所示,当有很多方法都需要user时,那么这些方法的参数必然都需要定义一个user或session对象,这就很冗余了,不仅类内的方法冗余,不同类之间也冗余。
3.如何解决:
这时候就有同学问了,把user单独设置成一个全局变量就不好了:
方法1:
User user;Class Service1{void method11(){user = new User(name, age);}void method12(){}Class Service2{void method21(){}void method22(){}	
}
这样确实对于类内的方法和不同类之间完美的解决了冗余问题,但会出现线程安全问题,而且线程安全问题是最严重的。
方法2:
Class Service1{User user;void method11(){user = new User(name, age);}void method12(){不用new,解耦了}Class Service2{User user;void method21(){user = new User(name, age);}void method22(){不用new,解耦了	}	
}
这种方法能减少类内的冗余,但是不同类之间还是需要重复定义User user,类间冗余依然没有改善,并且也有线程安全问题。
3.什么是线程安全问题:
举个例子,有一个方法用来创建订单:
Class OrderService{User user;void createOrder(){user = new User(name, age);orderMapper.insert(user);}
首先需要知道,Controller、Service和Component都是单例模式,使用IOC自动注入,也就是说在容器中只有一个实例,所有用户的线程(request)共用这一个对象。
如果同时有两个用户执行createOrder方法,user1刚执行new User()给user赋值,user2就抢占了CPU执行new User()。由于OrderService对象是单例,那么此时user中存的是user2的信息。user1获得CPU后执行orderMapper.insert(user)使用的是user2的信息,就会出错,这就是线程安全问题。
进程同步解决线程安全问题:
这时候学过操作系统的的宝宝看到 “抢占,调度” 是不是感觉似曾相识,就能想到了:如果把new User()和createOrder()定义原子操作,不能被其他线程抢占不就行了?
是的,这就是操作系统的进程(线程)同步机制。啥?进程同步里面的内容都忘了怎么办。那么我给几个提示:PV操作、互斥锁、条件变量。想起来没有?(虽然当时感觉没用吧,现在一结合实际问题,就觉得没白学)
PV操作即P(wait)和V(signal)操作,用于管理临界区的互斥访问和进程间的同步,例如哲学家就餐问题就是PV。在数据库事务的ACID特性中,PV操作主要与隔离性类似(但是不要混淆进程同步和事务,他们一个针对的是Service层的线程安全问题,一个针对的是Mapper层的数据脏读、不可重复读问题):
我们通常提到数据库事务时,都会想到ACID属性,ACID是指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。下面分别解释:
原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。例如,银行转账操作中,从A账户扣款和向B账户加款必须同时成功或同时失败。如果中途发生错误,则会回滚到事务开始前的状态。
一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。也就是说,事务执行前后,数据库都必须处于一致性状态。例如,转账前后两个账户的总金额应该保持不变。
隔离性(Isolation):多个用户并发访问数据库时,数据库为每个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。隔离级别有读未提交、读已提交、可重复读、串行化等。
持久性(Durability):一旦事务提交,则其对数据库的更改就是永久性的。即使系统发生故障,也不会丢失提交的事务。
如下所示,通过PV操作确保进程(线程)对临界区的串行访问,解决了并发环境下的资源竞争和协作问题:
Class OrderService{User user;void createOrder(){P()user = new User(name, age);orderMapper.insert(user);V()}
但是其实PV操作复杂了,所以实际开发一般用不到,反而是锁机制用的比较多。
锁机制解决线程安全问题:
这个后面的业务中会学到乐观锁、悲观锁…
ThreadLocal解决线程安全问题:
ThreadLocal就相当于这种方法:
User user;Class Service1{void method11(){user = new User(name, age);}void method12(){}Class Service2{void method21(){}void method22(){}	
}
线程(request请求)不再共享同一个user,而是每个线程分别分配一个user对象,既确保了不同线程之间的user隔离,又解决了线程安全问题。
这里留一个问号,既然这样的话后面为什么还要用到乐观锁呢?直接用Tread不就行了
我的推测是,类似于超卖问题:
Class OrderService{void Sail(user){检查库存;创建订单;扣减库存;}
这里user使用形参或者ThreadLocal实际上并没有线程安全问题,但是Sail()方法本身执行的这三个操作可能会有线程安全问题,这里我还没想到怎么会出问题,等后面项目再理解吧。
