线程安全问题

1 什么是线程安全问题

多个线程同时访问同一个资源(变量、对象、文件等)时就可能出现线程安全问题。

多个线程执行时是抢占式的,一个线程在执行一个操作时(调用方法,更新变量),可能会被其他线程打断,导致操作没有完全完成,可能会造成数据出现不一致的情况。

2.线程安全问题出现的条件

多个线程
同一个时间执行
同一段指令或修改同一个变量

线程安全案例:银行转账

package com.hopu.thread;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BankDemo {

    //模拟100银行账号
    private  int [] accounts=new int[100];

    // Lock lock=new ReentrantLock();

    //初始化账号
    {
        for (int i = 0; i < accounts.length; i++) {
            accounts[i]=10000;
        }
    }

    //模拟转账
    public void  transfer(int from,int to,int money){
        if (accounts[from]<money){
            throw  new RuntimeException("余额不足!");
        }
//        lock.lock();
//        try {
            accounts[from]-=money;
            System.out.printf("从%d转出%d%n",from,money);
            accounts[to]+=money;
            System.out.printf("向%d转入%d%n",to,money);
            System.out.println("银行总额为:"+getTotal());
//        }finally {
//            lock.unlock();
//        }

    }

    //计算总额
    public  int getTotal(){
        int sum=0;
        for (int i = 0; i < accounts.length; i++) {
            sum+=accounts[i];
        }
        return  sum;
    }

    public static void main(String[] args) {
        BankDemo bank=new BankDemo();
        Random random=new Random();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                int from = random.nextInt(100);
                int to=random.nextInt(100);
                int money =random.nextInt(1000);
                bank.transfer(from,to,money);
            }).start();
        }

    }
}


执行效果:

从69转出587
向51转入587
银行总额为:997218
从81转出317
向53转入317
从47转出46
向34转入46
从18转出561
向56转入561
从55转出913
向53转入913
从8转出945
向80转入945
银行总额为:999055
银行总额为:998142
银行总额为:997581

Process finished with exit code 0

我们通过执行的结果发现问题,银行的总账的数据不是固定的,数据有误,为什么会造成这种现象。因为在多线程的案例中,某个线程A在执行转账操作的时候,就是转账转完了,收款方还没有接收到款, cpu就被其他的线程B抢占,导致之前的线程A还没有完成一整套的转账业务。数据就会产生错误。

线程安全的解决办法

可以通过上锁机制解决,主要是将资源上锁,让线程将业务完整的完成,不让其他线程介入。

几种不同上锁方法:

  • 同步方法
  • 同步代码块
  • 同步锁

同步方法

给方法添加synchronized关键字 作用是给整个方法上锁

当前线程调用方法后,方法上锁,其它线程无法执行,调用结束后,释放锁

    /**
     * 模拟转账
     */
    public synchronized void transfer(int from,int to,int money){
        if(accounts[from] < money){
            throw new RuntimeException("余额不足");
        }
        accounts[from] -= money;
        System.out.printf("从%d转出%d%n",from,money);
        accounts[to] += money;
        System.out.printf("向%d转入%d%n",to,money);
        System.out.println("银行总账是:" + getTotal());
    }

同步代码块

给一段代码上锁

synchronized(锁对象){
	代码
}

锁对象,可以对当前线程进行控制,如:wait等待、notify通知;

任何对象都可以作为锁,对象不能是局部变量

   //同步代码块
        synchronized (lock) {
            accounts[from] -= money;
            System.out.printf("从%d转出%d%n", from, money);
            accounts[to] += money;
            System.out.printf("向%d转入%d%n",to,money);
            System.out.println("银行总账是:" + getTotal());
        }

synchronized的基本的原理:
一旦代码被synchronized包含,JVM会启动监视器(monitor)对这段指令进行监控线程执行该段代码时,monitor会判断锁对象是否有其它线程持有,如果其它线程持有,当前线程就无法执行,等待锁释放
如果锁没有其它线程持有,当前线程就持有锁,执行代码

同步锁

在java.concurrent并发包中的

Lock接口

基本方法:

  • lock() 上锁
  • unlock() 释放锁

常见实现类

  • ReentrantLock 重入锁
  • WriteLock 写锁
  • ReadLock 读锁
  • ReadWriteLock 读写锁

使用方法:

  1. 定义同步锁对象(成员变量)
  2. 上锁
  3. 释放锁
//成员变量
Lock lock = new ReentrantLock();

//方法内部上锁
lock.lock();
try{
	代码...
}finally{
	//释放锁
	lock.unlock();
}

三种锁对比:

  • 粒度

    同步代码块/同步锁 < 同步方法

  • 编程简便

    同步方法 > 同步代码块 > 同步锁

  • 性能

    同步锁 > 同步代码块 > 同步方法

  • 功能性/灵活性

    同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法

悲观锁和乐观锁

悲观锁

认为线程的安全问题非常容易出现,会对代码上锁

前面所讲的锁机制都属于悲观锁

悲观锁的锁定和释放需要消耗比较多的资源,降低程序的性能

乐观锁

认为线程的安全问题不是非常常见的,不会对代码上锁

有两种实现方式:

  • 版本号机制

    利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。

  • CAS (Compare And Swap)比较和交换算法

    • 通过内存的偏移量获得数据的值
    • 计算出一个预计的值
    • 将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改

悲观锁和乐观锁对比

  • 悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
  • 乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
public class AtomicDemo {

    static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            new Thread(() ->{
                count++;
            }).start();
        }
        System.out.println(count);
    }
}

问题:多线程同时执行++操作,最后结果少了

分析:

count++ 分解为三个指令:

  1. 从内存中读取count的值
  2. 计算count+1的值
  3. 将计算结果赋值给count

这三个指令不是原子性的,A线程读取count值10,加1后得到11,准备赋值给count;B线程进入读取count也是10,加1得到11,赋值给count为11;切换会A线程,赋值count为11。

解决方案:

  1. 悲观锁,使用同步方法、同步块、同步锁

  2. 乐观锁

    使用原子整数

原子类

AtomicInteger类

原子整数,底层使用了CAS算法实现整数递增和递减操作

常用方法:

  • incrementAndGet 原子递增
  • decrementAndGet 原子递减
public class AtomicDemo {

    static int count = 0;

    static AtomicInteger integer = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread(() ->{
                count++;
                //递增
                integer.incrementAndGet();
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:"+count);
        System.out.println("atomic:"+integer.get());
    }
}

CAS算法存在的问题

  1. ABA问题

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,
但是这个是经过一系列操作之后的数值

  1. 如果预期值和实际值不一致处于循环等待状态,对CPU的消耗比较大

ThreadLocal

线程局部变量,会为每个线程单独变量副本,线程中的变量不会相互影响

以空间换时间
案例代码

public class ThreadLocalDemo {

    static int count = 0;

    //线程局部变量
    static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        //设置初始值
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                count++;
                local.set(local.get() + 1);
                System.out.println(Thread.currentThread().getName() + "--->" + local.get());
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
        System.out.println(local.get());
    }
}

执行结果会发现System.out.println(local.get()); 打印的结果为0。
这是因为主线程并没有参与运算。
在子线程种打印会发现5个子线程的System.out.println(local.get()); 都是1因为他们执行的都是各自的线程局部变量,互不影响。

作业

  1. 了解什么是ABA问题

所谓ABA问题,就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。比如线程1和线程2同时也从内存取出A,线程T1将值从A改为B,然后又从B改为A。线程T2看到的最终值还是A,经过与预估值的比较,二者相等,可以更新,此时尽管线程T2的CAS操作成功,但不代表就没有问题。

  1. 编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)

    分析问题原因,解决问题

package com.hopu.thread;

public class MySingleton {

    //2.定义静态对象
    private static MySingleton instance = null;

    //1。将构造方法定义为私有
    private MySingleton(){
    }

    //3.定义静态方法返回该对象
    public static MySingleton getInstance(){
        if(instance == null){
            instance = new MySingleton();
        }
        return instance;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                System.out.println(instance.getInstance().hashCode());

            }).start();
        }

    }
}

效果:

1274452530
1274452530
1274452530
1274452530
1274452530
1274452530
...
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值