【Java并发】变量的内存存储、线程安全分析

要理解原因,首先要清楚局部变量是什么?局部变量的存储方式是什么?

局部变量,从名字上就可以知道,它是只在特定作用域内可见并且只能在该作用域内使用的变量。也就意味着不同作用域的局部变量是不共享的。在多线程环境下,局部变量是存储在线程栈上的,每个线程都有自己独立的线程栈。线程之间不会共享这些变量,因此局部变量是线程安全的。但是也可能存在例外,比如引用类型(如指针、数组、对象等)的局部变量可能存在线程安全问题,因为多个线程可以同时访问同一个地址,导致数据竞争问题。

更官方的解释:
局部变量是什么:局部变量是指在程序中定义的只在特定作用域内可见并且只能在该作用域内使用的变量。这些变量通常被定义在函数、代码块或循环体等限定作用域的区域内,其生命周期仅包括了该区域的执行时间段。当程序退出该作用域时,局部变量就会被销毁释放,不再占用内存空间。局部变量通常用于暂时保存和处理临时数据,有助于提高代码的可读性和安全性。

java文件经过编译器编译生成class文件 class文件进入jvm,由各种类加载器加载 加载完毕后交给jvm执行引擎执行
jvm内存模型就是运行时数据区,程序运行时用到的数据以及相关信息保存区

为什么需要考虑线程安全问题?在多线程环境下,多个线程可以同时访问共享的变量或资源。考虑线程安全问题可以避免数据竞争、死锁等问题的出现,保证程序的正确性和稳定性。

JAVA的内存模型:
在这里插入图片描述
局部变量的存储方式
线程栈是指每个线程在运行时所使用的一块内存区域,用于保存线程的局部变量、函数参数、返回值以及程序计数器等信息。每当一个线程被创建时,系统会为该线程分配一段内存作为线程栈,每个线程有自己独立的线程栈空间。线程栈具有“先进后出”的特点,也就是说,最后进入栈中的数据最先被取出来。
在这里插入图片描述

局部变量存储在线程栈上局部变量是在函数内定义的变量,其存储空间分配在函数调用时所创建的栈帧中,也就是存储在线程栈上。每个线程都有自己的线程栈,它用于存储函数的参数、局部变量和返回地址等信息。当函数被调用时,该函数的参数和局部变量将被压入栈中,在函数返回时再从栈中弹出,这样就可以保证多个函数之间的数据不会相互干扰。

线程栈的大小通常是固定的,因为它必须在编译时确定。如果在运行时需要更多的栈空间,则必须使用递归或动态分配内存来实现。由于局部变量存储在线程栈上,所以它们的生命周期与函数的执行时间相同。当函数返回时,它们的值将被销毁,并且该空间将被释放,以便其他函数可以使用该空间。

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

  • 如果他们没有共享,则线程安全
  • 如果被共享:
    - 只有读操作,则线程安全
    - 有写操作,则这段代码是临界区,需要考虑线程安全

2.局部变量是否线程安全

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

3.局部变量的线程安全分析

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

每个线程调用该方法时局部变量i,会在每个线程的栈帧内存中被创建多分,因此不存在共享
在这里插入图片描述
当局部变量的引用有所不同

先来看一个成员变量的里例子:

public class ThreadUnsafeDemo {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();}}
}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);}}

可能会发生一种情况:线程1和线程2都去执行method2,但是由于并发执行导致最后只有一个元素添加成功,当执行了两次移除操作,所以就会报错。

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0at java.util.ArrayList.rangeCheck(ArrayList.java:659)at java.util.ArrayList.remove(ArrayList.java:498)at org.example.juc.ThreadUnsafe.method3(ThreadUnsafeDemo.java:39)at org.example.juc.ThreadUnsafe.method1(ThreadUnsafeDemo.java:30)at org.example.juc.ThreadUnsafeDemo.lambda$main$0(ThreadUnsafeDemo.java:17)at java.lang.Thread.run(Thread.java:750)进程已结束,退出代码0

分析:

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

在这里插入图片描述
但如果将list修改为局部变量,就不会有上述的问题了。

class Threadsafe {public void method1(int loopNumber) {for (int i = 0; i < loopNumber; i++) {ArrayList<String> list = new ArrayList<>();// 临界区,会产生竞态条件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 中引用通过一个对象
  • menthod3 的参数分析与 method2 相同

在这里插入图片描述
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其他线程调用 mthod2 和 method3
  • 情况2:在情况1的基础上,为 ThreadSafe 类添加子类,子类覆盖为 method2 或 method3 方法

我们先来看情况1,这两个方法的访问修饰符修改为public,其他线程就可以调用了,但是它们不能调用 method1,所以 method1里的局部变量list是安全的,其他线程要调用 method2 的话只能使用自己创建新的list变量。

我们再来看情况2,访问修饰符修改为 public ,也就意味着子类可以去覆盖重写 method2 和 method3 方法,即

class ThreadUnsafe {public void method1(int loopNumber) {for (int i = 0; i < loopNumber; i++) {ArrayList<String> list = new ArrayList<>();// 临界区,会产生竞态条件method2(list);method3(list);}}public void method2(ArrayList<String> list) {list.add("1");}public void method3(ArrayList<String> list) {list.remove(0);}}class ThreadSafeSubClass extends ThreadUnsafe {@Overridepublic void method3(ArrayList<String> list) {new Thread(() -> {list.remove(0);}).start();}
}

我们重写方法中,开启了一个新的线程,这个线程就能够去操作method1方法中的局部变量 list,此时 list就变成共享变量了,会有多个线程去修改它,也就产生了线程不安全的问题。也就是我们前面提到的局部变量的引用逃离了方法的作用范围(有其他线程去使用)就可能会产生安全问题。

4.常见线程安全类

  • 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
if( table.get("key") == null) {table.put("key", "t1");
}
// 线程2
if( table.get("key") == null) {table.put("key", "t2");
}

这里也就是检查和上锁不同步导致的线程不安全。

不可变线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因为他们的方法都是线程安全的。

有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

5.深入刨析String类为什么不可变?
什么是不可变?

String s = "aaa";
s = "bbb";

我们现在有一个字符串 s = “aaa”,如果我把它第二次赋值 s = “bbb”,这个操作并不会在原内存地址上修改数据,也就是不会吧 “aaa” 的那块地址里的数据修改为"bbb",而是重新指向了一个新的 内存地址,即”bbb"的内存地址,所以说 String 类是不可变的,一旦创建不可被修改的。

String 类里的replace方法

我们可以看到就是创建了一个新的String对象。

 public String replace(char oldChar, char newChar) {if (oldChar != newChar) {int len = value.length;int i = -1;char[] val = value; /* avoid getfield opcode */while (++i < len) {if (val[i] == oldChar) {break;}}if (i < len) {char buf[] = new char[len];for (int j = 0; j < i; j++) {buf[j] = val[j];}while (i < len) {char c = val[i];buf[i] = (c == oldChar) ? newChar : c;i++;}return new String(buf, true);}}return this;}

不可变的本质

我们看String类的源码就可以发现
在这里插入图片描述

1.String 类是一个 final 类
String类由final修饰,我们都知道当final修饰一个类时,该类不可以被其他类继承,自然String类就没有子类,也更没有方法被子类重写的说法了,所以这就保证了外界无法通过继承String类,来实现对String不可变性的破坏。

2.String底层是通过一个char[]来存储数据的,且该char[]由private final修饰。

该value数组被final修饰,我们知道被final修饰的引用类型的变量就不能再指向其他对象了,也就是说value数组只能指向堆中属于自己的那一个数组,不可以再指向其他数组了。但是我们可以改变它指向的这个数组里面的内容啊,比如咱们随便举个例子:

public class StringDemo {public static void main(String[] args) {final char[] c = {'a', 'b', 'c'};c[0] = 'd';System.out.println(Arrays.toString(c));}
}

其实不然,我们虽然可以修改一个对象的内容,但是我们根本无法修改String类里的数据,因为 String 类里的 value 数组是私有的,也没有对外修改的public方法,所以根本就没有可以修改的机会。

保证String类不可变靠的就是以下三点:

  • String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变性。

  • 保存字符串的value数组被 final 修饰且为私有的。

  • String 类里没有提供或暴露修改这个value数组的方法。

6.实例分析
我们来看几个例子,检验一下我们学的怎么样吧

线程安不安全,看这几个方便:

  • 是否是共享变量
  • 是否存在多个线程并发
  • 是否有写操作

例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:HashMap是线程不安全的类,所以不安全
  • S1 :可以修改其对象的引用地址,线程不安全
  • S2 :被final修饰,所以不能修改它的引用地址,也不可能修改它的值
  • D1 :Date()是线程不安全的类
  • D2:虽然被final修饰,但可以修改它里面的值

例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++;}
}

userService:成员变量,不安全,有多个线程会修改它的count变量

例三

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

MyAspect没有指定是单例对象还是多例对象,Spring默认是单例。所以多个线程都共享一个MyAspect

  • start:成员变量,线程不安全

例四

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) {// ...}}
}

UserDaoImpl中的update方法中的 conn 是局部变量,并且没有逃离方法的作用范围,所以 conn是线程安全的,UserServiceImpl 中的 UserDao是成员变量,但是userDao它调用的方法是线程安全的,所以userDao也是线程安全的,同理,userService也是线程安全的。

例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 { public void update() {// 是否安全private Connection conn = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}
}

conn是成员变量,多个线程用的是同一个conn,所以是线程不安全的,同时 userDao 也是线程不安全的,userService也是线程不安全的。

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

UserServiceImpl中不在用的是成员变量而是局部变量,所以 conn 虽然是局部变量但是不被多个线程之间共享,所以conn是线程安全的,所以userDao也是线程安全的,userService也是线程安全的。

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

例8

private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {List<Thread> list = new ArrayList<>();for (int j = 0; j < 2; j++) {Thread thread = new Thread(() -> {for (int k = 0; k < 5000; k++) {synchronized (i) {i++;}}}, "" + j);list.add(thread);}list.stream().forEach(t -> t.start());list.stream().forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});log.debug("{}", i);
}

这里虽然i是静态变量,但是又synchronized给修改i的代码块上了锁,所以是线程安全的。

参考链接:
https://www.zhihu.com/question/601406551/answer/3033562577 、https://blog.csdn.net/weixin_53029342/article/details/128903824
https://blog.csdn.net/weixin_29091105/article/details/114717003

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/409848.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML静态网页成品作业(HTML+CSS+JS)——迪士尼公主介绍(6个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;使用Javacsript代码&#xff0c;共有6个页面。 二、作品演示 三、代码…

ICML 2024 顶级论文:机器学习有什么新进展?

在本周的文章中&#xff0c;我打算探讨在国际机器学习大会 ICML 上发表的论文&#xff0c;该大会目前于 2024 年 7 月 21 日至 27 日在奥地利首都维也纳举行。与其他顶级人工智能会议一样&#xff0c;每年都会有数千篇论文提交&#xff0c;但录取率相对较低&#xff08;过去三年…

机械学习—零基础学习日志(如何理解概率论5)

二维随机变量 这里的其实就是边缘分布 联合分布 当结合来看&#xff0c;小明和小红的成绩。可以发现&#xff0c;小明和小红是独立事件&#xff0c;可以放到一个模块内部分析。 而当所有的情况考虑&#xff0c;单独小红取得某个成绩的概率&#xff0c;都可以计算出来。 例如…

攻防世界 1000次点击

做题笔记。 下载解压 查壳。 32位ida打开。 查找字符串。 winmain函数写的&#xff0c;程序运行如下&#xff1a; 一开始思路是想着分析找到关键代码然后去od进行调试。 后来&#xff0c;额&#xff0c;不想看代码了。吐了。 尝试去字符串搜索flag样式&#xff0c;确实一发现…

【C/C++】Sleep()函数详解

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:Linux ⚙️操作环境:Visual Studio 2022 / Xshell (操作系统:CentOS 7.9 64位) 目录 &#x1f4cc;Windows系统下Sleep()函数简介 &#x1f38f;函数功能 &#x1f38f;函数参数 &#x1f579;️DWORD milliseconds &…

Linux云计算 |【第二阶段】SHELL-DAY2

主要内容&#xff1a; 条件测试&#xff08;字符串比较、整数比较、文件状态&#xff09;、IF选择结构&#xff08;单分支、双分支、多分支&#xff09;、For循环结构、While循环结构 一、表达式比较评估 test 命令是 Unix 和 Linux 系统中用于评估条件表达式的命令。它通常用…

致远OA OCR票据识别组件

OCR票据识别 技术支持 技术大佬支持本文档 使用范围 任何票种信息&#xff0c;只要需要对接到oa底表中&#xff0c;就能够实现各种票种&#xff0c;各种字段的对接&#xff0c;包括票据识别&#xff0c;发票核验&#xff0c;适配各种票据 使用介绍 1 配置每种发票的ocr设…

yup 使用 2 - 获取默认值,循环依赖,超大数字验证,本地化

yup 使用 2 - 获取默认值&#xff0c;循环依赖&#xff0c;超大数字验证&#xff0c;本地化 上一篇的使用在这里&#xff1a;yup 基础使用以及 jest 测试&#xff0c;这篇讲的是比较基础的东西&#xff0c; 获取默认值 之前用的都是 cast({})&#xff0c;然后如果有些值是必…

叉车(工业车辆)安全管理系统,云端监管人车信息运营情况方案

近年来&#xff0c;国家和各地政府相继出台了多项政策法规&#xff0c;从政策层面推行叉车智慧监管&#xff0c;加大叉车安全监管力度。同时鼓励各地结合实际&#xff0c;积极探索智慧叉车建设&#xff0c;实现作业人员资格认证、车辆状态认证、安全操作提醒、行驶轨迹监控等&a…

如何利用电商 API 数据分析助力精准选品!

电商 API 数据分析在选品过程中起着至关重要的作用&#xff0c;它们之间有着密切的关系&#xff1a; 一、提供市场趋势洞察 热门商品识别&#xff1a; 通过分析电商 API 中的销售数据&#xff0c;包括商品的销售量、销售额、销售频率等指标&#xff0c;可以快速准确地识别出当…

1Panel应用推荐:MeterSphere开源持续测试工具

1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。为了方便广大用户快捷安装部署相关软件应用&#xff0c;1Panel特别开通应用商店&am…

redis面试(二十一)读写锁互斥

读锁非互斥 非互斥的意思就是&#xff0c;一个客户端或者线程加锁之后&#xff0c;另一个客户端线程也可以来进行加锁。 还是拿着ReadLock的lua脚本来看看 刚才我们已经分析过第一个线程来加读锁的逻辑了 所以上半截不用重复说了&#xff0c; hset anyLock mode read hset an…

后端微服务架构:构建分布式博客系统

后端微服务架构&#xff1a;构建分布式博客系统 在当今的软件开发领域&#xff0c;微服务架构已经成为构建可扩展、灵活且易于维护的应用程序的主流选择。本文将探讨如何利用微服务架构来设计和实现一个分布式的博客系统。 1. 微服务架构简介 微服务架构是一种将应用程序分解…

【微服务部署】Linux部署微服务启动报ORA-01005

问题背景&#xff1a; Linux机器部署springboot微服务&#xff0c;部署完成后发现无法启动&#xff0c;后台报ORA-01005错误。 解决方案&#xff1a; 1.检查当前服务器是否已安装oracle客户端 命令行执行sqlplus username/passwd实例名&#xff0c;如果执行成功&#xff0c;说…

微信小程序源码 图书管理系统 万字文档 Springboot vue

源码地址 系统演示 SpringBoot vue 微信小程序源码 图书管理系统 附带运行教程 系统演示 万字文档&#xff0c;全套开发工具 开发工具:IDEA,微信小程序工具 数据库:mysql8 使用环境:Windows JDK版本:1.8 后端构建工具:maven 项目使用到的技术栈 Springboot2 mybatis vue Mys…

计算机毕业设计选题推荐-摇滚音乐鉴赏网站-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

领域驱动设计DDD详解与战术建模落地

一、什么是DDD&#xff1f; 1.1、DDD的概念 Domain-Driven Design&#xff08;领域驱动设计&#xff09;它由Eric Evans在他的2003年出版的书籍《Domain-Driven Design: Tackling Complexity in the Heart of Software》中首次提出。DDD 核心思想是通过领域驱动设计方法定义领…

透明度测试

&#xff11;、透明测试是用于处理哪种透明需求 在游戏开发中对象的某些部位完全透明而其他部位完全不透明,这种透明需求往往不需要半透明效果,相对比较极端&#xff0c;只有看得见和看不见之分比如树叶、草、栅栏等等。&#xff08;即一张图除了主要物体有颜色&#xff0c;其…

奇文网盘项目对应windows版本的中间件下载,otp,rabbitmq,postgresql,onlyoffice(在线预览编辑等)

之前的解压是百度网盘&#xff0c;要会员&#xff0c;油猴也无法下载&#xff0c;所有我下载之后给你们提供阿里云盘链接&#xff08;不限速&#xff09; 本次软件版本介绍&#xff1a; 01-otp_win64_24.1 02-rabbitmq-server-3.9.8 03-postgresql-9.6.23-2-windows-x64 0…

如何解决错误Given calling package android does not match caller‘s uid-学员提问

背景&#xff1a; 近来有学员反馈说wms课程中讲解的借壳Shell帮忙执行一些shell命令有问题&#xff0c;具体啥问题呢&#xff1f; 在ShellProvider的call方法加入如下代码&#xff1a; 目的就是想让shell帮我们执行一下settings值的写入&#xff0c;这里其实可以更加简单的set…