Java并发编程之线程知识五:线程安全

本文围绕Java线程安全展开,介绍了线程、共享资源、锁等基础概念,指出存在多个线程同时修改一个共享资源会造成线程不安全。分析了静态变量、实例变量和局部变量的线程安全性,最后给出避免线程不安全的方法,如用局部变量、不对共享资源修改、加锁和使用线程安全类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.基础概念

2.造成线程不安全的条件 

3.变量在JVM内存中的存储

4.变量种类与线程安全

5.如何避免线程不安全


1.基础概念

  • 线程:线程是程序中一个单一的顺序控制流程,在单个程序中同时运行多个线程完成不同的工作,称为多线程。
    例如:在电子商务网站中用户发起一个请求,服务器从收到这条请求开始到处理完所有的业务逻辑返回结果的过程一般就是一个线程。当客户端N多的请求同时请求服务器,这就是多线程并发。
  • 共享资源:允许被不同线程访问的资源,共享资源是多线程中允许被不只一个线程访问的类变量或实例变量,共享资源可以是单个类变量或实例变量,也可以是一组类变量或实例变量。
  • 锁:当有多个线程共用一共享资源的时候,便会出现资源争抢(冲突),锁就是用来解决这种冲突,保证各个线程有序使用资源的。这个解决冲突的过程跟上厕所一样,假如有ABC三个人都来上厕所而厕所只有一个坑,一次只能进一人,A先来了,那么在A出来之前,这个厕所就处在了“锁”定状态,B和C憋死也要在外面等着,直到A出门(原因很多,如睡着了,方便完了,忘带厕纸了跑出来找人要....)“锁”定解除B和C才能进入。
  • 线程安全:一个资源在可以被多个线程中的对象调用情况下,不会出现任何冲突的时候就是线程安全的。

2.造成线程不安全的条件 

一个线程操作共享资源的过程如下: 
将共享资源从主内存拷贝副本到工作内存==>对该副本进行修改操作==>将该副本从工作内存写回到主内存,这样就可能出现在两个线程同时将主内存中的共享资源副本拷贝到各自的工作内存,但是两个线程在将自己修改后的副本放回主内存的时候就会有先后问题,后放回去的就会覆盖先放回去的内容这就造成了线程不安全。
造成线程不安全的唯一条件就是“存在多个线程同时修改一个共享资源”。
其中包括下面两个关键点:
(1)存在共享资源
(2)不同线程同时对共享资源修改

3.变量在JVM内存中的存储

4.变量种类与线程安全

4.1.静态变量

静态变量即类变量,位于JVM内存的方法区,为所有对象共享,一旦静态变量被修改,其他对象均对修改可见,故静态变量是线程不安全的。

4.1.1.静态变量线程不安全测试代码

4.1.1.1StaticVariableTest.java代码
public class StaticVariableTest extends Thread {
    /**
     * 静态变量(共享资源)
     */
    private static int static_i;

    public void run() {
        for(int i = 0; i <= 10; i++){
            static_i = i;
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(static_i != i){
System.err.println("[" + Thread.currentThread().getId()   + "]当i=" + i + "获取static_i 的值:" + static_i);
            } else {
System.out.println("[" + Thread.currentThread().getId() + "]当i=" + i + "获取static_i 的值:" + static_i);
            }
        }
    }
}

4.1.1.2.StaticMain.java代码
public class StaticMain{
    public static void main(String[] args) {
        // 启动尽量多的线程才能很容易的模拟出问题
        for (int i = 0; i < 10; i++) {
            StaticVariableTest t = new StaticVariableTest();
            t.start();
        }
    }
}

4.1.1.3.执行结果
 
4.1.1.4.分析结果

出现图中的结果场景,当线程15执行了static_i = 9; 后,线程15进入了sleep状态(在业务系统中可能是执行很多其他的业务代码),这时候当某个线程刚好执行到static_i = 10;static_i醒来这时候static_i的值已经被改为10了,线程15继续执行下面的判断if(static_i != i)就出现了上面的结果。

4.2.    实例变量

实例变量为对象实例私有,在JVM内存的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。

4.2.1.实例变量线程不安全测试代码

4.2.1.1.InstanceVariableTest.java代码
public class InstanceVariableTest {
    /**
     * 实例变量(当InstanceVariableTest为单例的时候是共享资源)
     */
    private int instance_i;

private static InstanceVariableTest instanceVariableTest = new InstanceVariableTest();
    
    /**
     * 把构造器私有化,确保单例
     */
    private InstanceVariableTest(){}
    
    public static InstanceVariableTest getInstance(){
        return instanceVariableTest;
    }
    
    public void runInstanceVariableTest() {
        for(int i = 0; i <= 10; i++){
            instance_i = i;
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(instance_i != i){
System.err.println("[" + Thread.currentThread().getId()   + "]当i=" + i + "获取static_i 的值:" + instance_i);
            } else {
System.out.println("[" + Thread.currentThread().getId() + "]当i=" + i + "获取static_i 的值:" + instance_i);
            }
        }
    }
}

4.2.1.2.InstanceThread.java代码
public class InstanceThread extends Thread {
    public void run() {
        InstanceVariableLockTest ivt = InstanceVariableLockTest.getInstance();
        ivt.runInstanceVariableTest();
    }
}

4.2.1.3.InstanceMain.java代码
public class InstanceMain {
    
    public static void main(String[] args) {
        // 启动尽量多的线程才能很容易的模拟出问题
        for (int i = 0; i < 10; i++) {
            InstanceLockThread t = new InstanceLockThread();
            t.start();
        }
    }
}

4.2.1.4.执行结果

4.2.1.5.结果分析

出现图中的结果场景,当线程11执行了static_i = 5; 后,线程11进入了sleep状态(在业务系统中可能是执行很多其他的业务代码),这时候当某个线程刚好执行到static_i = 6;static_i醒来这时候static_i的值已经被改为6了,线程11继续执行下面的判断if(static_i != i)就出现了上面的结果。

4.3.局部变量

每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。

4.3.1.局部变量线程安全测试代码

注:这里所说的局部变量是指在方法内部声明和定义的变量,不包括方法传递的参数(从某种角度上说参数也算是局部变量),当方法的参数传递的是一个对象引用的时候也会存在线程安全问题。
具体的测试代码见4.4

4.3.1.1.LocalVariableTest.java代码
public class LocalVariableTest extends Thread {
    public void run() {
        
        /**
         * 局部变量(非共享资源)
         */
        int local_i;
        
        for(int i = 0; i <= 10; i++){
            local_i = i;
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(local_i != i){
System.err.println("[" + Thread.currentThread().getId()   + "]当i=" + i + "获取static_i 的值:" + local_i);
            } else {
System.out.println("[" + Thread.currentThread().getId() + "]当i=" + i + "获取static_i 的值:" + local_i);
            }
        }
    }
}

4.3.1.2.LocalMain.java代码
public class LocalMain {
    
    public static void main(String[] args) {
        // 启动尽量多的线程才能很容易的模拟出问题
        for (int i = 0; i < 10; i++) {
            LocalVariableTest t = new LocalVariableTest();
            t.start();
        }
    }
}

4.3.1.3.执行结果

4.3.1.4.结果分析

由于每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间是不共享的,这样就不满足造成线程不安全的唯一条件“存在多个线程同时修改一个对象实例”,所以局部变量是线程安全的。

4.4.引用参数传递的线程不安全测试代码
 

4.4.1.User.java代码
public class User {
    
    private String name;

    private static User user = new User("zhangsan");;
    
    private User(){}
    private User(String name){}
    
    public static User getUser(){
        return user;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

4.4.2.ReferParamTransTest.java代码
public class ReferParamTransTest {
    
    public void referParamTransRun(User user){
        for(int i = 0; i <= 10; i++){
            String newName = "name" + Thread.currentThread().getId();
            user.setName(newName);
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(newName.equals(user.getName())){
System.out.println("[" + Thread.currentThread().getId() + "]当name=" + newName + "获取name 的值:" + user.getName());
            } else {
System.err.println("[" + Thread.currentThread().getId() + "]当name=" + newName + "获取name 的值:" + user.getName());
            }
        }
    }
}

4.4.3.ReferParamTransThread.java代码
public class ReferParamTransThread extends Thread {

    public void run() {
        ReferParamTransTest rt = new ReferParamTransTest();
        rt.referParamTransRun(User.getUser());
    }
}

4.4.4.ReferParamTransMain.java代码
public class ReferParamTransMain {

    public static void main(String[] args) {
        for(int i=0; i<10; i++){
            ReferParamTransThread t = new ReferParamTransThread();
            t.start();
        }
    }
}

4.4.5.执行结果

4.4.6.结果分析

这个例子中的user对象虽然从头到尾都是被当作参数在方法中传递,但是由于User对象是单例的,所以多线程的情况下不同线程中方法中传递的user对象都是同一个实例对象(共享资源)。同时,由于java中的非基本数据类型(注1)对象参数传递采用的是“引用传递”,而不是 “值传递”,所以一个线程在接受到user实例对象的referParamTransRun()方法中队user实例对象做了修改,对其它的线程是可见的。这样就会出现下面的场景:当线程11给user对象的name属性赋值为name12之后,进入睡眠状态,这个时候线程12将ser实例对象的name属性修改为name12,当name11醒来的时候继续执行下面的代码用到的user对象就不是自己想象的了,而是被线程12篡改过的。这样就导致了线程不安全问题。

注1:java中非基本数据类型中的String是个例外,它和基本数据类型一样也是采用的是值传递。

5.如何避免线程不安全

  • 尽量使用局部变量:这样做是为了不满足造成线程不安全条件中的“存在共享资源”。
  • 尽量不去做对共享资源修改的操作:这样做是通过不对共享资源进行修改,从而达到不满足造成线程不安全条件中的“不同线程同时对共享资源修改”的目的。
  • 对共享资源加锁:若必须用到共享资源又需要对其修改资源,那就对共享资源加锁,确保在一个时刻只有一个线程在操作它。最常用的加锁方式就是添加synchronized关键字
  • 使用线程安全的类:若必须用到共享资源又需要对其修改,那就使用线程安全的类,如:Map要用java.util.concurrent包下面的ConcurrentHashMap而不能用 HashMap 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hughjin

动力动力动力动力动力动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值