变量的线程安全分析
成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
局部变量是线程安全的
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用访问,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
局部变量如果引用的对象没有逃离整个方法的作用范围,哪它就是线程安全的,每个线程调用 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对象本身是线程不安全的,所以整个代码造成线程不安全问题,