除了Synchronized关键字还有什么可以保证线程安全?

除了Synchronized关键字还有什么可以保证线程安全?

    日常使用Java开发时,多线程开发,一般就用Synchronized保证线程安全,防止并发出现的错误和异常,那么
除了Synchronized关键字还有什么可以保证线程安全吗?

什么是线程安全?

    在了解什么方法可以保证线程安全之前,我们先定义什么是线程安全。Wikipedia是如此定义的:

线程安全是程式设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的
共享变量,使程序功能正确完成。

按我的理解,在多线程环境下程序功能正确完成,说白了就是无论单线程或者多线程环境下,这个程序运行要完全
实现了程序设计时功能的需求,没有BUG。所以为了没有BUG,我们必须保证单或者多线程环境下代码运行的线程
安全。

不考虑线程安全问题行不行?

    有人会说,不要多线程行不行,我只写单线程代码不就可以了,单线程总不会有线程安全问题了吧。假如只有单
线程,那么访问一个网页就会出现类似排队买票的场景,一个请求返回了才能继续处理下一个请求,效率非常之低。
利用多线程,可以充分利用多核CPU资源,提高性能,特别是存在计算密集型、IO密集型任务。
    那么开多进程(process),一个进程对应一个线程行不行?这种情况下,基于Java语言的程序运行,每次访问一
个请求就需要开启一个新的JVM,再来一次class加载等等,包括JVM的优化、缓存、预热之类的优化优势全部没了。
    那么只开一个线程,一个线程对应多个协程(coroutine)或者微线程(fiber)行不行? 首先Java语言本身没有提供原
生的协程实现,其次即便实现了了,只用单线程还是无法充分利用多核CPU资源,性能也会达到瓶颈。
    说到底为了性能,使用多线程有益,代价就是对于一个共享并且状态可变的资源而言,只要存在竞争条件(race
condition),线程安全问题就必须纳入开发代码时的考虑范围。

什么情况下才不线程安全?

    为了避免线程不安全,我们必须先了解什么情况下才会出现线程不安全的问题。一个变量只在线程内使用,这种
情况下需要考虑线程安全问题吗?不需要,只有对需要在多个线程之间共享的变量的访问操作,我们才需要考虑线程
安全问题。所有线程共享的变量都需要考虑吗?不是,如果一个变量初始化后就不再变化,即变量为不可变状态,我
们也无需考虑。所以,只有线程之间存在对共享可变的对象访问操作时,才会出现线程安全问题。

如何保证线程安全?

不共享

    正如前文所说,只有需要操作共享的可变对象时会出现线程安全问题,那么只要不共享对象不就就可以保证线程
安全了吗。方法在于要确保对象本身或者其引用不会被共享出去,例如局部变量肯定是不会暴露出去,也可以利用
ThreadLocal保存并传递对象。

不可变(immutable)

    如果必须共享,那么对象为不可变状态,也能保证线程安全。什么情况下对象才是不可变的呢?对象构造完后所
有属性都不会再发生改变就是不可变。以A类为例,

public class A {
    private int fieldA;
    private int[] fieldB;
    
    public A(C c) {
        c.setD(new D());
        // 初始化
    }

    public void setFieldA(int a) {
        fieldA = a;
    }

    public int[] getFieldB() {
        return fieldB;
    }

    private void updateA() {
        //修改内部状态
    }

    class D {
        public void updateD() {
            updateA();
        }
    }
}

首先需要将属性变为final,保证构造函数初始化后其他属性不会发生变动,防止属性状态会被更改。

public class A {
    private final int fieldA;
    private final int[] fieldB;
    
    public A(C c) {
        c.setD(new D());
        // 初始化
    }

    public int[] getFieldB() {
        return fieldB;
    }

    private void updateA() {
        //修改内部状态
    }

    class D {
        public void updateD() {
            updateA();
        }
    }
}

其次防止内部的可变对象引用暴露出去。

public class A {
    // 置为final状态
    private final int fieldA;
    private final int[] fieldB;
    
    public A(C c) {
        c.setD(new D());
        // 初始化
    }

    public int[] getFieldB() {
        if (fieldB == null) {
            return null;
        }
        // 防止引用暴露,外部可以修改fieldB状态
        return Arrays.copyOf(fieldB, fieldB.length);
    }

    private void updateA() {
        //修改内部状态
    }

    class D {
        public void updateD() {
            updateA();
        }
    }
}

最后构造函数时要防止A类的this逃逸(this escape),防止构造函数还没有完成初始化时,其他线程可以利用逃逸的
this调用内部方法。

public class A {
    // 置为final状态
    private final int fieldA;
    private final int[] fieldB;
    
    public A(C c) {
        // updateD方法可能在c被其他线程调用,间接调用updateA方法,修改了Class A实例的状态
        c.setD(new D());
        // 初始化
    }

    public int[] getFieldB() {
        if (fieldB == null) {
            return null;
        }
        // 防止引用暴露,外部可以修改fieldB状态
        return Arrays.copyOf(fieldB, fieldB.length);
    }

    private void updateA() {
        //修改内部状态
    }

    class D {
        // 去除updateD方法
    }
}

单线程修改

    对象必须共享且是可变的,如果只会被其中一个线程修改,其他线程只是读取,那么只需要对该对象其属性状态添加volatile
关键字修饰,保证内存可见性,其他线程能读取到对象的最新状态,这样也可以保证线程安全。

将对象属性修改为线程安全类型

public class A {
    private Map<Integer, B> fieldB;

    public A() {
        fieldB = new HashMap<>();
    }
    
    public B getB(Integer key) {
        return fieldB.get(key);
    }

    public B addB(Integer key, B value) {
        B b = fieldB.get(key);
        if (b == null) {
            fieldB.put(key, value);
        }
        return b;
    }
}

将线程安全委托给属性对象,由属性对象保证线程安全

public class A {
    private final Map<Integer, B> fieldB;

    public A() {
        fieldB = new ConcurrentHashMap<>();
    }
    
    public B getB(Integer key) {
        return fieldB.get(key);
    }

    public B addB(Integer key, B value) {
        return fieldB.putIfAbsent(key, value);
    }
}

使用synchronized关键字

    对于共享的可变对象,要求所有属性状态修改读取都用synchronized修饰

public class A {
    private B fieldB;
    
    public synchronized B getB() {
        return fieldB;
    }

    public synchronized void updateB() {
        // 其他操作
        fieldB.update();
        // 其他操作
    }
}

可以优化使用内部的私有对象作为锁对象,避免锁住整个方法

public class A {
    private B fieldB;
    private final Object lock = new Object();
    
    public synchronized B getB() {
        return fieldB;
    }

    public void updateB() {
        // 其他操作
        synchronized (lock) {
            fieldB.update();
        }
        // 其他操作
    }
}

猜你喜欢

转载自www.cnblogs.com/yeyu456/p/12016324.html
今日推荐