JAVA并发编程实战——共享对象

  • Post author:
  • Post category:java

思维导图

在这里插入图片描述

1. 可见性

内存可见性:当一个线程修改使用的共享对象,其它线程能够看到改变

以下面demo为例:

public class NoVisibility {
    private static boolean ready;
    private static int number;
    private static class ReadyThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                //线程从执行态转为就绪态,放弃cpu
                Thread.yield();
            }
            System.out.println(number);
        }
    }
    public static void main(String[] args) {
        new ReadyThread().start();
        number = 10;
        ready = true;
    }
}

上述不是线程安全的。

  1. 正常输出10,主线程修改后,对ready线程可见。
  2. 可能输出0,如果进行了重排序可能先修改ready,而number还未修改就已经被打印出来。
  3. 处于一直循环状态,ready的值可能一直不可见。

因为涉及到数据的线程共享,我们应该使用下面的简单方法,避免上述各种结果:

只要数据被跨线程共享,就应该进行恰当的同步。

1. 1 过期数据

如下代码:

public class MutableInteger {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

如果多个线程都访问value,可能出现一个线程调用getValue,另一个调用setValue,可能导致看不到最新的值。

为了避免看到过期值,可以采取对getValue和setValue进行加锁synchronized。

对于long和double类型,可能出现另一种现象:得到一个凭空而来的值。

jvm允许将64位读写分为两个32位操作,如果此时读写不发生在同一线程,将会导致出现一个数的高32和另一个数低32位。

1.2 锁和可见性

如下图:
在这里插入图片描述
线程A和B都访问了共享变量x,如果不加锁可能B会读到过期的x。但是内部锁保障了可见性。

当线程B执行到同一个代码块,线程A对x的操作加锁对B来说都是可见的。
锁不仅可以同步和互斥,也是内存可见的,当然读取和修改线程是公共的相同的锁

1.3 Volatile变量

Java提供了一种同步的弱形式:volatile变量,来保证可见性。

  1. volatile修饰的变量,不会与其它操作进行重排序。
  2. volatile修饰的变量,不会被缓存在寄存器或者在多处理器系统中对其它处理器隐藏的地方。因此每次读取都是从内存中读取,保障了读取的都是最新的值。

volatile通常用于循环
在这里插入图片描述
java并发包中的原子类基本都是使用volatile修饰变量,改变状态使用下面的while进行:
在这里插入图片描述
通过while不停判断我们修改时,使用的是最新的可见的值。

加锁可以保障可见性和原子性,volatile变量只能保障可见性。

2. 发布和逸出

发布:发布一个对象使其可以在当前作用域之外的代码使用。
逸出:一个对象尚未准备好,就将它发布出去。

如下是一个逸出情况:

public class UnsafeStatus {
    private String[] states = new String[]{"new", "going", "end"};
    
    public String[] getStates() {return states;}
}

上述代码将导致任何线程都可以修改states变量。

2.1 安全构建实践

不要让this引用在构造期间逸出。

如下不安全示例,构造期间导致this逸出

public class ThisEscapeExample {

    public ThisEscapeExample( EventSource eventSource) {
        eventSource.registerListener(new EventListener() {
            public void onEvent(Event event) {
                doSomething();
            }
        });
    }

    public void doSomething() {
        System.out.println("do something");
    }

}

class EventSource {
    void registerListener(EventListener eventListener) {
        System.out.println("do something");
    }
}

上述由于还没构建完对象就使this逸出到了eventsource中。出现安全隐患

安全代码如下:

public class SafeExample {
    private final  EventListener listener;

    private SafeExample() {
        this.listener = new EventListener() {
            public void onEvent(Event event) {
                doSomething();
            }
        };
    }

    public static SafeExample newInstance(EventSourceO eventSourceO) {
        SafeExample safeExample = new SafeExample();
        eventSourceO.registerListener(safeExample.listener);
        return safeExample;
    }

    private void doSomething() {
        System.out.println("do something");
    }
}

class EventSourceO {
    void registerListener(EventListener eventListener) {
        System.out.println("do something");
    }
}

通过工厂方法和私有构造确保了对象构造完成后,发布到eventsource中,保障了并发安全性。

3. 线程封闭

一个避免同步的方式就是不共享数据,比如将数据限制在本线程中。

3.1 栈限制

对于栈是java线程私有的,当本地变量没有发布,其它线程就不能访问这个变量。

示例代码:

public class StackLimit {

    public void loadObject(Collection<Object> objects) {
        SortedSet<Object> innerVar;
        innerVar = new TreeSet<>();
        innerVar.addAll(objects) ;
        for (Object o : innerVar) {
            System.out.println("do something" + o.toString());
        }
    }
}

innerVar只有在执行线程本地变量表中存在引用,没有发布出去。从而保障了对象安全性。

3.2 ThreadLocal

ThreadLocal提供get和set访问器,为每个访问它的线程提供一个ThreadLocalMap的备份,这个map存储在线程Thread中,只有线程本身可以访问,故get返回的当先线程set的值。

比如我们可以使用下面demo获取一个与线程绑定的数据库连接:

public class ConnThreadLocal {
    private static  ThreadLocal<Connection> connHolder = new ThreadLocal<Connection>(){
        @SneakyThrows
        @Override
        protected Connection initialValue() {
                return DriverManager.getConnection("DB_URL");
        }
    };

    public static Connection getConnection() {
        return connHolder.get();
    }
}

4. 不可变性

不可变对象永远是线程安全的。

如下demo:

public class FinalObjectExample {
    private final Set<String> places = new HashSet<>();
    
    public FinalObjectExample() {
        places.add("Beijing");
        places.add("Shanghai");
        places.add("Xian");
    }
    
    public boolean isPlaces(String targetPlace) {
        return places.contains(targetPlace);
    }
}

该对象是一个不可变类。

只有满足以下状态, 一个对象才是不可变的:
 1. 它的状态在创建后不能更改。
 2. 所有域都是final;并且创建期间没有发生this逸出。

5 安全发布的模式

为了安全发布对象,对象的引用和对象的状态必须同时对其它线程可见,一个正确创建对象应该通过下列条件进行安全发布:
 1. 通过静态初始化器初始化对象的引用。
 2. 将对象引用存储到volatile或者AtomicReference。
 3. 将它的引用存储到正确创建对象的final域中。
 4. 将它的引用存储到由锁保护的域中。

参考文献

[1]. 《JAVA并发编程实战》.


版权声明:本文为qq_36566262原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。