实现多线程安全的3种方式


1、先来了解一下:为什么多线程并发是不安全的?

  在操作系统中,线程是不拥有资源的,进程是拥有资源的。而线程是由进程创建的,一个进程可以创建多个线程,这些线程共享着进程中的资源。所以,当线程一起并发运行时,同时对一个数据进行修改,就可能会造成数据的不一致性,看下面的例子:

    假设一个简单的int字段被定义和初始化:
    int counter = 0;
    该counter字段在两个线程A和B之间共享。假设线程A、线程B同时对counter进行计算,递增运算:
    counter ++;
    那么计算的结果应该是 2 。但是真实的结果却是 1 ,这是因为:线程A得到的运算结果是1,线程B的运算结果也是1,当线程A将结果写回到内存中的 count 后,线程B也将结果写回到内存中去,这就会把线程A的计算结果给覆盖了。

上面仅仅是一种简单的情况,还有更复杂的情况,本文不深入去了解。
2、多线程并发不安全的原因已经知道,那么针对这个种情况,java中有两种解决思路:

    给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。
    让线程也拥有资源,不用去共享进程中的资源。

3、基于上面的两种思路,下面便是3种实施方案:

1. 多实例、或者是多副本(ThreadLocal):对应着思路2,ThreadLocal可以为每个线程的维护一个私有的本地变量,可参考java线程副本–ThreadLocal;
2. 使用锁机制 synchronize、lock方式:为资源加锁,可参考我写的一系列文章;
3. 使用 java.util.concurrent 下面的类库:有JDK提供的线程安全的集合类

可能说的还不太清楚,更新一下,以及给出一个线程安全模拟的例子:
上面说了,多线程之所以不安全,是因为共享着资源(如果没有资源变量共享,那么多线程一定是安全的)。比如,存在共享变量a,线程A在使用变量a时进行计算时,因为时间片的到来,导致线程不得不由运行中状态进入就绪状态,暂停运行。等该线程A又重新被调度,得以继续执行时,得到了最终的结果。但是此时内存中的变量a可能已经被其他线程改变了,但线程A的结果再写回到内存中时,就会覆盖了其他线程的计算结果,这就是多线程不安全的原理。

下面给出线程安全模拟的例子的思路:1、让三个线程瞬间同时并发(不得不用到锁,wait/notify机制,如果不懂,只要知道这是 等待/通知 便可,下面有注释);2、模拟3个线程共享着一个变量,使用变量进行计算的过程 与 将计算结果分成两次执行。
下面是没有进行同步,也就是线程不安全的情况:

CountMoney countMoney = new CountMoney();
String obj="";
    //创建启动3个线程
    for(int i=0;i<3;i++){
        Thread t1 = new Thread(){
            @Override
            public void run() {
            //用锁来让线程第一次运行时,进入等待状态,直到被通知来了才继续往下运行
                synchronized (obj) {
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //通知来了后,执行addMoney的方法
                countMoney .addMoney(1);
            }
        };
        //线程启动
        t1.start();
        //确保创建的线程的优先级一样
        t1.setPriority(Thread.NORM_PRIORITY);
    }
    try {
        //确保创建的3个线程已经运行了一次,进入等待状态
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    synchronized (obj) {
        //瞬间唤醒3个线程
        obj.notifyAll();
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

CountMoney 类

public class CountMoney {
    //线程共享着CountMoney对象中的money变量
    volatile long money = 0;

    public long getMoney() {
        return money;
    }

    public void setMoney(long money) {
        this.money = money;
    }

    public  void addMoney(long a) {//synchronized
        //1、取得变量money的值,计算出结果
        a = getMoney() + a;
        //线程完成第一步后,让出CPU;目的是:模拟1、2两行代码是分成两次执行的,不是一次性执行的
        Thread.yield();
        //2、将计算结果写回到变量money中
        setMoney(a);
        System.out.println("线程"+Thread.currentThread().getName()+"的计算结果"+getMoney());
    }

}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

运行结果:

    线程Thread-2的计算结果1
    线程Thread-1的计算结果1
    线程Thread-0的计算结果1

我们再来看一下,加锁后的 addMoney()方法,也就是进行同步后:

public synchronized void addMoney(long a) {//加了synchronized 修饰
        //1、取得变量money的值,计算出结果
        a = getMoney() + a;
        //线程完成第一步后,让出CPU;目的是:模拟1、2两行代码是分成两次执行的,不是一次性执行的
        Thread.yield();
        //2、将计算结果写回到变量money中
        setMoney(a);
        System.out.println("线程"+Thread.currentThread().getName()+"的计算结果"+getMoney());
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9

运行结果:

    线程Thread-2的计算结果1
    线程Thread-1的计算结果2
    线程Thread-0的计算结果3
---------------------
作者:jinggod
来源:CSDN
原文:https://blog.csdn.net/jinggod/article/details/78275763
版权声明:本文为博主原创文章,转载请附上博文链接!


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM