java中的线程安全

来源:互联网 发布:海岛奇兵兵种升级数据 编辑:程序博客网 时间:2024/06/02 14:43

先来看一下通用的线程安全定义以及对应的例子:


线程安全就是在多线程访问时采用了加锁机制,当一个线程访问某个对象时,对这个对象进行保护,其他线程不能访问该对象,直到该线程读取完,其他线程才可使用,以保证对象不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。


比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。


在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;


而如果是在多线程情况下,比如有两个线程,线程 A 先将元素1存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B向此 ArrayList 添加元素2,因为此时 Size 仍然等于 0 (注意,我们假设的是添加一个元素是要两个步骤,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值,结果Size等于2。


最后,我们来看看 ArrayList 的情况,期望的元素应该有2个,而实际只有一个元素,造成丢失元素,而且Size 等于 2。这就是“线程不安全”了。


多说无益,我们来用一个实际可运行的例子来看一下当线程不安全发生时会导致什么结果:


首先构造一个对象,然后把它分别传递给两个不同的线程,其中一个线程对对象的变量执行1000次自增,另一个线程对对象的变量执行1000次自减,预期是两个线程执行完成后对象的变量值保持不变,下面先贴上代码:


/** * 类的描述 * @author   李庆 * @version  0.0.1 */package thread_test;/** * @author 李庆 * */public class ThreadTest {    public static void main(String[] args) throws InterruptedException {        Count count = new Count();        Thread t1 = new Thread(new Thread1(count), "add");        Thread t2 = new Thread(new Thread2(count), "delete");        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count.getCount());    }}class Count {    int count = 1000;    public void add(){        this.count++;    }    public void delete(){        this.count--;    }    public int getCount(){        return this.count;    }}class Thread1 implements Runnable{    Count count;        public Thread1(Count count){        this.count = count;    }        @Override    public void run() {        for(int i=0;i<1000;i++){            this.count.add();        }    }}class Thread2 implements Runnable{    Count count;        public Thread2(Count count){        this.count = count;    }        @Override    public void run() {        for(int i=0;i<1000;i++){            this.count.delete();        }    }}

接下来是笔者3次的运行结果:


第一次:




第二次:




第三次:




每一次的结果都不相同,且每次的结果都与之前我们的预期不符,这就是线程不安全导致的结果。


那么,有办法可以解决这个问题吗?当然是有的,就是加锁,在java中,就是用synchronized关键字包住可能导致线程不安全的部分代码。


注意:加锁的位置是非常重要的,如果加锁的范围太大,则会影响整个引用的运行效率,而加锁的范围太小,则又会导致线程不安全风险的上升,所以对代码进行恰当的加锁,是一个合格的程序员必须掌握的。(如上面的代码,为了解决线程不安全,可以对整个count对象加锁,但此时如果我们再新起一个线程,想要调用它的getCount方法来获取内部变量的当前值,会因为加锁而无法访问,这显然是不合适的,读操作并不会导致线程不安全)


加锁后的代码(对Count类的add和delete两个方法进行加锁):


/** * 类的描述 * @author   李庆 * @version  0.0.1 */package thread_test;/** * @author 李庆 * */public class ThreadTest {    public static void main(String[] args) throws InterruptedException {        Count count = new Count();        Thread t1 = new Thread(new Thread1(count), "add");        Thread t2 = new Thread(new Thread2(count), "delete");        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count.getCount());    }}class Count {    int count = 1000;    public synchronized void add(){        this.count++;    }    public synchronized void delete(){        this.count--;    }    public int getCount(){        return this.count;    }}class Thread1 implements Runnable{    Count count;        public Thread1(Count count){        this.count = count;    }        @Override    public void run() {        for(int i=0;i<1000;i++){            this.count.add();        }    }}class Thread2 implements Runnable{    Count count;        public Thread2(Count count){        this.count = count;    }        @Override    public void run() {        for(int i=0;i<1000;i++){            this.count.delete();        }    }}

变更部分如下:




此时无论运行多少次,结果都是一样的:




加锁不仅仅可以对函数加锁,也可以对变量、对象、类进行加锁,具体视项目的业务需要而定。


另外要注意的是,加锁是代码粒度的行为,在开发过程中很容易漏掉加锁(或是加锁范围存在问题),所以我们在模块设计时也需要注意这一点,在多线程的开发中尽量避免让多个线程对同一个对象执行非读操作(增删改)。如果业务场景存在不可避免的多线程操作同一个对象的场景,则可以考虑像数据库那样增加一个commit步骤,用于对操作同一个对象的变更申请进行安全检查,保障数据安全。

0 0
原创粉丝点击