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

这篇具有很好参考价值的文章主要介绍了【Java并发编程】变量的线程安全分析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

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

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

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

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

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

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

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

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: 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 分析相同

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

但如果将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 相同

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

方法访问修饰符带来的思考,如果把 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 {
    @Override
    public 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类的源码就可以发现,

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

  1. String 类是一个 final 类

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

  1. 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. 是否是共享变量
  2. 是否存在多个线程并发
  3. 是否有写操作

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

例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.toymoban.com/news/detail-476246.html

到了这里,关于【Java并发编程】变量的线程安全分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java并发编程面试题——线程池

    参考文章: 《Java 并发编程的艺术》 7000 字 + 24 张图带你彻底弄懂线程池 (1) 线程池 (ThreadPool) 是一种用于 管理和复用线程的机制 ,它是在程序启动时就预先创建一定数量的线程,将这些线程放入一个池中,并对它们进行有效的管理和复用,从而在需要执行任务时,可以从

    2024年02月07日
    浏览(48)
  • Java并发编程之线程池详解

    目录 🐳今日良言:不悲伤 不彷徨 有风听风 有雨看雨 🐇一、简介 🐇二、相关代码 🐼1.线程池代码 🐼2.自定义实现线程池 🐇三、ThreadPoolExecutor类 首先来介绍一下什么是线程池,线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程

    2024年02月12日
    浏览(43)
  • 关于并发编程与线程安全的思考与实践 | 京东云技术团队

    作者:京东健康 张娜 并发编程的意义是充分的利用处理器的每一个核,以达到最高的处理性能,可以让程序运行的更快。而处理器也为了提高计算速率,作出了一系列优化,比如: 1、硬件升级:为平衡CPU 内高速存储器和内存之间数量级的速率差,提升整体性能,引入了多

    2024年02月07日
    浏览(66)
  • JUC并发编程-集合不安全情况以及Callable线程创建方式

    1)List 不安全 ArrayList 在并发情况下是不安全的 解决方案 : 1.Vector 2.Collections.synchonizedList() 3. CopyOnWriteArrayList 核心思想 是,如果有 多个调用者(Callers)同时要求相同的资源 (如内存或者是磁盘上的数据存储),他们 会共同获取相同的指针指向相同的资源 , 直到某个调用者

    2024年01月23日
    浏览(45)
  • 【Java 并发编程】一文读懂线程、协程、守护线程

    在 Java 线程的生命周期一文中提到了 就绪状态的线程在获得 CPU 时间片后变为运行中状态 ,否则就会在 可运行状态 或者 阻塞状态 ,那么系统是如何分配线程时间片以及实现线程的调度的呢?下面我们就来讲讲线程的调度策略。 线程调度是指系统为线程分配 CPU 执行时间片

    2023年04月08日
    浏览(58)
  • Java面试_并发编程_线程基础

    进程是正在运行程序的实例, 进程中包含了线程, 每个线程执行不同的任务 不同的进程使用不同的内存空间, 在当前进程下的所有线程可以共享内存空间 线程更轻量, 线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程) 并发是单个

    2024年02月07日
    浏览(49)
  • 【Java并发编程】线程中断机制(辅以常见案例)

    本文由浅入深介绍了中断机制、中断的常见案例和使用场景。 因为一些原因需要取消原本正在执行的线程。我们举几个栗子: 假设踢足球点球时,A队前4轮中了4个球,B队前4轮只中了2个球,此时胜负已分,第5轮这个点球就不用踢了,此时需要停止A队的线程和B队的线程(共

    2024年02月13日
    浏览(35)
  • java并发编程:多线程基础知识介绍

    最初的计算机只能接受一些特定的指令,用户每输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。这样效率非常低下,在很多时候,计算机都处在等待状态。 后来有了 批处理操作系统 ,把一系列需要操作的指令写下来,形成一个清单,一次

    2024年02月07日
    浏览(49)
  • Java并发编程学习16-线程池的使用(中)

    上篇分析了在使用任务执行框架时需要注意的各种情况,并简单介绍了如何正确调整线程池大小。 本篇将继续介绍对线程池进行配置与调优的一些方法,详细如下: ThreadPoolExecutor 为 Executors 中的 newCachedThreadPool 、 newFixedThreadPool 和 newScheduledThreadExecutor 等工厂方法返回的 Exe

    2024年02月10日
    浏览(42)
  • Java并发编程学习笔记(一)线程的入门与创建

    认识 程序由指令和数据组成,简单来说,进程可以视为程序的一个实例 大部分程序可以同时运行多个实例进程,例如记事本、画图、浏览器等 少部分程序只能同时运行一个实例进程,例如QQ音乐、网易云音乐等 一个进程可以分为多个线程,线程为最小调度单位,进程则是作

    2024年02月16日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包