Java线程安全——关于多线程并发环境下,数据的安全问题

目录

一、(重点)关于多线程并发环境下,数据的安全问题

1.1 什么时候数据在多线程并发的情况下存在安全问题?

1.2 那么怎么解决这个问题呢?

1.3. 线程同步,涉及以下两个专业术语:

1.4 三种同步机制:

1.4.1 同步代码块

1.4.2 同步方法

1.4.3 还可以在静态方法上使用synchronized(锁当前类)

1.4.3 Lock 锁

1.4 关于线程安全(银行账户)案例:

1.5 线程私有数据和线程共享数据对多线程并行有什么影响吗?

二、内存结构与线程安全​​​​​​​


一、(重点)关于多线程并发环境下,数据的安全问题

线程安全问题的出现主要是因为多线程并发访问共享资源时可能导致的竞态条件(Race Condition)和并发访问冲突。以下是一些常见的原因:

  • 竞态条件(Race Condition):多个线程同时访问共享资源,并且执行的顺序不确定,这可能导致不同的线程在不同的时间点对资源进行操作,从而产生意外的结果。
  • 共享资源:线程安全问题通常发生在多个线程共享同一资源(如变量、数据结构、文件、数据库等)的情况下。如果不正确地管理对共享资源的访问,就会发生冲突。
  • 并发修改:多个线程同时对数据结构(如集合、数组、映射等)进行修改,如果没有适当的同步控制,可能导致数据结构的损坏或不一致。
  • 非原子操作:某些操作不是原子操作,它们在多线程环境中可能被中断,导致部分操作被执行,而不是完整的原子操作。
  • 缓存不一致:多个线程使用不同的本地缓存副本,而不是从主内存中获取最新的值。这可能导致不同线程看到不同的数据状态。
  • 不同步的访问:没有适当的同步机制(如锁、信号量、条件变量等)来确保多线程之间的协调和互斥,从而导致竞态条件和并发问题。
  • 死锁:当多个线程在等待彼此释放锁资源时,可能导致死锁问题,其中所有线程都无法继续执行。
  • 饥饿:某些线程可能会永远无法获得所需的资源,因为其他线程总是优先获得资源。

为了解决线程安全问题,开发者需要采用适当的同步机制(如锁、信号量、条件变量等),以确保多线程之间的协调和互斥,防止竞态条件和并发问题的发生。此外,选择合适的数据结构和算法,以及遵循最佳的并发编程实践,也是确保线程安全的关键。

1.1 什么时候数据在多线程并发的情况下存在安全问题?

有三个条件:
    - 条件一:多线程并发
    - 条件二:有共享数据
    - 条件三:共享数据有修改的行为

1.2 那么怎么解决这个问题呢?

- 采用“线程同步机制”
         - 即线程排队执行,不能并发。也就是说把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

1.3. 线程同步,涉及以下两个专业术语:

  - 异步编程模型:
         - 线程t1和t2,各自执行各自的,谁也不需要等谁,也就是:多线程并发(效率较高)
  - 同步编程模型:
        - 线程t1和t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说线程t2执行的时候,必须等待t1线程结束,两个线程之间发生了等待关系。即线程排队执行。(效率较低)
 

线程同步机制的语法:synchronized (){ 
                         线程同步代码块 }
 \* synchronized ()这个括号里面传递的数据是非常关键的。并且不能为null值
 \* 这个数据必须是多线程共享的数据。才能达到多线程排队。
 那么()中写的就是你想让哪些线程同步。
 假设有t1,t2,t3,t4,这四个线程,你只希望t1,t2排队,
那么你一定要在()中写一个t1,t2共享的对象。而这个对象对t3,t4来说不是共享的。。。。
如何保证线程安全呢?具体的措施有
  • synchronized同步
  • Lock
  • volatile关键字

​​​​​​​下面用synchronized保证线程安全问题

synchronized Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是 多个线程之间访问资源的同步性, 可以保证被它修饰的方法或者代码块在任意时刻只 能有一个线程执行。

1.4 三种同步机制:

  1. synchronized 同步代码块
  2. synchronized 同步方法
  3. synchronized 修饰静态方法
  4. 锁机制【Lock 锁】
1.4.1 同步代码块

`synchronized `关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

语法:
synchronized(同步锁){
    //同步的代码
}

这里的同步锁:
对象的同步锁只是一个概念,可以想象为在任意对象上标记了一个锁,具体的就是上面讲述的。

1.4.2 同步方法

使用 synchronized 修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:
public synchronized void method(){
    //可能产生安全问题的代码
}

那么同步方法中的同步锁是谁呢?
        对于“实例方法” ,同步锁就是 this 。
        对于 “static 方法” ,我们使用当前方法所在类的字节码对象(类名.class)

1.4.3 还可以在静态方法上使用synchronized(锁当前类)

​​​​​​​给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定 实例,被类的所有实例共享。

synchronized static void method() {
//业务代码
}
静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互 斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需 要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥 现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
1.4.3 Lock 锁

`java.util.concurrent.locks.Lock `机制提供了比 synchronized 代码块和 synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能 Lock 都有,除此之外更强大,更体现面向对象。在 JDK 5 引入了 `ReentrantLock `, `ReentrantLock` 重入锁,是实现` Lock 接口`的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。支持公平锁和非公平锁两种方式。

  • 常用方法
  - `public void lock() :`加同步锁。
  - `public void unlock()` :释放同步锁

代码演示:

public class Ticket implements Runnable {
    private int ticket = 100;
    private Lock lock = new ReentrantLock();
    /**
    * 执行卖票操作
    */
    @Override
    public void run() {
        while (true) {
            // 加同步锁
            lock.lock();
            //产生安全问题的方法
            sellTicket();
            // 释放同步锁
            lock.unlock();
        }
    }
    private void sellTicket(){
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在卖第" + ticket-- + "张票");
        }
    }
}

1.5 关于线程安全(银行账户)案例:

//关于线程安全(银行账户):
/*不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题*/
public class test03 {
    public static void main(String[] args) {
        //创建账户对象,只创建一个
        Account2 account2 = new Account2("act01",10000);
        //创建两个线程对同一个账户取款
        Thread t1 = new AccountThread(account2);
        Thread t2 = new AccountThread(account2);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
class AccountThread extends Thread{
    //两个线程必须共享同一个账户对象
   private Account2 act = null;//实例变量
   //通过构造方法把账户对象传递过来
   public AccountThread (Account2 act){
       this.act = act;
   }

    @Override
    public void run() {
       //run方法的执行表示取款操作
        double money = 5000;
        //取款
        /*synchronized (act){     //这种方法也可以,只不过扩大了同步的范围,效率更低
            act.withdraw(money);
        }*/
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName()+"对"+act.getActno()+"取款成功,余额"+act.getBanlance());
    }
}
class Account2{
    //账号
    private String actno;
    //余额
    private double banlance;
    public Account2(){    }
    public Account2(String actno,double banlance){
        this.actno = actno;
        this.banlance = banlance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBanlance() {
        return banlance;
    }

    public void setBanlance(double banlance) {
        this.banlance = banlance;
    }
    //取款的方法
    //synchronized也可以出现在实例方法中:
    //但是出现在这里,一定锁的是this,不能是其他的对象了,所以这种方式不灵活,不常用
    //public synchronized void withdraw(double money){
    public void withdraw(double money){
        //t1和t2 并发这个方法,(t1和t2是两个栈,两个栈操作堆中的同一个对象)
        //解决线程安全问题:
        /*以下的这几行代码必须是线程排队的,不能并发。
        * 一个线程把这里的全部代码执行完毕之后,另一个线程才能执行。*/
        //synchronized ("adsfs"){   //---->字符串也是可以的,只不过t1,t2,t3,t4都可以共享了
        synchronized (this){        //--->只有t1,t2才可以共享了
            //取款之前的余额
            double befor = this.getBanlance();
            //取款之后的余额
            double after = befor-money;
            //在这里模拟一下网络延迟,1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //更新余额
            this.setBanlance(after);
        }

    }
}

##### 执行结果:

`t2对act01取款成功,余额5000.0`
`t1对act01取款成功,余额0.0`

1.6  线程私有数据和线程共享数据对多线程并行有什么影响吗?

  • 如果线程中的数据是线程私有的,那么就不会出现线程安全问题。因为线程A数据只能被线程A操作。
  • 如果数据是线程间共享的,那么就可能出现线程安全问题。因为数据可以被多个线程同时操作。

二、内存结构与线程安全

  • 栈,也叫线程栈。每创建一个线程,JVM就会为这个线程创建线程栈,线程栈中 存储这个线程私有的数据。
  • 堆:堆中的数据是所有线程都可以访问的,因此堆中的数据称为线程共享的数
所有线程的共享数据都在堆中

三、具有天然线程安全的情况

有些情况下线程天然具有线程安全性,比如:
  • 栈空间中的数据
  • 无状态的对象
  • 常量
  • ThreadLocal
3.1 从存储空间看线程安全
        Java内存分为堆、栈(其实就是线程栈)、元空间。
栈空间
        栈空间每个线程独立,线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性。
堆空间
        多个线程同时操作实例变量可能存在线程安全问题。
元空间
        元空间也是多个线程可以共享的,因此访问非堆空间中的静态变量也可能存在线程安全问题。

​​​​​​​3.2 从对象状态看线程安全

  • 无状态对象是指没有属性的对象,即对象中没有数据。
  • 有状态对象是指有属性的对象,即对象中有数据。
  • 线程安全问题的前提是,多个线程操作共享的数据。实现线程安全的一种办法就 是避免在多个线程之间共享数据,使用无状态对象由于没有数据,所以天然就是 线程安全的。

3.3 从不可变对象看线程安全
不可变对象是指一经创建它的状态就保持不变的对象 。 例如 String 类的字面量就是不可变对象,当为String 字面量重新赋值时,会产生新的字面量,而不是修改了原有的字面量的值。不可变对象具有固有的线程安全性。
一个不可变对象需要满足以下条件 :
  • 类本身使用 final 修饰,防止通过创建子类来改变它的定义
  • 所有的字段都是 final 修饰的,final 字段在创建对象时必须显示初始化,不能被修改
  • 如果字段引用了其他状态可变的对象(集合、数组),则这些字段必须是 final修饰的

3.4 ThreadLocal类看线程安全
存储在 ThreadLocal 类中的数据是每个线程私有的数据,因此存储在
ThreadLocal 中的数据是线程安全的。

 

​​​​​​​如果你觉得博主的文章的不错或者对你有帮助,可以点一个免费的关注支持一下博主,你的鼓励将是我创作的最大动力!!  

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

-今非昔比°

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值