线程安全与实现方法

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全与实现方法相关的知识,希望对你有一定的参考价值。

线程安全

文章目录

线程安全的定义

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不用进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

线程安全的分类

  • 不可变

    只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸),那其外部的可见状态永远不会改变,永远都不会看到它在多个线程之中处于不一致的状态。对于一个基本数据类型,在定义时使用final关键字修饰它就可以保证它是不可变的。

  • 绝对线程安全

    不管运行时环境如何,调用者都不需要任何额外的同步措施。

  • 相对线程安全

    通常意义上的线程安全,它需要保证这个对象单次的操作是线程安全的,在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,需要在调用端使用额外的同步措施来保证调用的正确性。

  • 线程兼容

    指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步措施来保证在并发环境中可以安全地使用。

  • 线程对立

    指不管调用端是否采用了同步措施,都无法在多线程环境中并发使用代码。

补充:this引用逃逸

逃逸分析针对的是内存逃逸,当一个对象在方法体定义后,通过一些方式在其他地方被引用,可能导致GC时,无法立即回收,从而造成内存逃逸。如果被外部方法引用,譬如作为调用参数传递到其他方法中,这种称为方法逃逸;如果被外部线程访问到,例如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸。

什么是this引用逃逸

在构造器还未完成前(实例初始化完成之前),将自身this引用向外抛出并被其他线程复制使用了,可能会导致其他线程访问到"初始化了一半的对象"(即尚未初始化完成的对象),会造成影响。

逃逸场景

场景一

在构造函数中启动了新的线程(新的线程拥有this引用)

import java.text.SimpleDateFormat;
import java.util.Date;

public class EscapeForThis 

    int a;
    int b = 0;

    public EscapeForThis() 
        a = 1;
        // 在构造函数中新建一个线程(拥有this引用),访问成员变量
        new Thread(new Runnable() 

            @Override
            public void run() 
                System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + a + ",b=" + b);
            
        ).run();
        b = 1;
    

    public static void main(String[] args) 
        EscapeForThis s = new EscapeForThis();
        System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + s.a + ",b=" + s.b);
    

执行结果s

[12:04:55--new] a=1,b=0
[12:04:55--main] a=1,b=1

新建的线程在访问b的时候b尚未完成初始化,没有访问到正确的数据。

场景二

在构造器中内部类使用外部类:内部类可以无条件的访问外部类(自动持有外部类的this引用),当内部类发布出去后,即外部类的this引用也发布出去了,此时无法保证外部类已经初始化完成。

外部类EscapeForThis,内部类EventListener

/**
 * 事件监听器接口,调用事件处理器
 */
public interface EventListener 
    /**
     * 事件处理方法
     */
    void doEvent(Object obj);

demo

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class EscapeForThis 

  	private final int a;
    private final String b;

    private EscapeForThis(EventSource<EventListener> source) throws InterruptedException 
        a = 1;
        source.registerListener(o -> System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + a + ",b=" + EscapeForThis.this.b));
        // 为了演示效果
        TimeUnit.SECONDS.sleep(2);
        b = "xx";
    

    static class EventSource<T> 
        private final List<T> eventListeners;

        public EventSource() 
            eventListeners = new ArrayList<T>();
        

        /** 注册监听器*/
        public synchronized void registerListener(T eventListener)   //数组持有传入对象的引用
            this.eventListeners.add(eventListener);
            this.notifyAll();
        

        /** 获取事件监听器*/
        public synchronized List<T> retrieveListeners() throws InterruptedException   //获取持有对象引用的数组
            List<T> dest = null;
            if (eventListeners.size() <= 0) 
                this.wait();
            
            dest = new ArrayList<T>(eventListeners.size());  //这里为什么要创建新数组,好处在哪里?防止对象逃逸,同时减少对eventListeners的竞争
            dest.addAll(eventListeners);
            return dest;
        
    

    /**新建一个执行线程上*/
    static class ListenerRunnable implements Runnable 
        private final EventSource<EventListener> source;

        public ListenerRunnable(EventSource<EventListener> source) 
            this.source = source;
        

        @Override
        public void run() 
            List<EventListener> listeners = null;

            try 
                // 获取事件监听器
                listeners = this.source.retrieveListeners();
             catch (InterruptedException e) 
                e.printStackTrace();
            
            assert listeners != null;
            for (EventListener listener : listeners) 
                listener.doEvent(new Object());  //执行内部类获取外部类的成员变量的方法
            
        
    
    
    public static void main(String[] args) throws InterruptedException 
        EventSource<EventListener> s = new EventSource<>();
        ListenerRunnable runnable = new ListenerRunnable(s);
        Thread thread = new Thread(runnable);
        thread.start();
        EscapeForThis escapeForThis = new EscapeForThis(s);
        System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + escapeForThis.a + ",b=" + escapeForThis.b);
    


执行结果

[15:34:39--Thread-0] a=1,b=null
[15:34:41--main] a=1,b=xx

在外部类中对成员变量进行初始化的时候 休眠了2s,是为了凸显this引用逃逸的效果。

形成this引用逃逸的条件

  • 一个是在构造函数中创建内部类,并且在构造函数中就把这个内部类给发布了出去。可以通过破外条件才避免this引用逃逸。

解决方案

  1. 不要在构造函数中启动线程,可以单独一个方法在外部启动线程。

    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class EscapeForThis 
    
        int a;
        int b = 0;
    
    	private Thread t;
        public EscapeForThis() 
            a = 1;
            // 在构造函数中新建一个线程(拥有this引用),访问成员变量
            t= new Thread(new Runnable() 
    
                @Override
                public void run() 
                    System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + a + ",b=" + b);
                
            );
            b = 1;
        
        
        public void initStart() 
            t.start();
        
        
        public static void main(String[] args) 
            EscapeForThis s = new EscapeForThis();
            s.initStart();
            System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + s.a + ",b=" + s.b);
        
    
    
  2. 不要在外部类初始化完成之前,发布内部类。

    import java.text.SimpleDateFormat;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    public class EscapeForThis 
        private final int a;
        private final String b;
        private final EventListener listener;
    
        private EscapeForThis()
            a = 1;
            listener = new EventListener()
                @Override
                public void doEvent(Object obj) 
                    System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + a + ",b=" + EscapeForThis.this.b);
                
            ;
            b = "xx";
        
    
        public static EscapeForThis getInstance(EventSource<EventListener> source) 
            EscapeForThis safe = new EscapeForThis();  //先初始化
            source.registerListener(safe.listener);  //发布内部类
            return safe;
        
    
        static class EventSource<T> 
            private final List<T> eventListeners;
    
            public EventSource() 
                eventListeners = new ArrayList<T>();
            
    
            /** 注册监听器*/
            public synchronized void registerListener(T eventListener)   //数组持有传入对象的引用
                this.eventListeners.add(eventListener);
                this.notifyAll();
            
    
            /** 获取事件监听器*/
            public synchronized List<T> retrieveListeners() throws InterruptedException   //获取持有对象引用的数组
                List<T> dest = null;
                if (eventListeners.size() <= 0) 
                    this.wait();
                
                dest = new ArrayList<T>(eventListeners.size());  //这里为什么要创建新数组,好处在哪里?防止对象逃逸,同时减少对eventListeners的竞争
                dest.addAll(eventListeners);
                return dest;
            
        
    
        /**新建一个执行线程上*/
        static class ListenerRunnable implements Runnable 
            private final EventSource<EventListener> source;
    
            public ListenerRunnable(EventSource<EventListener> source) 
                this.source = source;
            
    
            @Override
            public void run() 
                List<EventListener> listeners = null;
    
                try 
                    // 获取事件监听器
                    listeners = this.source.retrieveListeners();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                assert listeners != null;
                for (EventListener listener : listeners) 
                    listener.doEvent(new Object());  //执行内部类获取外部类的成员变量的方法
                
            
        
    
        public static void main(String[] args) throws InterruptedException 
            EventSource<EventListener> s = new EventSource<>();
            ListenerRunnable runnable = new ListenerRunnable(s);
            Thread thread = new Thread(runnable);
            thread.start();
            EscapeForThis escapeForThis = EscapeForThis.getInstance(s);
            System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "--" + Thread.currentThread().getName() + "] a=" + escapeForThis.a + ",b=" + escapeForThis.b);
        
    
    
    

线程安全的实现

互斥同步(阻塞同步)

采用悲观并发策略的同步措施,无论是否出现竞争,都按加锁操作。

同步是指在多个线程并发访问数据时,保证共享数据在同一时刻只被一条线程使用。互斥是实现同步的一种手段,临界区、互斥量、信号量都是常见的互斥实现方式。

同步手段就是synchronized关键字、Lock接口。

线程阻塞和唤醒需要额外的性能开销。

实现举例

public class Monitor 

    private int a = 0;

    public synchronized void writer(String s)      // 1
        System.out.println(s);                      // 2
        a++;                                        // 3
                                                   // 4

    public synchronized void reader(String s)     // 5
        System.out.println(s);                     // 6
        int i = a;                                 // 7
        System.out.println(i);                     // 8
                                                  // 9

public class Client 

    public static void main(String[] args) 
        Monitor monitor = new Monitor();

        new Thread(() -> 
            monitor.reader("Thread 0");
        ).start();

        new Thread(() -> 
            monitor.writer("Thread 1");
        ).start();

        new Thread(() -> 
            monitor.reader("Thread 2");
        ).start();
    

如何创建线程?如何保证线程安全?

在后台线程上安全保存 Core Data 托管对象上下文的正确方法?

线程安全与可重入编写方法

从多个线程使用 QuantLib 的正确方法是啥?

简单测试Java线程安全中阻塞同步与非阻塞同步性能

线程基础四