并发编程学习——2 线程的安全性

线程安全性

所谓的线程安全性核心在于对访问状态操作进行管理,特别是对共享和可变的状态的访问。一个对象有时候不仅仅保存在对象本身,有时候还保存在许多和它关联的对象中,任何一个地方发现变更,都可能影响其他地方数据。

什么是线程安全

对象可以在多个线程之间调用,同时线程之间不会出现错误的交互。

不安全的线程出现的问题

竞态条件

并发编程中有一个情况叫做“竞态条件”,描述的是当两个线程对同一个资源进行竞争的时候因为不同的访问顺序导致出现不可预测的结果。

下面是一个很明显的竟态条件

    private static UnsafeSequence getObject () {
        if (obj == null) {
            obj = new UnsafeSequence();
        }
        return obj;
    }

这种先检查然后再执行某些操作的逻辑,当检查-执行操作不是一个原子操作的情况下,很可能出现重复创建的问题。

失效的数据

/**
 * 非安全的数值生成器
 * @author daify
 * @date 2019-05-30
 */
public class UnsafeSequence {

    public int value;

    public int getNext() {
        return value++;
    }
}

public class MainTest {

    public static void main(String[] args) {
        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 5; i++) {
            Runnable run = new Runnable() {
                public void run() {
                    for (int j = 0; j < 20; j++) {
                        int next = UnsafeSequence.getNext();
                        System.out.println("此时值为:" + next);
                    }
                }
            };
            Thread thread = new Thread(run);
            threads.add(thread);
        }

        for (int i = 0; i < 5; i++) {
            threads.get(i).start();
        }
    }
}

其打印结果可能是这样的

此时值为:92
此时值为:93
此时值为:94
此时值为:95
此时值为:96
此时值为:97
此时值为:98

而理论上5个线程每个执行20次最终输出应该是99,此时说明当读线程查看变量的时候,可能会得到一个已经失效的值。

最低安全性 : 当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但是至少这个值是由之前的某个线程设置的值,而不是一个随机值,这种安全性保证也被称为最低安全性。

大多数变量适用于最低安全性,但是存在例外,非volatile类型的64位数值变量double和long。java内存模型要求,变量的读取操作和写入操作都必须是原子操作。但是对于非volatile类型的long和double变量,JVM允许将64位的读操作或者写操作分解为两个32位的操作。 如果对变量的读操作和写操作在不同线程中执行,很可能会读取到一个值的高32位和另一个值的低32位。 所以在多线程中使用共享且可变的long或者double灯类型的变量也是不安全的,除非使用关键词volatile来声明他们,或者使用锁保护起来。

逸出

发布 : 当我们让一个对象能够在当前作用域之外进行使用的时候。比如我们将对象的引用保存到其他代码可以进行访问的地方,此时我们发布了这个对象

当一个对象在不该发布的地方或者不该发布的时机被发布了,此时这个对象逸出了。

在构造函数启动一个线程。当对象在其构造函数中创建一个线程时,无论显式还是隐式创建。this都会被新创建的线程共享,在未完全构造之前,新的线程就能看见它,这会导致this在构造过程中逸出

public class NoSafeListener {

    private int index;

    private String name;

    public NoSafeListener() throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                System.out.println("线程启动");
                Class<? extends Runnable> aClass = this.getClass();
            }
        });

        thread.start();
        Thread.sleep(1000);
        System.out.println("开始赋值");
        this.index = 5;

        this.name = "test";
    }

    public static void main(String[] args) throws InterruptedException {
        NoSafeListener safeListener = new NoSafeListener();
    }
}

image

此时可以看到,在线程中我们可以意外的访问的还在进行初始化的对象。这个时候假如我们尝试通过this去操作对象的时候,就可能发生不可预测的问题。

保证线程安全

使用锁保护

平时我们说起线程安全,大多数时间我们第一时间想到的就是加锁,无论内置锁还是Lock的确是一个保护线程安全的好办法。

内置锁

内置锁就是我们常说的synchronized,由其保护的内容我们叫做同步代码块。

public Thread getDefThread () {
        return new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取 锁,离开代码块后就释放锁
                synchronized (lock) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    value = value + 1;
                    System.out.println(value);
                }
            }
        });
    }

这是一个很简单但的加锁保护线程安全的方式。

Lock锁

在java 1.5 之后官方提供了Lock接口以及配套的实现类,进一步丰富了锁对线程安全的保护

Lock锁相关实现类我们使用比较多的有。

  • ReentrantLock
  • ReentrantReadWriteLock
    /**
     * 锁的简单用法
     */
    public Thread getDefThread () {
        return new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁
                lock.lock();
                try {
                    Thread.sleep(1000);
                    System.out.println("do some things");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        });
    }

一个简单的锁的使用方式

线程封闭

访问共享的可变数据时,通常需要使用同步。其中一种避免使用同步的方法就是不贡献数据。当同一时间只有一个线程能够访问的数据,总是安全的。

线程封闭最常见的应用就是JDBC。 JDBC规范并不要求Connection对象时必须是线程安全的。但是线程从线程池获得一个Connection对象,并且使用该对象处理请求,这个过程都是单个线程采用同步的方式处理,在Connection对象返回之前,连接池不会再将它分配给其他线程。

安全发布

假如我们希望一些数据必须在多条线程之间共享,除了保证修改过程中的安全,还必须保证数据不在错误的时间被发布。就像上面的例子,在构造方法中开启一个新的线程从而导致一个尚未完成初始化的类在这个线程中被发布。

安全共享数据

  • 线程封闭:线程封闭的对象只能由一个线程拥有。
  • 只读共享:可以由多个线程并发访问,但是都不能修改它。
  • 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口访问而不需要进一步同步。
  • 保护对象:被保护的对象只能通过持有特定的锁访问。

最低安全保证

Volatile

弱同步机制,用来确保将变量的更新操作通知到其他线程,把变量声明为volatile类型后,编译器与运行时都会主要到这个变量是共享的。因此不会讲变量上的操作和其他内存操作一起重排。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方。因此读取volatile类型的变量时总会返回最新写入的值。

Volatile字段的作用就是,当对一个Volatile变量进行写操作的时候,java会把该数据的值强制刷新到主内存,同时这个数据会更新到其他线程中,这样其他线程就可以第一时间得到最新的值。
但是有一点需要注意的是,因为Volatile字段并不等同于加锁,所以当多个线程尝试去修改这个值的时候,依旧会发生错误,具体要取决于业务逻辑。获取的Volatile字段的值只是代表这个字段最新的值(或者是说曾经某个时间最新的值)

这一点可以举个简单的例子,我们写计数器的时候一般是获得最新数据然后+1。我们用锁的时候一般会将获得值和设置值的步骤进行加锁,而Volatile只能保证当前线程获得最新的值,而不能保证在你获得值和设置值之间这个值是否被人编辑。

final

用于构造不可变性对象。final类型的域是不能修改的(然而final引用的对象是可以被替换的)。在JAVA内存模型中final能确保初始化过程中的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

总结

这篇主要说了线程安全出现的问题,以及如何保证线程安全。现在的实际开发中,锁是我们最得力的工具。但是当你使用锁的时候也意味着你要面对因为错误使用锁而带来的风险以及锁带来的性能消耗。这个我会在后面再来详细说明。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大·风

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

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

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

打赏作者

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

抵扣说明:

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

余额充值