线程安全实例分析

news2024/11/26 3:26:53

一、变量的线程安全分析

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

● 如果它们没有共享,则线程安全
● 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
—— 如果只有读操作,则线程安全
—— 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?
● 局部变量是线程安全的
● 但局部变量引用的对象则未必
—— 如果该对象没有逃离方法的作用访问,它是线程安全的
—— 如果该对象逃离方法(eg:使用return)的作用范围,需要考虑线程安全

1.1 线程安全分析-局部变量

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

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

反编译后的二进制字节码:

public static void test1();
 descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10        // 准备常数10赋值给i
 2: istore_0         // 赋值给i
 3: iinc 0, 1        // 在局部变量i的基础上自增 
 6: return           // 方法运行结束返回
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3 4 0 i I

每个方法调用时都会创建一个栈帧,每个线程有自己独立的栈和栈帧内存(局部变量会在栈帧中被创建多份)
如图:
在这里插入图片描述
若局部变量的为对象,则稍有不同
观察一个成员变量的例子

public class TestThreadSafe {
    // 创建两个线程(每个线程调用method1循环200次)
    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+1)).start();
        }
    }
}
class ThreadUnsafe {
     ArrayList<String> list = new ArrayList<>();
     public void method1(int loopNumber) {
         for (int i = 0; i < loopNumber; i++) {
         // { 临界区, 会产生竞态条件
         // method2()、method3()访问的为共享资源(多个线程执行时会发生指令交错)
             method2();
             method3();
           // } 临界区
      }
 }
 private void method2() {
     // 往集合中加一个元素
      list.add("1");
 }
     // 往集合中移除一个元素
 private void method3() {
      list.remove(0);
    }
}

多个线程执行时会发生指令交错会产生问题

运行结果:其中一种情况是线程1的method2()还未add,线程2的method3()尝试移除,此时集合为空就会报错
在这里插入图片描述

分析
● 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
● method3 与 method2 分析相同
在这里插入图片描述
若将 list 修改为局部变量就不会存在上述问题

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    public void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        System.out.println(1);
        list.remove(0);
    }
}

分析
● list 是局部变量,每个线程调用时会创建其不同实例,没有共享
● 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象(均引用的为堆中的对象)
● method3 的参数分析与 method2 相同
在这里插入图片描述

1.2 线程安全分析-局部变量引用

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
● 情况1:有其它线程调用 method2 和 method3
● 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即

class ThreadSafe {
    public final void method1(int loopNumber) {
         ArrayList<String> list = new ArrayList<>();
          for (int i = 0; i < loopNumber; i++) {
              method2(list);
              method3(list);
    }
 }
   private void method2(ArrayList<String> list) {
         list.add("1");
 }
   private void method3(ArrayList<String> list) {
         list.remove(0);
   }
}
// 添加子类继承ThreadSafe,在子类中覆盖/重写method3
class ThreadSafeSubClass extends ThreadSafe{
     @Override
     public void method3(ArrayList<String> list) {
     // 重写后重启一个新的线程
        new Thread(() -> {
            list.remove(0);
          }).start();
   }
}

此时会带来线程安全问题,新的线程可以访问到共享变量
从这个例子可以看出 private 或 final 提供【安全】的意义所在,可以体会开闭原则中的【闭】(使用private修饰符避免子类改变覆盖其行为)

1.3 线程安全分析-常见类-组合调用

常见线程安全类
● String
● Integer
● StringBuffer
● Random
● Vector
● Hashtable
● java.util.concurrent 包下的类

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

Hashtable table = new Hashtable();   // 查看源码,发现其底层被synchronized关键字修饰
    new Thread(()->{
       table.put("key", "value1");
}).start();
    new Thread(()->{
       table.put("key", "value2");
}).start();

● 它们的每个方法是原子的
● 但注意它们多个方法的组合不是原子的,见后面分析

线程安全类方法的组合

分析下面代码是否线程安全?
get()、put()底层均有synchronized修饰

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

将两个方法组合到一起使用就不是线程安全的,中间会受到线程上下文切换的影响,其只能保证每一个方法内部代码是原子的。要使其组合后仍可以保证原子性,还需在外层加以线程安全的保护!

eg:线程1、2均执行方法内的代码,线程1执行get(“key”) == null,还未执行完,线程发生上下文切换轮到线程2执行,线程2也执行到此处得到的get(“key”) == null,线程2发现为null后put(“key”, v2),完成后又切换为线程1,线程1又put(“key”, v1)。理论上判断为空时,我们只存放一个键值对,实际上put(“key”, value)被执行两次,导致后一个执行的put将前一个执行put的结果覆盖,不是我们锁预期的效果。
在这里插入图片描述

1.3 线程安全分析-常见类-不可见

不可变类线程安全
String、Integer 等都是不可变类,因为其内部的状态(属性)不可以改变,因此它们的方法都是线程安全的(只可读不可修改)

那么,String 有 replace,substring 等方法【可以】改变值,那么这些方法又是如何保证线程安
全的?(其没有改变字符串的值,而是创建了一个新的字符串对象对原有的字符串复制,里面包含截取后的结果)用新的对象实现对象的不可变效果

public class Immutable{
      private int value = 0;
    public Immutable(int value){
      this.value = value;
 }
 public int getValue(){
      return this.value;
   }
}

如果想增加一个增加的方法应该如何实现?

public class Immutable{
    private int value = 0;
 public Immutable(int value){
    this.value = value;
 }
 public int getValue(){
    return this.value;
 }
 
 public Immutable add(int v){
     return new Immutable(this.value + v);
   } 
}

1.4 线程安全分析-实例分析

例1
Servlet运行Tomcat环境下,只有一个实例(会被Tomcat多个线程所共享使用)

public class MyServlet extends HttpServlet {
   // 是否安全?
   /*Map不是线程安全的,线程安全的实现有HashTable,而HashMap并非线程安全,
    若多个请求线程访问同一个Servlet,有的存储内容而有的读取内容,会造成混乱*/
   Map<String,Object> map = new HashMap<>();
   // 是否安全?
   /*是线程安全的,字符串属于不可变量*/
   String S1 = "...";
   // 是否安全?(是)
   final String S2 = "...";
   // 是否安全?(不是)
   Date D1 = new Date();
   // 是否安全?
   /*final修饰后只能说明D2这个成员变量的引用值固定,而Date中的其它属性还可以可变的)*/
  final Date D2 = new Date();
 
   public void doGet(HttpServletRequest request, HttpServletResponse response) {
  // 使用上述变量
   }
}

例2:Servlet调用Service

public class MyServlet extends HttpServlet {
    // 是否安全?
    /*不是===>Servlet只有一份,而userService是Servlet的一个成员变量,
      因此也只有一份,会有多个线程共享使用*/
    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++;
   }
}

例3

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

spring中若未指定scope为非单例。默认为单例模式(需要被共享,其成员变量也需被共享,因此无论是执行复制操作还是下面执行减法运算,都会涉及到对象对成员变量的并发修改,会存在线程安全问题)

如何解决上述问题?
可以使用环绕通知(环绕通知可以将开始时间、结束时间变为环绕通知中的局部变量,此时便可保证线程安全)

例4

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

① Dao无成员变量,意味着即使有多个线程访问也不能修改它的属性、状态===>没有成员变量的类都是线程安全的
② Connection也是线程安全的,Connection属于方法内的局部变量,即使有多个线程访问,线程1创建的为Connection1而线程2创建的为Connection2,两者独立互不干扰

例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 {
     // 是否安全(不安全)
     /*Connection不为方法内的局部变量,而是做为Dao的成员变量(Dao只有一份会被多个线程共享,其内的共享变量也会被线程共享)*/
     /*eg:线程1刚创建Connection还未使用,此时线程2close()*/
     private Connection conn = null;
     public void update() throws SQLException {
       String sql = "update user set password = ? where username = ?";
       conn = DriverManager.getConnection("","","");
 // ...
       conn.close();
   }
}

对于Connection这种对象应将其变为线程内私有的局部变量,而不是设置为共享的成员变量

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

UserDao在Service中作为方法内的局部变量存在,每一个线程调用时都会创建一个新的UserDa0对象,其内部的Connection也为新的。因此线程安全。

例7

public abstract class Test {
 
     public void bar() {
     // 是否安全
     /*SimpleDateFormat虽然为方法内的局部变量,但其会暴露给其他线程(抽象方法其子类可能会产生一些不恰当的操作)*/
     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();
   }
}

不想向外暴露的变量可以使用final、private修饰,这样可以增强类的安全性

可以比较 JDK 中 String 类的实现:String类是不可变的,同时也是final的

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

为何将String类涉及为final?:若不使用final修饰,其子类也许可能覆盖掉String父类中的一些行为,导致线程不安全的发生(子类可能会破坏父类中某一方法的行为)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32学习笔记-USART串口通信+与野火STM32F407板载ESP8266进行通信

文章目录STM32USART介绍STM32USART框图第一部分第二部分第三部分发送器时序图接收器第四部分软件部分&#xff1a;STM32通过USART与板载ESP8266通讯实验板载WIFI模块电路图实现方式&#xff1a;第一步&#xff1a;配置USART1和USART3的GPIO及其中断第二步&#xff1a;通过中断服…

(2023版)零基础入门网络安全/Web安全,收藏这一篇就够了

由于我之前写了不少网络安全技术相关的文章和回答&#xff0c;不少读者朋友知道我是从事网络安全相关的工作&#xff0c;于是经常有人私信问我&#xff1a; 我刚入门网络安全&#xff0c;该怎么学&#xff1f; 要学哪些东西&#xff1f; 有哪些方向&#xff1f; 怎么选&#x…

智慧渔业海洋鱼类捕捉系统

我国的水产捕捞业可分为海洋捕捞、远洋捕捞和淡水捕捞三类&#xff0c;其中淡水渔业是指在淡水水域进行捕捞、养殖和加工淡水水产品的社会生产领域。近年来&#xff0c;随着经济水平的提高和淡水渔业的快速发展&#xff0c;捕捞业规模不断壮大。尽管渔业已从单纯的捕捞发展为系…

卷麻了,00后测试用例写的比我还好,简直无地自容......

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 如何编写测试用例&#xff1f; 作为一个测试新人&#xff0c;刚开始接触测试&#xff0c;对于怎么写测试用例很是头疼&#xff0c;无法…

Apple主推的智能家居是什么、怎么用?一篇文章带你从零完全入门 HomeKit

如果你对智能家居有所了解&#xff0c;那应该或多或少听人聊起过 HomeKit。由 Apple 开发并主推的的 HomeKit 既因为产品选择少、价格高而难以成为主流&#xff0c;又因其独特的优秀体验和「出身名门」而成为智能家居领域的焦点。HomeKit 究竟是什么&#xff1f;能做什么&#…

供应链的有效管理,分析指标有哪些

对于企业而言&#xff0c;供应链是一个很复杂的、体系化的生态系统&#xff0c;从原材料、到供应商、到生产、仓库、物流&#xff0c;最后到达经销商或者最终客户那里&#xff0c;这个链条很长。相关的分析指标也有很多&#xff0c;在这些指标里面也有非常多可以扩展、延申的内…

Android 系统的启动流程

前言&#xff1a;从开机的那一刻&#xff0c;到开机完成后launcher将所有应用进行图标展示的这个过程&#xff0c;大概会有哪一些操作&#xff1f;执行了哪些代码&#xff1f;作为Android开发工程师的我们&#xff0c;有必要好好的梳理一遍。既然要梳理Android系统的启动流程&a…

Python-scatter散点图及颜色大全

# -*- coding: utf-8 -*- import numpy as np import matplotlib.pyplot as pltplt.rcParams[font.sans-serif][SimHei] plt.rcParams[axes.unicode_minus] False #matplotlib画图中中文显示会有问题&#xff0c;需要这两行设置默认字体plt.xlabel(X) plt.ylabel(Y) plt.xlim…

人工智能——离线情况下自动给视频添加字幕,支持中文,英文,日文等等

前言 最近打开百度网盘&#xff0c;看到播放视频有一个AI字幕功能&#xff0c;心情非常激动&#xff0c;看视频的同时可以看自动生成的字幕&#xff0c;防止听不清视频中人物的话语 然而不是SVIP,我试用过了之后就没有这个功能选项了 我在想&#xff0c;如果随便哪一个“免费”…

Windows 安装RocketMQ

文章目录一、RocketMQ是什么&#xff1f;二、准备工作1.环境要求2.下载与解压3.启动MQ4. 测试是否成功启动三、安装管理端1. 代码下载2. 修改配置文件3. 启动MQ客户端jar包四、rocketMQ代码的使用入门五、问题记录1. 启动mqbroker.cmd没有反应2.消费者重复消费消息一、RocketMQ…

二叉搜索树实现

树的导览 树由节点&#xff08;nodes&#xff09;和边&#xff08;edges&#xff09;构成&#xff0c;如下图所示。整棵树有一个最上端节点&#xff0c;称为根节点&#xff08;root&#xff09;。每个节点可以拥有具有方向的边&#xff08;directed edges&#xff09;&#xf…

第51天|LeetCode503.下一个更大元素 II、LeetCode42. 接雨水

1.题目链接&#xff1a;下一个更大元素 II 题目描述&#xff1a; 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序&#…

Android kotlin 组件间通讯 - LiveEventBus 及测试(更新中)

<<返回总目录 文章目录 一、LiveEventBus是什么二、测试一、LiveEventBus是什么 LiveEventBus是Android中组件间传递消息,支持AndroidX,Event:事件,Bus:总线 范围全覆盖的消息总线解决方案 进程内消息发送App内,跨进程消息发送App之间的消息发送更多特性支持 免配…

进制转换(二进制,八进制,十进制,十六进制)涵盖整数与小数部分,内容的图片全为手写【详细图解】

各种进制之间的相互转换1. 各进制表示数1.1 数码1.2 基数1.3 位权2. 十进制转换为其他进制2.1 整数部分2.2 小数部分3. 其他进制转换为十进制4. 二进制转换为八进制5. 二进制转换为十六进制6. 八进制转换为十六进制1. 各进制表示数 二进制&#xff1a;0&#xff0c;1逢二进一 八…

Java企业开发学习笔记(5下)采用注解方式使用AOP

该文章主要为完成实训任务&#xff0c;详细实现过程及结果见【http://t.csdn.cn/FBkpc】 文章目录二、采用注解方式使用AOP2.1 创建所需自包2.2 创建杀龙任务2.3 创建勇敢骑士类2.4 创建吟游诗人切面2.5 创建Spring配置类2.6 创建骑士测试类2.7 运行测试方法testBraveKnight()&…

【学习总结】Kalibr标定相机与IMU

本文仅用于记录自己学习过程。 使用方法 Kalibr包括&#xff1a;相机内参&#xff0c;多相机外参&#xff0c; (已知IMU和相机内参的)相机与IMU标定&#xff0c;以及扩展Kalibr支持IMU内参标定。 当已知IMU内参和相机内参后&#xff0c;使用按照指定方式录制的rosbag&#x…

西电数据库简答题核心考点汇总(期末真题+知识点)

文章目录前言一、关系代数1.1 真题一1.2 真题二二、SQL语句2.1 真题一2.2 真题二三、事务3.1 真题一四、关系理论4.1 真题一4.2 真题二五、数据库设计5.1 样例一5.2 考题二前言 主要针对西安电子科技大学《数据库系统》的概念核心考点进行汇总&#xff0c;包含核心考点。 【期…

第14天-ElasticSearch环境配置,构建检索服务及商品上架到ES库

1.ElasticSearch概念 官网介绍&#xff1a;https://www.elastic.co/cn/what-is/elasticsearch/ 官网学习文档&#xff1a;https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html 1.1.ElasticSearch与MySQL的比较 MySQL有事务性,而ElasticSearch没有…

GEBCO海洋数据下载

一、数据集简介 GEBCO&#xff08;General Bathymetric chart of the Oceans&#xff09;旨在为世界海洋提供最权威的、可公开获取的测深数据集。 目前的网格化测深数据集&#xff0c;即GEBCO_2022网格&#xff0c;是一个全球海洋和陆地的地形模型&#xff0c;在15角秒间隔的…

GAN | 代码简单实现生成对抗网络(GAN)(PyTorch)

2014年GAN发表&#xff0c;直到最近大火的AI生成全部有GAN的踪迹&#xff0c;快来简单实现它&#xff01;&#xff01;&#xff01;GAN通过计算图和博弈论的创新组合&#xff0c;他们表明&#xff0c;如果有足够的建模能力&#xff0c;相互竞争的两个模型将能够通过普通的旧反向…