Basic Of Concurrency(四:线程安全)

共享资源可以被多个线程访问且不会造成竟态条件即为线程安全的代码。因此分清哪些资源为共享资源,对于区分代码是否为线程安全相当重要。html

线程安全与共享资源

局部变量

基础数据类型

局部变量基础数据类型仅会存储在线程栈中,供本线程使用,因此局部变量基础数据类型是线程安全的。java

public void safeMethod() {
	int threadSafeInt = 10;
    System.out.println(++threadSafeInt);
}
复制代码
对象引用

虽然引用类型自己不会被共享,但引用类型指向的对象是存储在共享堆中的,所以须要判断堆中的对象是否会逃逸出本线程,被其余线程访问到,若该对象仅会在本线程被访问,那么它是线程安全的,若它可以被其余线程所访问那么它将不是线程安全的。数据库

public void method0(){
    LocalObject localObject = new LocalObject();
    localObject.add(2);    
	method1(localObject);
}

public void method1(LocalObject localObject){
	localObject.add(5);
}
复制代码

method0即便被多个线程调用也不会产生竞态条件,由于局部对象仅会在线程内部建立和访问,即便在method0将该局部对象传递给本对象的其余方法或其余对象的方法也是如此。数组

对象成员

若多个线程访问多一个对象的成员,将会产生竟态条件。安全

public class UnSafeObject {

    private int count = 0;

    public void add(int val) {
        int result = this.count + val;
        this.count = result;
    }

    public int getCount() {
        return this.count;
    }

    public static void main(String[] args) {
        final UnSafeObject unSafeObject = new UnSafeObject();
        Runnable runnable = () -> {
            unSafeObject.add(2);
        };
        IntStream.range(1, 3)
                .forEach(i -> new Thread(runnable).start());
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

该实例中,两个线程并发访问了UnSafeObject对象的成员方法add()。所以该成员变量是线程不安全的。bash

对以上实例稍加修改:多线程

public static void main(String[] args) {
	Runnable runnable = () -> {
        final UnSafeObject unSafeObject = new UnSafeObject();
    	unSafeObject.add(2);
    };
	IntStream.range(1, 3)
    	.forEach(i -> new Thread(runnable).start());
	try {
    	Thread.sleep(2000L);
    } catch (InterruptedException e) {
    e.printStackTrace();
	}
}
复制代码

让UnSafeObject分别在不一样的线程中建立新的实例,这样就会是线程安全的。并发

事实证实,只要措施得当就能让自己不安全的资源变成安全的。函数

线程控制逃逸规则

若是一个资源的建立使用和销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则这个资源永远是线程安全的。post

资源能够是对象,数组,文件,数据库,套接字等。Java对象的销毁能够指没有任何引用指向该对象。

就算一个对象自己是线程安全的,可是该对象中包含其余不安全资源(文件,数据库链接),则整个对象都再也不是线程安全的。

如多个线程建立的数据库链接指向同一个数据库,则有可能多个线程对同一条记录做出更新或插入,此时该数据库资源是线程不安全的。

运行轨迹以下:

线程1: 检查记录x是否存在,记录x不存在
线程2: 检查记录x是否存在,记录x不存在
线程1: 插入记录x
线程2: 插入记录x
复制代码

最后数据库会产生两条同样的记录,不符合预期。

线程安全与不可变性

之因此会产生竞态条件是由于一到多个线程同时访问了相同的对象,且至少有一个线程进行了写操做。多个线程访问不会变化的对象并不会产生线程安全问题。所以能够利用对象的不可变性来规避线程安全问题。不可变性仅能保证对象在多个线程间传递是安全的。但凡是有线程对传递的对象进行操做,都会产生新的对象。

done like this:

public class ImmutableExample {
    private int val;

    public ImmutableExample(int val) {
        this.val = val;
    }

    public int getVal() {
        return this.val;
    }

    public ImmutableExample add(int val) {
        return new ImmutableExample(this.val + val);
    }
}
复制代码

不可变对象仅能经过构造函数注入数值,没有更新入口,当须要更新数据时,仅能经过构造新对象来实现。

不可变类型的引用

public class Calculator {
    private ImmutableExample immutableExample;

    public Calculator(ImmutableExample immutableExample) {
        this.immutableExample = immutableExample;
    }

    public ImmutableExample getImmutableExample() {
        return immutableExample;
    }

    public void add(ImmutableExample immutableExample) {
        this.immutableExample = this.immutableExample.add(immutableExample.getVal());
    }
}
复制代码

实例中Calculator内部引用了一个不可变对象,虽然引用的对象是不可变的。但引用自己倒是可变的,所以在多线程环境下,整个Calculator仍然是线程不安全的。使用不可变类型来规避线程安全问题须要牢记这点。

总结

线程安全与共享资源息息相关,因此肯定哪些资源是线程不安全的,对于区分代码是不是线程安全的相当重要。

虽然不可变类型能够规避线程安全问题,可是要当心不可变类型的引用,引用不是线程安全的,仍然会致使整个代码是线程不安全的。

该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 竞态条件和临界区
下一篇: Java内存模型

相关文章
相关标签/搜索