关于线程安全问题

579 阅读5分钟

java内存区域

按是否共享

  • 线程共享数据区:堆、方法区
  • 线程隔离数据区:程序计数器、本地方法栈、虚拟机栈

程序计数器:可以理解为当前线程所执行字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)

虚拟机栈本地方法栈:每个java方法在执行时都会创建一个栈帧用于存储局部变量、操作数栈、动态链接等信息。只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

:存放对象实例

方法区1:它用于存储已被虚拟机加载的类型信息、常量2、静态变量3、即时编译器编译后的代码缓存等数据(《Java虚拟机规范》说是堆的一个逻辑分区,暂且认为他是放在和堆一个地方)

运行时常量池:属于方法区的一部分,存放编译后生成的各种字面量和符号引用(这也是class文件常量池的内容)

并发问题

① 当一个单例实例对象有成员变量时,多线程访问会出现数据问题。
② 当访问一个单例实例对象方法有局部变量时,线程是安全的。

第一点解释:

    class c {
        public int num;
        public void method(int i){
            if(i == 1){
                num = 10;
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }                
                System.out.println("a is " + num);
            }else {
                num = 20;
                System.out.println("b is " + num);
            }
        }
    }

假设线程a和线程b同时访问实例c的method(i)方法,线程a的i=1,线程b的i=2,当线程a执行到7行,线程b执行到第13行,num=20,a的num原本是10的,结果被更改了。(掘金还不支持行号显示,从class c { 算第一行开始)

第二点解释:

    class c {
        
        public void method(int i){
            int num = 0;
            if(i == 1){
                num = 10;
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }                
                System.out.println("a is " + num);
            }else {
                num = 20;
                System.out.println("b is " + num);
            }
        }
    }

这里需要有java内存区域的知识点,上一节讲过。

  • 对象实例保存在中,局部变量保存在虚拟机栈中,堆是共享内存区域,而虚拟机栈是隔离区域,因此各个线程之间不会影响。
  • 方法内部的变量为方法私有的变量,其生存周期随着方法的结束而终结。

spring mvc中bean的作用域及并发问题

作用域

spring 3 中bean的作用域有5种

作用域(scope)解释
singleton单例模式,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是Spring中的缺省作用域
prototype原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态,而 singleton 全局只有一个对象。根据经验,对有状态的bean4使用prototype作用域,而对无状态的bean5使用singleton作用域
request在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean实例也将会被销毁。
session在一次 Http Session 中,容器会返回该 Bean 的同一实例。而对不同的 Session 请求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求内有效,请求结束,则实例将被销毁。
global Session在一个全局的 Http Session 中,容器会返回该 Bean 的同一个实例,仅在使用 portlet context 时有效。

Spring并发访问的线程安全性问题

  1. 由于Spring MVC默认是Singleton的,所以会产生一个潜在的安全隐患。根本核心是instance变量保持状态的问题。这意味着每个request过来,系统都会用原有的instance去处理,这样导致了两个结果:

    • 我们不用每次创建Controller
    • 减少了对象创建和垃圾收集的时间
  2. 由于只有一个Controller的instance,当多个线程同时调用它的时候,它里面的instance变量就不是线程安全的了,会发生窜数据的问题。

解决办法:

  1. 在控制器中不使用实例变量
  2. 将控制器的作用域从单例(singleton)改为原型(prototype)
  3. 在Controller中使用ThreadLocal变量6

死锁

由于多线程的访问,会出现一些意想不到的问题,常见的数据问题。通常会进行加锁。加锁就会出现另一种问题-死锁

定义:多个并发进程因争夺系统资源而产生相互等待的现象。

必要条件

互斥、占有且等待、不可抢占、循环等待

参考资料

  1. Java多线程5:方法内部变量为线程安全
  2. springMVC一个Controller处理所有用户请求的并发问题
  3. 死锁的四个必要条件和解决办法

Footnotes

  1. jdk1.7及之前hotspot虚拟机对方法区的实现为永久代,jdk1.7把字符串常量池、静态变量等从方法区移除;jdk1.8移除了永久代,用本地内存实现的元空间代替,也就是对方法区的实现为元空间

  2. jdk1.7之前方法区常量包括字符串常量池、运行时常量池、;jdk1.7把字符串常量池移到堆中。

  3. jdk1.7之后静态变量从方法区移除

  4. 有状态bean(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。一般是prototype scope。

  5. 无状态对象(Stateless Bean),就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。一般是singleton scope。

  6. 线程本地变量