Java并发编程实战 – 笔记[亲测有效]

Java并发编程实战 – 笔记[亲测有效]Java并发编程实战第一章简介1.1并发简史​ 之所以在计算机加入操作系统来实现多个程序同时执行,主要基于以下原因:资源利用率:在某些程序中,可能需要执行非常耗时的操作,而这些操作往往不需要使用CPU,例如IO操作,此时CPU处于空闲状态,对于CPU而言,是个极大的浪费。如果在这种情况下,我们同时间运行另一个需要占用CPU的程序,无疑是提高了CPU的利用率。公平性:不同用户和程序对计算机的资源有着同等的使用权力,较为常用的是基于操作系统,已时间片分为运行的粗粒度保证了计算机资源使用的公平性

大家好,欢迎来到IT知识分享网。

Java并发编程实战

第一章 简介

1.1 并发简史

​ 之所以在计算机加入操作系统来实现多个程序同时执行,主要基于以下原因:

  1. 资源利用率:在某些程序中,可能需要执行非常耗时的操作,而这些操作往往不需要使用CPU,例如IO操作,此时CPU处于空闲状态,对于CPU而言,是个极大的浪费。如果在这种情况下,我们同时间运行另一个需要占用CPU的程序,无疑是提高了CPU的利用率。
  2. 公平性:不同用户和程序对计算机的资源有着同等的使用权力,较为常用的是基于操作系统,已时间片分为运行的粗粒度保证了计算机资源使用的公平性
  3. 便利性:通常来说,在计算多个任务时,应该编码多个程序,每个程序执行一个任务,并在必要的时候进行程序间的通信,这比写一个程序实现所有任务更为简单的多

1.2 线程的优势

​ 线程时轻量级的进程,现代的大多数操作系统都是已线程为调度单位;相比于进程来说,同一个进程下的所有线程,共享和共用所属进程的内存空间,与进程间的通信来说,线程通信的代价更少。但正因为同一进程下的线程公用内存地址空间,所以更容易造成数据的不安全,例如当某个线程1正在使用某个变量A,而同时有另外一个线程2修改变量A,导致变量A修改,但是线程1依然使用修改前的值(脏数据)。

​ 当代的CPU都是多核多处理器,而CPU的调度单位是线程,如果是单线程程序,会导致CPU只被占用一个核心,导致其他CPU核心的浪费,因此多线程能够发挥多核处理器的能力。

​ 除此之外,多线程还能够提高程序的吞吐量。在某个线程进行IO的输入或者输出时候,此时的CPU将处于空闲状态,如果此时有另一个线程能够使用CPU的话,就能够使程序继续运行,不会因为IO阻塞导致程序停止运行。

​ 在单线程中,如果线程因为某些操作被阻塞后,会影响整个程序的运行,例如IO流操作,因为IO读取或写入磁盘的速度远远低于内存的运行速度,而内存的运行速度又远远低于CPU的执行速度,因此,在单线程中,IO阻塞整个程序的运行是必然的,会导致CPU的资源浪费。而在多线程中,线程之间正常是不会堵塞(除非是线程间强用同一资源),因此一个线程的阻塞不会使得程序停止运行。特别是在服务器请求中,我们不可能因为一个请求堵塞导致整个服务器的不可用,解决办法有使用NIO(NON BLOCKING IO),非阻塞的IO和多线程,为每个请求都给予单独一个线程,这就不会导致单个请求的阻塞影响到整个服务器。

1.3 多线程带来的风险

1.3.1 安全性问题

​ 线程安全性的问题是非常复杂的,在没有充足同步情况下,多个线程的操作执行顺序是不可预测的,例如以下代码

public class UnsafeSequence { 
   

    private int value;
	
    //线程不安全
    public int getSequence(){ 
   
        return value++;
    }

}

​ 代码中的方法 **getSequence()**主要是获取唯一序列,这在单线程应用中是可行的,但是在多个线程之间交替执行操作将有可能获取到重复的序列。发生线程问题主要在于 value++这一步操作,虽然在代码中看是一个操作,但实际上编程成字节码,可以分成 三步操作。 第一步 获取value值 ,赋值给一个变量,第二步 对 变量+ 1,第三步,将 + 1 后的变量值存储回原来的value,由于运行时,线程的交替操作,因此在这三步操作也有可能交替执行,也就是多个线程同时读取了 value值(此时为6),然后这几个线程++后将会获取同一样的序列7,如图:

在这里插入图片描述

​ 在UnsafeSequence类中发生的并发安全问题,称为竞态条件;竞态条件是描述在线程,进程,系统之间交互运行,运行结果往往去取决于运行的顺序,在多线程中由于运行顺序的不确定往往会导致运行结果并不是我们所希望看到的或者是出乎我们期望的。

​ 为了解决竞态条件所产生的问题,我们一般对共享变量加锁,能够限制多线程在操作共享变量的时候保证同步操作,能够保证线程之间不会相互干扰。例如我们可以给UnsafeSequence类中的方法加上修饰词使方法成为一个同步方法。

public class UnsafeSequence { 
   

    private int value;

    //同步方法,保证方法线程安全
    //可以防止线程交替执行产生重复的序列
    public synchronized int getSequence(){ 
   
        return value++;
    }

}

1.3.2 活跃性问题

​ 除了安全性问题外,多线程还会出现单线程中不会有的问题:活跃性问题;活跃性问题的形式之一就是无意中造成的无限循环,从而导致其他代码无法被执行,还有一些活跃性问题,如死锁,锁死,饥饿,活锁等等。

1.3.3 性能问题

​ 虽然多线程是为了解决单线程资源浪费,运行慢的问题,但是不合理的设置线程数量也会导致多线程有性能问题,线程数量过多可能比单线程的情况下效率更低。当线程挂起活跃的线程,转而去运转另一个线程时,就会频繁的发生上下文切换,上下文切换因为需要保存被挂起线程的当前状态,比如代码执行到哪里,变量值等等,运行另一个线程时需要恢复之前保存的数据,因此保存和恢复上下文会带来大的开销,当线程数量过于多的时候,就会导致CPU忙于执行上下文切换(切换上下文的频率次数变高),而导致代码的执行效率变低。

第一部分-基础知识

第二章 线程安全性

基本概念:
1. 摩尔定律:集成电路芯片上所集成的晶体管数量,越每隔18个月便会翻一番。
2. Amdahl定律:对计算机系统的某个部件采用优化措施后所获得的计算机性能的提高,依赖于这部分的执行时间在整个运行时间中所占的比率。
3. 竞争条件:多个任务并发访问和操作同一数据且执行结果与访问的特定顺序有关,称为竞争条件。(多个任务竞争响应某个条件,因访问顺序不同产生冲突或不一致的情况)。比如“检查再运行”“惰性初始化”。
4. 原子操作:任务在执行过程中不能被打断的一序列操作
5. 复合操作:任务在执行过程中可以被打断的一序列操作
6. 不变约束:不变式表达了对状态的约束,这些状态是应该符合这个约束的值的组合。不变式可以代表某种业务规则。
7. 先验条件:针对方法,规定了在调用方法之前必须为真的条件
8. 后验条件:针对方法,规定了在调用方法之后必须为真的条件
9. 原子性:(见原子操作)
10. 可见性:确保线程对变量的写入对其他线程是可见的。即刷新内存中的变量。

​ 在构建安全的并发程序时,必须正确的使用线程和锁来保证程序的的线程安全,这些只是保证线程安全的机制;要编写线程安全的程序,其核心在于对共享状态和可变状态的管理。

​ 状态一般指的是对象中的实例和静态实例(字段,field),对象中的状态也可能依赖其内嵌的其他对象,比如汽车的引擎作为汽车类的内嵌对象,这些内部的引用可能也会影响到类的安全性;

​ 状态的共享意味着可能会多个线程访问共享状态,而状态的可变性意味着,在对象的生命周期内,对象的状态可以发生变化;所以一个对象是否需要线程安全的设计时,就需要考虑该对象是否会被多个线程同时访问,考虑该对象的状态是否可变,如果是单线程访问,或者状态是不可变的就不用考虑在多线程访问对象时,会l获得不一致的对象状态;如果是被多线程访问,状态也是可变的话,特别是线程中存在写入的操作,这时候我们就要考虑使用锁,原子类,volatile等同步机制来保证线程安全。

​ 许多线程不安全的类看似并不会影响到程序的执行(活跃性问题会影响到程序的执行,死锁等等),但是可能在某一个时刻中会导致数据的异常,发生错误,而这种异常往往是比较难复现的,因为线程安全性往往都是竞态条件导致的,取决与线程间和代码间的运行顺序。

​ 解决多线程访问共享的可变状态导致的线程不安全的问题,一般可以使用下面的三种解决办法:

  1. 不允许在多线程之间共享该状态。
  2. 保证状态是不可变的,不允许被执行写操作。
  3. 多线程访问共享状态时候,使用同步机制。

2.1 什么是线程安全性

​ 线程安全性最核心的概念就是(类的)正确性,那什么是正确性呢?正确性的含义简单来说就是,类的行为与其规范完全一致。在新建某个类的时候,一般都会对类做一些规范要求(业务中,就是业务规则),例如举个简单的例子,在一个类中,我们有两个属性,一个最大值,一个最小值,而我们对这个类的规范就是最小值不允许大于最大值,而在多线程中,我们对这两个属性的修改就必须要使用同步机制保证他们的修改操作为原子操作,这样才能够保证这个类的正确性。一般一个类在单线程的正确性我们一般可以认为这就是类的正确性,因此只要多线程操作的时候,该类始终能够与单线程中保持一致的正确行为,那么就称这个类是线程安全的

	当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程按什么顺序交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能便显出正确的行为,我们就称这个类是线程安全的。(无状态对象一定是线程安全的)

2.2 原子性

​ 原子在化学反应中是不可分割的最小单位,原子性指的就是一个操作是不可分割的。例如I++,虽是一行代码,实则是做了三个操作,读取 I , I+1,将结果写回变量 I 中,因此代码不具有原子性。原子性最直接明了的解释就是具有原子性的代码要么不执行,要执行就一次性全部执行,在执行过程中不会切换线程。

2.2.1 竞态条件

​ 当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换个说法,也就是说正确的执行结果取决于”运气”。最常见的竞态条件就是 先检查后执行,通过一个可能是已经失效的或过期的状态来决定下一个操作;比较有代表性的就是单例模式懒汉式的延迟加载。还有一种较为常见的竞态条件**,读取-修改-写入**这种操作,要使这三个操作正确执行必须确保变量没有被其他线程更新或使用。

2.2.2 复合操作

​ 包含一组需要以原子方式执行的操作,要避开竞态条件,就必须保证在执行过程中,其他线程无法访问和更新变量,从而保证其他线程获得变量是相对新的。先检查后执行读取-修改-写入 等操作统称为复合操作:包含了一组以原子方式执行的操作。

2.3 加锁

​ 在Servlet中添加一个状态变量时,我们可以使用线程安全的类来管理,例如AtomicReference类,保证该对象变量的读取修改是线程安全的;但如果添加多个状态变量呢?

@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet{ 
   

    private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
    private final AtomicReference<BigInteger[]> lastFactories = new AtomicReference<>();

    public void service(ServletRequest req , ServletResponse rep){ 
   
        BigInteger i = extractFromRequest(req);
        if(i == lastNumber.get()){ 
   
            encodeIntoResponse(rep,lastFactories.get());
        }else { 
   
            BigInteger[] factors = factor(i);
            //下面两行代码必须是复合操作
            lastNumber.set(i);
            lastFactories.set(factors);
            
            encodeIntoResponse(rep,factors);
        }
    }

}

​ 代码将分解因数的结果缓存起来,实现该缓存策略我们使用了两个状态变量,lastNumber保存最近传进来的参数,lastFactories保存lastNumber对应的因数。代码中尽管这些原子引用都是线程安全的,但是代码依然存在竞态条件(check-Then-act),可能会导致错误的结果,会导致不变性条件被破坏,代码中的不变性条件就是lastFactories存储的因数之积要等与lastNumber。只有确保该不变性条件不被破坏才能保证线程的安全性,因此当更新一个状态时,必须以原子方式同时更新另一个状态变量。

2.3.1 内置锁

​ Java提供了一种内置锁来支持原子性:同步代码块 (Synchronized Block)。同步代码块包括了两个部分,一个作为锁的对象的引用,另一个作为由这个锁保护的代码块,以关键字Synchronized 来修饰的方法,锁的对象引用就是调用该方法的对象,保护的代码块就是方法里的代码;静态的synchronized方法的锁对象就是Class对象。还有一种方式:

synchronized(lock){
  // 访问由lock保护的共享状态
}

​ 每个Java对象都可以用作实现同步的锁,这些锁被称为内置锁或监视锁。线程在进入同步代码块之前会自动获取锁(在操作系统层面就是获取监视器),并且在推出同步代码块时会自动释放对应的锁。锁是进入同步代码块中的唯一条件。

​ 内置锁是一种互斥锁,这意味着同一时刻内只能由一个线程获取锁进入同步代码块,其他线程必须等待运行中的线程释放锁,如果线程一直不释放锁就会使其他线程等待或者阻塞。因为只有一个线程能够进入同步代码块,所以能够确保这些代码已原子性的方式执行,不会同时被多个线程所执行和访问,是一个并行到串行的一个操作。因此在UnsafeCachingFactorizer类中要保持线程安全我们可以在 service方法中加上修饰符synchronized。正是因为从并行到串行的一个转变因此该方法的效率会大大降低,甚至不如单线程执行(因为上下文切换有消耗,单线程没有),使得服务的响应性非常低,这个问题将在2.5中解决。

2.3.2 重入

​ 当线程请求一个已经被其他线程所占有的锁,该线程将处于等待或者阻塞的状态。然而内置锁是可重入的,因此一个线程试图获取已经被它自己持有的锁的时候,那么这个线程是可以获取到锁的。重入意味着获取的锁的粒度不是调用,而是线程;重入锁的一个实现方法是,为每个锁关联一个计数器和所有者线程;当计数器为0的时候,此时锁并没有被任何线程所有。当线程请求一个未被持有的锁时,会记录下锁的持有者,并且将计数器置为1;如果同一个线程再次获取这个锁,计数器递增,当线程退出同步代码块时,计数器会相应的递减,当计数器为0的时候,这个锁将被释放。

​ 重入可以避免死锁的情况。比如子类改写了父类的synchronized方法,此时调用子类方法需要获取当前对象的锁,重写的方法中调用父类的方法(super.dosomthing),此时需要进入同步代码块,需要再次请求当前对象的锁,如果该锁是不可重入的,那么就会形成死锁问题,获取不到请求的锁,而请求的锁也不释放。

2.4 用锁来保护状态

​ 锁能够保护代码以串行的方式访问,因此可以通过锁来操作和实现对共享状态的独占访问。只要始终遵循这些协议就能够确保状态的一致性。

​ 并不是只有在写操作的时候需要使用同步,在读操作如果不加锁会导致可见性的问题。

​ 锁与状态之间没有什么内在联系,在获取锁时,并不能阻止其他线程去访问状态变量,在某个线程获取锁后,只能够阻止其他线程获取同一个锁;之所以每个对象都有一个内置锁,只是为了免去显示地创建锁对象。

Tips;
1.对于可能被多个线程同时访问的可变状态变量,在访问它时都需要同一个锁,在这种情况下我们称这个变量由这个锁保护
2.每个共享的和可变的变量都应该只由一个锁来保护。
3.对于每个包含多个变量的不变性条件,其中涉及的所有变量都应该使用同一个锁来保护。

2.5 活跃性与性能

2.3中UnsafeCachingFactorizer 类,是个线程不安全的类,我们也给出了解决线程问题的方法,在方法中加上修饰符使得访问这个方法时是以同步的方式访问,虽然解决了线程安全,但是也引入了性能上的问题,Serlvet初衷就是能够同时处理多个请求,对每个请求都能够迅速响应,但是同步的方式使得在负载高的情况下会带给用户糟糕的体验。每个请求都需要等待上一个请求的结束,虽然我们使用的是多线程,但是实际上却是以一个串行的方式去运行,并不能够很好的利用CPU资源,我们称这种并发为不良并发。因此我们为了保证线程的安全,但是同时又要顾及到性能问题,我们必须对函数进行一定的分析,将没有访问共享变量代码从同步代码块中剔除出去,使得这些操作在运行的同时其他线程又能够同时访问共享变量,修改后的代码如下

public void service(ServletRequest req , ServletResponse rep){ 
   
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) { 
   
            if(i == lastNumber.get()) { 
   
                factors = lastFactories.get();
                encodeIntoResponse(rep,lastFactories.get());
            }
        }
        
        if(factors == null) { 
   
            //分解操作,会耗费大量时间,且不会访问共享变量,因此剔除出同步代码块
            factors = factor(i);
            synchronized (this) { 
   
                lastNumber.set(i);
                lastFactories.set(factors);
            }
            encodeIntoResponse(rep,factors);
        }
        
    }

​ 当使用锁时,应该清楚代码中实现的功能,以及在执行该代码块时是否需要耗费大量的实际。无论时执行CPU密集操作,还是某个阻塞操作,如果持有锁的时间过长,都会带来活跃性的问题。

第三章 对象的共享

​ 第2章的关键的概念:在访问共享的可变状态变量时需要进行正确的管理,介绍了通过锁,同步代码块来避免多个线程在同一时刻访问相同的变量,而本章介绍如何共享和发布对象,使得状态能够被多个线程安全的同时访问。

​ 一般来说,只认为synchronized修饰符只用于实现同步访问,原子操作和定义临界区,但同步其实还有另一个作用:内存的可见性。我们不仅希望防止某个线程在使用状态而另一个线程同时修改状态,而且更希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。可以通过显示的同步来保证对象的被安全的发布。

3.1 可见性

​ 在单线程中,我们对一个变量执行写操作,在没有其他写操作的情况下读取这个值,那么总能够得到相同的值;然而读操作和写操作在不同线程中执行,结果往往跟我们所想的会有所出入,这正是因为可见性的问题,我们无法保证执行读操作的线程能够看到其他线程写入的值,为了保证读操作的线程能够看到写操作现场的值,必须使用同步机制。

​ 下面代码很可能会出现死循环或者输出number为0(没有实现死循环,和输出0的的结果),正是多个线程对共享的状态变量没有使用同步代码块出现可见性的问题。主线程和新起的线程共享状态 readynumber,主线程对ready的写操作,可能新线程会读取不到主线程写操作得值,因此死循环,要么就只是读取到部分值,会输出 0 。如果要避免这些问题,就是在多线程中有共享的可变状态中,使用正确的同步机制。

public class NoVisibility { 
   

    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread{ 
   
        @Override
        public void run() { 
   
            while(!ready){ 
   
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException { 
   
        new ReaderThread().start();
        Thread.sleep(1000);
        number = 42;
        ready = true;
    }

}

3.1.1 失效数据

NoVisibility展示了如果多线程访问可变的共享状态没有使用正确的同步机制有可能会导致出现错误,而这些错误正是由于线程无法获取共享变量的最新值,也就是获取到了失效数据;失效数据在程序中有可能导致较大的错误,例如线程的活跃性问题,死循环,数据计算的不准确等等错误。

​ 下面这个MutableInteger类就不是一个线程安全的类,get set 并没有同步访问,因此很容易出现失效数据的问题,例如一个线程正在执行set的操作,而另一个线程执行get操作,而get操作的线程可能会获取到线程set的值,但是也很有可能几率获取到失效的数据(set之前的数据);

/** * 如果需要保证该类是线程安全, * 我们需要在get set 方法中加上修饰符synchironize,如果get不加,很有可能会因为可见性的问题获取到了失效数据 */
public class MutableInteger{ 
   
    private int value;
    
    public void setValue(int value){ 
   
        this.value = value ;
    }
    
    public int getValue(){ 
   
        return this.value;
    }
}

3.1.2 非原子的64位操作

​ Java中的8种基本类型,除了double 和 long 类型,其他6种基本类型的读写操作都是原子性;因为JVM中,会将64位的读操作和写操作划分成两个32位的操作,因此double和long的读写操作都不是原子性,因此如果在多线程中共享double和long类型的变量,单独的写和读的操作,都会导致数据的不正确,可能会获取到一个随机的值并不是失效数据(失效数据指的是之前的确是由某个线程在某个时刻设置的)。

​ volatile并无法保证方法的原子性,但是能够保证doublelong类型读写的原子性,Java文档中有说明 : Writes and reads of volatile long and double values are always atomic. 要保证doublelong的读和写操作的原子性,我们可以使用同步代码块或者volatile修饰变量。

3.1.3 加锁与可见性

​ 内置锁可以确保某个线程以一种可预见的方式查看另一个线程的执行结果。如下图所示,一个线程A执行完由锁M所保护的同步代码块后,再由线程B来执行由锁M所保护的同步代码块,这种情况就可以保证,线程B获取锁M后的,可以准确的获取到线程A在释放锁M之前的所有操作结果。如果没有同步就无法有上述的保证。

在这里插入图片描述

​ 所以这就是为什么在访问多线程中的可变共享的变量时需要要求所有线程在同一个锁对象上同步,就是为了保证线程间的可见性,一个线程在未持有正确的锁的情况下读取某个变量,很可能获取到的是个失效值。

​ 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读或者写操作的线程必须要在同一个锁上同步。

3.1.4 Volatile 变量

Java提供了一种稍弱的同步机制,volatile变量,用来确保更新操作通知到其他线程。当某个变量被声明位volatile后,编译器和运行器都会注意到该变量是个共享的,因此不会将该变量的操作与其他内存的操作进行重排序。声明位volatile的变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取的时候会返回最新写入的值。访问volatile变量不会进行加锁的操作,因此不可以保证对变量操作的原子性,例如 COUNT++,即便被声明为volatile也无法保证原子性。也因此对volatile的操作的代价远比加锁小,仅比普通变量的开销略高一点。

volatile变量就是为了保证可见性,从内存可见性来说,读取volatile变量相当于进入同步代码块,而写volatile变量相当于退出同步代码块。

​ 加锁机制可以确保可见性又可以确保原子性,而volatile只能够确保可见性。

​ 当且仅当满足以下条件,才建议使用volatile变量:

  • 当变量的写入操作不依赖变量的当前值,或者能够确保只有一个线程对变量进行更新。
  • 当变量不与其他变量一起纳入不变性条件中
  • 在访问对象时候不需要加锁

3.2 发布与逸出

​ 发布一个对象的意思是指,对象能够在当前当前作用域之外的代码中使用。一般来说我们要确保对象不被发布,这样能够保证一个类的封装性不被破坏。如果在对象构造完成之前发布该对象,就会破坏线程安全性。当某个不应该被发布的对象被发布了,这种情况我们称之为逸出。

​ 发布对象最简单的方法就是将对象的引用保存到一个共有的静态变量中,以便任何类和线程都能看见该对象。当发布对象时,可能会简介地发布其他对象,例如knowSecrets被发布了,其中存储的Secret也被发布了。当发布了一个对象,该对象的所有被私有域中所有引用的对象统一会被发布。Secret对象中如果有Password对象是共有的,那Password也被发布了。

public static Set<Secret> knowSecrets;

public void initialize(){ 
   
    this.knowSecrets = new HashSet<Secret>();
}

​ 无论线程对已发布的对象做何种操作其实都不重要,重要的是发了的对象始终存在被误用的风险,无法保证这种错误是不会发生的。所以这正是需要使用封装来保证对。

​ 下面是一种隐式的使this引用逸出,就是但对象还没被构造完成时,该对象就已经能够被其他对象获取或者操作。

public class ThisEscape { 
   
    public final int id;
    public final String name;
    public ThisEscape(EventSource<EventListenerB> source) { 
   
        id = 1;
        source.registerListener(new EventListenerB() { 
   
            public void onEvent() { 
   
                System.out.println("id: "+ThisEscape.this.id);
                System.out.println("name: "+ThisEscape.this.name);
            }
        });
        name = "flysqrlboy";

    }

    public static void main(String[] args) { 
   
        //输出ID 为 1 name 为空,此时初始化还没完成会存在线程安全的问题。
        new ThisEscape(new EventSource<EventListenerB>());
    }

    private static class EventSource<T> { 
   
        public void registerListener(EventListenerB eventListener) { 
   
            eventListener.onEvent();
        }
    }
}

​ this引用逸出是怎样产生的。它需要满足两个条件:一个是在构造函数中创建内部类(EventListener),另一个是在构造函数中就把这个内部类给发布了出去(source.registerListener)。因此,我们要防止这一类this引用逸出的方法就是避免让这两个条件同时出现。也就是说,如果要在构造函数中创建内部类,那么就不能在构造函数中把他发布了,应该在构造函数外发布,即等构造函数执行完毕,初始化工作已全部完成,再发布内部类。

​ 关于隐式this逸出可以查看这篇blog,写的很详细,this引用逸出(“this” Escape)解释

​ 要想在构造函数中正确的注册一个事件监听器或者启动线程,那么可以使用一个私有的构造器和工厂方法。

public class SafeListener{ 
   
    private final EventListener listener;
    
    private SafeListener(){ 
   
        listener = new EventListener(){ 
   
            public void onEvent(Event e){ 
   
                dosomething(e);
            }
        };
    }
    
    public static SafeListener getInstance(EventSource source){ 
   
        SafeListener safe = new SafeListener();
        soruce.registerListener(safe);
        return safe;
    }
    
}

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/25431.html

(0)
上一篇 2023-07-24 13:00
下一篇 2023-07-25 15:33

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信