线程安全性与安全策略

线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。

  • 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
  • 可见性:一个线程对主内存的修改可以及时被其他线程观察到
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

原子性

原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作

  • atomic包, 以Atomic开头的类,Java底层是使用CAS进行实现
    例如:AtomicInteger 调用了java底层的Unsafe.compareAndSwapInt方法,这里是实现CAS的关键,Unsafe类中调用的方法大多数是native,由jvm本地实现
  • AtomicLong、LongAdder
  • AtomicReference AtomicReferenceFieldUpdater
@Slf4j
@ThreadSafe
public class AtomicExample4 {

    private static AtomicReference count = new AtomicReference<>(0);

    public static void main(String[] args) {
        count.compareAndSet(0,2); // 2
        count.compareAndSet(0,1); // no
        count.compareAndSet(1,3); // no
        count.compareAndSet(2,4); // 4
        count.compareAndSet(3,5); // no
        log.info("count:{}",count.get());
    }
}
@Slf4j
public class Test3 {

    private static AtomicReferenceFieldUpdater updater = 
    AtomicReferenceFieldUpdater.newUpdater(Test3.class, Integer.class, "count");

    private volatile Integer count = 0; // 必须 非静态的 volatile 修饰的字段

    public static void main(String[] args) throws Exception{
        Test3 test3 = new Test3();
        updater.compareAndSet(test3, 0, 1);
        updater.compareAndSet(test3, 0, 2);
        updater.compareAndSet(test3, 1, 3);
        updater.compareAndSet(test3, 2, 4);
        log.info("结果:{}", updater.get(test3));

    }

}
  • AtomicStampReference:CAS的ABA问题
    该类的核心方法compareAndSet,该类是通过一个重新定义一个stamp的值来标记当前值是否被更改过。

  • AtomicBoolean

@Slf4j
@ThreadSafe
public class Test5 {
    // 请求总数
    private static int clientTotal = 5000;
    // 可同时执行的线程数
    private static int threadNum = 200;
    private static AtomicBoolean isHappen = new AtomicBoolean(false);

    public static void main(String[] args) throws Exception {
        final Semaphore semaphore = new Semaphore(threadNum);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });

        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("ishappen:{}", isHappen.get());
    }

    private static void update() {
        if (isHappen.compareAndSet(false, true)) { // 只执行一次
            System.out.println("update exec");
        }
    }
}

可见性

可见性:一个线程对主内存的修改可以及时的被其他线程观察到

JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从而是用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)

关于synchronized 影响可见性的问题

synchronized

  • 修饰代码块:大括号括起来的代码,作用于调用的对象
  • 修饰方法:整个方法,作用于调用的对象
  • 修饰静态方法:整个静态方法,作用于所有对象
  • 修饰类,括号括起来的部分,作用于所有对象

synchronized:不可中断锁,适合竞争不激烈,可读性好
Lock:可中断锁,多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值

volatile

通过加入内存屏障和禁止重培训优化来实现:

  • 对于volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
  • 对于volatile变量读操作时,会在读操作前加入一条load屏障指令,从主存中读取共享变量

有序性

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

如果两个操作执行的次序无法从happens-before原则中推导出来,那么就不能保证他们的有序性,jvm就可以随意的对他们进行重排序

happen-before原则:

  1. 程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。

  2. 管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。

  3. volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。

  4. 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。

  5. 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  6. 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。

  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。

  8. 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

volatile可以保证一定的有序性,不能保证原子性。

synchronized,lock保证了单线程的运行,因此肯定时有序的

安全策略

不可变对象

  • Collections.unmodifiableXXX:Collection、List、Set、Map…
  • Guava:ImmutableXXX:Collection、List、Set、Map

线程封闭

  • 堆栈封闭:局部变量,无并发问题
  • ThreadLocal 线程封闭:特别好的封闭方法
  1. 使用Filter将用户信息保存到ThreadLocal中
  2. 在业务方法中使用处理
  3. 使用拦截器(Inteceptor)在方法执行完后移除

同步容器:

  • ArrayList -> Vector, Stack
  • HashMap -> HashTable(key, value不能为null)
  • Collections.SynchronizedXXX(List,Set,Map)

并发容器

  • ArrayList -> CopyOnWriteArrayList
  • HashSet/TreeSet -> CopyOnWriteArraySet/ConcurrentSkipListSet
  • HashMap/TreeMap -> ConcurrentHashMap/ConcurrentSkipListMap

最佳实践

  • 使用本地变量
  • 使用不可变类
  • 最小化锁的作用域范围
  • 使用线程池的Executor,而不是直接new Thread
  • 宁可使用同步也不要使用线程的wait和notify
  • 使用BlockingQueue实现生产-消费模式
  • 使用并发集合而不是加了锁的同步集合
  • 使用Semaphore创建有界的访问
  • 宁可使用同步代码块,也不使用同步的方法
  • 避免使用静态变量(使用时最好能加上final)

你可能感兴趣的:(java,Java高并发)