0


【Java并发编程】变量的线程安全分析

文章目录

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

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

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

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

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

publicstaticvoidtest1(){int i =10;
 i++;}

每个线程调用该方法时局部变量

i

,会在每个线程的栈帧内存中被创建多分,因此不存在共享

image-20230206104606041

当局部变量的引用有所不同

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

publicclassThreadUnsafeDemo{staticfinalintTHREAD_NUMBER=2;staticfinalintLOOP_NUMBER=200;publicstaticvoidmain(String[] args){ThreadUnsafe test =newThreadUnsafe();for(int i =0; i <THREAD_NUMBER; i++){newThread(()->{
                test.method1(LOOP_NUMBER);},"Thread"+ i).start();}}}classThreadUnsafe{ArrayList<String> list =newArrayList<>();publicvoidmethod1(int loopNumber){for(int i =0; i < loopNumber; i++){// 临界区,会产生竞态条件method2();method3();}}privatevoidmethod2(){
        list.add("1");}privatevoidmethod3(){
        list.remove(0);}}

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

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at 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 分析相同

image-20230206105633799

但如果将list修改为局部变量,就不会有上诉的问题了。

classThreadsafe{publicvoidmethod1(int loopNumber){for(int i =0; i < loopNumber; i++){ArrayList<String> list =newArrayList<>();// 临界区,会产生竞态条件method2(list);method3(list);}}privatevoidmethod2(ArrayList<String> list){
        list.add("1");}privatevoidmethod3(ArrayList<String> list){
        list.remove(0);}}

分析

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用通过一个对象
  • menthod3 的参数分析与 method2 相同

image-20230206132201148

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

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

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

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

classThreadUnsafe{publicvoidmethod1(int loopNumber){for(int i =0; i < loopNumber; i++){ArrayList<String> list =newArrayList<>();// 临界区,会产生竞态条件method2(list);method3(list);}}publicvoidmethod2(ArrayList<String> list){
        list.add("1");}publicvoidmethod3(ArrayList<String> list){
        list.remove(0);}}classThreadSafeSubClassextendsThreadUnsafe{@Overridepublicvoidmethod3(ArrayList<String> list){newThread(()->{
            list.remove(0);}).start();}}

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

4.常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说的线程安全是指,多个线程调用他们同一个实例的方法时,时线程安全的,也可以理解为:

Hashtable table =newHashtable();newThread(()->{
 table.put("key","value1");}).start();newThread(()->{
 table.put("key","value2");}).start();

他们的每个方法是原子的,但它们多个方法的组合不是原子的,比如:

Hashtable table =newHashtable();// 线程1if( table.get("key")==null){
 table.put("key","t1");}// 线程2if( 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对象。

publicStringreplace(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[]=newchar[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++;}returnnewString(buf,true);}}returnthis;}

不可变的本质

我们看String类的源码就可以发现,

image-20230206140632920

  1. String 类是一个 final 类

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

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

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

publicclassStringDemo{publicstaticvoidmain(String[] args){finalchar[] 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. 是否是共享变量
  2. 是否存在多个线程并发
  3. 是否有写操作

前置知识:tomcat中一个servet类只会有一个实例,所以多个请求用的都是同一个servet对象

例1

publicclassMyServletextendsHttpServlet{// 是否安全?Map<String,Object> map =newHashMap<>();// 是否安全?StringS1="...";// 是否安全?finalStringS2="...";// 是否安全?DateD1=newDate();// 是否安全?finalDateD2=newDate();publicvoiddoGet(HttpServletRequest request,HttpServletResponse response){// 使用上述变量}}

他们都是成员变量

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

例2

publicclassMyServletextendsHttpServlet{// 是否安全?privateUserService userService =newUserServiceImpl();publicvoiddoGet(HttpServletRequest request,HttpServletResponse response){
 userService.update(...);}}publicclassUserServiceImplimplementsUserService{// 记录调用次数privateint count =0;publicvoidupdate(){// ...
 count++;}}
  • userService:成员变量,不安全,有多个线程会修改它的count变量

例三

@Aspect@ComponentpublicclassMyAspect{// 是否安全?privatelong start =0L;@Before("execution(* *(..))")publicvoidbefore(){
 start =System.nanoTime();}@After("execution(* *(..))")publicvoidafter(){long end =System.nanoTime();System.out.println("cost time:"+(end-start));}}

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

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

例四

publicclassMyServletextendsHttpServlet{// 是否安全privateUserService userService =newUserServiceImpl();publicvoiddoGet(HttpServletRequest request,HttpServletResponse response){
 userService.update(...);}}publicclassUserServiceImplimplementsUserService{// 是否安全privateUserDao userDao =newUserDaoImpl();publicvoidupdate(){
 userDao.update();}}publicclassUserDaoImplimplementsUserDao{publicvoidupdate(){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

publicclassMyServletextendsHttpServlet{// 是否安全privateUserService userService =newUserServiceImpl();publicvoiddoGet(HttpServletRequest request,HttpServletResponse response){
 userService.update(...);}}publicclassUserServiceImplimplementsUserService{// 是否安全privateUserDao userDao =newUserDaoImpl();publicvoidupdate(){
 userDao.update();}}publicclassUserDaoImplimplementsUserDao{publicvoidupdate(){// 是否安全privateConnection conn =null;publicvoidupdate()throwsSQLException{String sql ="update user set password = ? where username = ?";
 conn =DriverManager.getConnection("","","");// ...
 conn.close();}}

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

例6

publicclassMyServletextendsHttpServlet{// 是否安全privateUserService userService =newUserServiceImpl();publicvoiddoGet(HttpServletRequest request,HttpServletResponse response){
 userService.update(...);}}publicclassUserServiceImplimplementsUserService{publicvoidupdate(){UserDao userDao =newUserDaoImpl();
 userDao.update();}}publicclassUserDaoImplimplementsUserDao{// 是否安全privateConnection=null;publicvoidupdate()throwsSQLException{String sql ="update user set password = ? where username = ?";
 conn =DriverManager.getConnection("","","");// ...
 conn.close();}}
UserServiceImpl

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

例7

publicabstractclassTest{publicvoidbar(){// 是否安全SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");foo(sdf);}publicabstractfoo(SimpleDateFormat sdf);publicstaticvoidmain(String[] args){newTest().bar();}}

foo 方法是抽象方法,所以它的行为是不确定的,可能导致不安全的方法,被称之为外星方法

publicvoidfoo(SimpleDateFormat sdf){String dateStr ="1999-10-11 00:00:00";for(int i =0; i <20; i++){newThread(()->{try{
                sdf.parse(dateStr);}catch(ParseException e){
                e.printStackTrace();}}).start();}}

例8

privatestaticInteger i =0;publicstaticvoidmain(String[] args)throwsInterruptedException{List<Thread> list =newArrayList<>();for(int j =0; j <2; j++){Thread thread =newThread(()->{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的代码块上了锁,所以是线程安全的。

标签: java jvm 开发语言

本文转载自: https://blog.csdn.net/weixin_53029342/article/details/128903824
版权归原作者 背书包的小新 所有, 如有侵权,请联系我们删除。

“【Java并发编程】变量的线程安全分析”的评论:

还没有评论