线程安全

线程安全

   定义:多个线程同时运行这段代码。如果每次运行结果和单线程运行结果是一样的,而且其他变量的值也和预期的是一样的,就是线程安全的。或者说,一个类或者程序所提供的接口对于线程来说是原子操作,多个线程间的切换不会导致执行结果存在二义性,也就是不用考虑同步的问题。线程安全策略就是多线程访问时,采用加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可以使用。否则,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

  若多线程中,每个线程对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

 

示例1:

   比如一个ArrayList类,在添加一个元素的时候,它会有两步来完成:
   在Items[size]的位置上存放此元素,增加size的值。
   如果是单线程运行的情况下,size =0 ,添加一个元素后,此元素在位置0,而且size = 1;
   而如果是在多线程情况下,比如有两个主线程,线程A先将元素存放在位置0,但是此时CPU调度栈线程A暂停,线程B得到运行的机会。线程B也向此ArrayList添加元素,因为此时size仍等0,所以线程B也将元素存放在位置0,然后线程A和线程B都继续运行,都增加size值,那好,ArrayList的情况,元素实际上只有一个,存放在位置0,而size却等于2.这就是线程不安全了。

 

示例2:

public class TraditionalThreadSynchronized {
    public static void main(String[] args) {
        final Outputter outputter = new Outputter();
        // 运行两个线程分别输出名字zhangsan和lisi
        new Thread() {
            public void run() {
                outputter.output("zhangsan");
            }
        }.start();      
        new Thread() {
            public void run() {
                outputter.output("lisi");
            }
        }.start();
    }
}
class Outputter {
    public void output(String name) {
        // TODO 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符
        for(int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
            // Thread.sleep(10);
        }
    }
}

// zhlainsigsan

显然输出的字符串被打乱了,我们期望输出的是zhangsanlisi,这就是线程同步问题,我们希望output方法被一个线程完整的执行完之后再切换到下一个线程,可以使用synchronized保证一段代码在多线程执行时互斥,使用sycnhronized修饰的部分可以看成是一个原子操作,具体用法有两种:

   方法1: 使用synchronized将需要互斥的代码包含起来,并上一把锁。

{
    synchronized (this) {
        for(int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
    }
}

 

  方法2:将synchronized加在需要互斥的方法上。

public synchronized void output(String name) {
    // TODO 线程输出方法
    for(int i = 0; i < name.length(); i++) {
        System.out.print(name.charAt(i));
    }
}

 

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒notify后,才会进入到就绪队列,等待CPU的调度,反之,当一个线程被wait后,就会进入阻塞队列,等待下一次会唤醒,这个涉及到线程间的通信。

 

一个线程互斥代码过程如下:

1、获得同步锁

2、清空工作内存

3、从主内存拷贝对象副本到工作内存

4、执行代码(计算或者输出等)

5、 刷新主内存数据

6、释放同步锁

 

 

如何保证线程安全呢

 保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案

1、互斥同步

     互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证数据在同一时刻制备一个线程使用。(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。

  互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题你,因此,这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施,那就肯定会出现问题, 无论共享数据是否真的会出现竞争,它都要进行加锁。

 2、非阻塞同步

     随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地讲,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。

     非阻塞的实现CAS:CAS指令需要有三个操作数,分别是内存地址V、旧的预期值A和新值B。CAS指令执行时,CAS指令执行时,当且仅当V处的值符合预期值A时,处理器用B更新V处的值,否则它就不执行更新。
     CAS缺点,可能会遇到ABA问题,因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有变化,但是实际上,它却变化了。ABA问题的解决思路就是使用版本号,在变量前面追加版本号,每次变量更新的时候把版本号更新。

3、  无需同步方案

      要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性手段,如果一个方法本来就不涉及共享数据,那么它自然就无需任何同步操作去保证正确性,也就是有些代码天生就是线程安全的。

      可重入代码:所谓可重入代码,也称为纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入。可重入的代码的特点是不依赖存储在堆上的数据和公用的系统资源,用到的状态量都是由参数中传入,不调用非可重入的方法等。

posted on 2021-05-10 19:27  zhishiyv  阅读(64)  评论(0编辑  收藏  举报

导航