山一程--语言特性--Java 并发

目的:深入研究基于语言特性级别的并发


2023-01-25

1. Thread 是任务与执行单元 二合一,

而 Runnable, Callable, ForkJoinTask 将 任务分离, 执行由 Executor框架执行.

2. new Thread(Runnable).start()  -> Executor  replace Thread.    Executore ->  void execute( Runnable command ) 

   executor.execute(runnable)

  基于Executor,可以实现调优,管理,监视,记录日志,错误报告。


任务并发

public class FutureRender {

  private static final Logger logger = LoggerFactory.getLogger(FutureRender.class);
  private static final ExecutorService executor = Executors.newFixedThreadPool(9);

  void renderPage(CharSequence charSequence) {

      final List<ImageInfo> imageInfos = mockScanForImage(charSequence);

      // image task
      Callable<List<ImageInfo>> imageInfoTask = new Callable<List<ImageInfo>>() {
          @Override
          public List<ImageInfo> call() throws Exception {
              List<ImageInfo> result = new ArrayList<>();
              imageInfos.forEach(ImageInfo::download);
              return result;
          }
      };
      Future<List<ImageInfo>> imagInfoFuture = executor.submit(imageInfoTask);

      renderText(charSequence);

      // acquire image I/O task
      try{
          List<ImageInfo> imageTaskResult = imagInfoFuture.get();
          imageTaskResult.forEach(this::renderImage);
      }catch (InterruptedException e){
          // reset the threader interrupted state
          Thread.currentThread().interrupt();
          // result is not necessary, cancel task
          imagInfoFuture.cancel(true);
      }catch (ExecutionException e){
          throw lanuderThrowable(e.getCause());
      }
  }

    //<editor-fold desc="image operations">
    private List<ImageInfo> mockScanForImage(CharSequence charSequence) {
    return new CopyOnWriteArrayList<>();
  }

  private class ImageInfo {

      ImageInfo download(){
          return new ImageInfo();
      }

  }
    //</editor-fold>

    private void renderText(CharSequence source){}

    private void renderImage(ImageInfo imageInfos){

    }
    private RuntimeException lanuderThrowable(Throwable t){
      if(t instanceof RuntimeException){
          return (RuntimeException)t;
      }else if(t instanceof Error){
          throw (Error)t;
      }else
          return new IllegalStateException("illegal state: "+t.getCause());
    }

}
View Code

Executor 框架, 多项任务,超时控制

public class RankedTravelQuote {

  private final ExecutorService executor = Executors.newFixedThreadPool(8);

  public List<TravelQuote> getRankedTravelQuote(
      TravelInfo travelInfo,
      Set<TravelCompany> companySet,
      Comparator<TravelQuote> comparator,
      long time,
      TimeUnit timeUnit)
      throws InterruptedException {

    List<QuoteTask> quoteTasks = new ArrayList<>();

    companySet.stream().forEach(e -> quoteTasks.add(new QuoteTask(e, travelInfo)));

    List<Future<TravelQuote>> travelQuoteFuture = executor.invokeAll(quoteTasks, time, timeUnit);

    List<TravelQuote> quotes = new ArrayList<>(quoteTasks.size());

    Iterator<QuoteTask> quoteTaskIterator = quoteTasks.iterator();
    for (Future<TravelQuote> quoteFuture : travelQuoteFuture) {

      QuoteTask quoteTask = quoteTaskIterator.next();

      try {
        TravelQuote travelQuote = quoteFuture.get();
        quotes.add(travelQuote);
      } catch (ExecutionException e) {
        quotes.add(quoteTask.getFailureQuote(e.getCause()));
      } catch (CancellationException e) {
        quotes.add(quoteTask.getTimeoutQuote(e.getCause()));
      }
    }

    Collections.sort(quotes, comparator);
    return quotes;
  }
}
View Code
public class QuoteTask implements Callable<TravelQuote> {

    private final TravelCompany travelCompany;
    private final TravelInfo travelInfo;

    public QuoteTask(TravelCompany travelCompany, TravelInfo travelInfo) {
        this.travelCompany = travelCompany;
        this.travelInfo = travelInfo;
    }

    @Override
    public TravelQuote call() throws Exception {
        return travelCompany.solicitQuote(travelInfo);
    }

    TravelQuote getFailureQuote(Throwable throwable){
        return new TravelQuote();
    }

    TravelQuote getTimeoutQuote(Throwable throwable){
        return new TravelQuote();
    }
}
View Code

 特定时间内更新或采用default值

public class RenderPageWithAd {

    private final ExecutorService executor = Executors.newFixedThreadPool(8);
    private final Ad DEFAULT_AD = new Ad();

    public Page renderPageWithAd() throws InterruptedException{
        long endNanos = System.nanoTime() + 1000l;

        Callable<Ad> adTask = new Callable<Ad>() {
            @Override
            public Ad call() throws Exception {
                return getAd();
            }
        };

        Future<Ad> future = executor.submit(adTask);
        Page page = renderPage();
        Ad ad;
        try{
            long timeLeft = endNanos - System.nanoTime();
            ad = future.get(timeLeft, TimeUnit.NANOSECONDS);
        }catch (ExecutionException e){
            ad = DEFAULT_AD;
        }catch (TimeoutException e){
            ad = DEFAULT_AD;
            future.cancel(true);
        }

        page.setAd(ad);
        return page;
    }

    private class Page{
        private Ad ad;
        private void setAd(Ad ad){
            this.ad = ad;
        }
    }

    private class Ad{

    }

    private Ad getAd(){
        return new Ad();
    }

    private Page renderPage(){
        return new Page();
    }

}
View Code


2023-02-02

夯实基础

线程安全问题:

对象可变,且是并发访问,且无同步控制

1.race condition  数据不正确, 由于不恰当的执行时序出现不正确的结果。

2.线程活跃性问题:死锁,活锁,饥饿

3.性能问题

Servlet 通常会访问其他Servlet 共享的信息,例如应用程序中的对象,这些对象保存在 ServletContext 中,或者会话中的对象(这些对象保存在每个客户端的HttpSession中).

共享情况下,要协同访问,多个请求可能在不同的线程中同时访问这些对象。 Servlet filter 和对象,必须是线程安全的.


 RMI :1.正确协同在多个对象中共享的状态,以及对远程对象本身状态的访问(由于同一个对象可能会在多个线程中被同时访问)


 1.不在线程间共享该状态变量

2.将状态变量修改为不可变的变量

3.在访问状变量时使用同步。


无状态类中添加一个状态,该状态完全由线程安全的对象来管理,则这个类仍然是线程安全的.

但是当添加的数量是一个以上时,要注意把他们整体原子性管理。 


<JUC> p22

仅将复合操作 计数器(读取- 修改 - 写入) 或延迟初始化 (先检查后执行)封装到一个同步代码块是不够的.

要同步来协调对某个变量的访问,在访问这个变量的所有位置上都要使用同步。

而且,当使用锁来协调对某个变量的访问时,在访问变量的所有位置上都要使用同一个锁。

要自行构造加锁协议或者同步策略来实现对共享状态的安全访问。


许多线程安全类,Vector 和同步集合类的模式:将所有可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问,对象状态中的所有变量都由对象的内置锁保护起来。

Vector 如果不存在则添加 是不安全的, Composite 方法, 将其传入自定义类的构造函数,作为 final 底层对象, 而后在扩展类上 Synchonize 实现,默认限制,vector 被作为构造参数后,不再使用。 


2023-02-03

线程性能:需要很长时间或者执行计算密集的操作,某个可能阻塞的操作,,网络I/O 等,不要持有锁。(思考:任何保证并发安全) 


final atomic

分解锁:

public class RecommandCache {
    private BigInteger lastNumber = new BigInteger("0");
    private BigInteger[] lastFactors = new BigInteger[]{new BigInteger("0")};
    private long hits = 0L;
    private long cacheHits = 0l;

    // 二者有关联性,要原子操作
    public synchronized long getHits(){return this.hits;}
    public synchronized  double getCachedHitRadio(){
        return (double) cacheHits / (double)hits;
    }

    public void service(int mockInput){
        BigInteger mockExtractParam = new BigInteger(String.valueOf(mockInput)); // mock
        BigInteger[] factors = null;
        synchronized (this){
            hits++;
            if(mockExtractParam.equals(lastNumber)){
                cacheHits++;
                factors = lastFactors.clone();
            }
        }

        if(factors == null){
            factors = mockCalc(mockExtractParam);
            synchronized (this){
                lastNumber = mockExtractParam;
                lastFactors = factors;
            }
        }
        // 以上分解有顺序依赖,最后返回结果
        mockReturn(factors);
    }
View Code

  2023-02-04

1.通过同步来避免多个线程在同一时刻访问相同的数据。

2.共享和发布安全对象,使之能安全由多个线程同时访问

二者构建线程安全类以及通过 concurrent 类库来构建并发应用程序的重要基础.


非原子的64位操作。因JVM分解为两个32位操作。数据不安全。

long, double 等类型变量,要用 volatile 声明或用 锁保护。

内存可见性。临界区

加锁:互斥,内存可见性。所有线程能看到共享变量的最新值,

所有执行读操作或者写操作的线程都必须要在同一个锁上同步。


volatile 编译器和运行时会主要到该变量共享,不会将该变量上的操作与其他内存操作一起重排序,

不会被缓存在寄存器或对其他处理其不可见的地方,因此在读取该类型的变量时总会返回最新写入的值。

不会使执行线程阻塞。更加轻量级。但用于:

1. 确保它们自身状态的可见性。

2.确保它们引用的对象可见性。

3.标识一些重要的程序声明周期事件的发生,初始化或关闭。

 

volatile 不能确保原子性,加锁机制既能确保原子性,也能确保可见性.

必须同时满足以下三个条件时才能使用:

1.对变量的写入操作不依赖变量的当前值,或确保只有单个线程更新变量的值。

2.该变量不与其他状态变量一起纳入不可变条件。

3.在访问该变量时不需要加锁


发布与逸出

对象能在当前作用域之外的代码中使用,

在对象构造完成之前就使用该对象,会破坏线程安全性。

不该发布的对象被发布,称为逸出 escape.

1. 将对象的引用保存到一个公有的静态变量中。任何类和线程都能看见。

2.发布一个对象时,该对象的非私有域中引用的对象同样会被发布。注意 final, private

3.发布一个内部的类实例,


 this 逸出常见错误:在构造函数中启动一个线程。

在对象在器构造函数中创建一个线程时:

1.显示创建 通过将它传给构造函数

2.隐式创建 由于在 Thread 或 runnable 是该对象的一个内部类。

this 引用都会被新创建的线程共享。在对象完全构造之前,新的线程就可以看到它。

新建线程不要立即启动,通过一个 start 或 initialize 方法启动。 

构造函数中调用一个可以改写的实例方法(非 private , final ). 同样会导致this 逸出。

下例中 this 逸出。发布对象或其内部状态的机制。内部类的实例中包含了对外部类实例的隐含引用。

public class ThisEscape {
    public ThisEscape(EventSource source){
        source.registerListener(
                new EventListener(){
                    public void onEvent(Event e){
                        doSomething(e);
                    }
                }
        );
    }
}

 修正:

只有当构造函数返回时,this引用才能从线程逸出。

构造函数将this引用保存在某个地方,只要其他线程不会再构造函数完成之前使用它。

要在构造函数中注册一个事件监听器或启动线程,可以使用

一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程.

public class SafeListener {
    private final EventListener listener;
    
    public SafeListener(){
        listener = new EventListener() {
            public void onEvent(Event e){
                // doSomething ( e)
            }
        };
    }
    
    public static SafeListener newInstance(EventSource source){
        SafeListener safeListener = new SafeListener();
        source.registerListener(safeListener.listener);
        return false;
    }
}

封闭线程: 

共享可变的数据时,需要同步。为了避免同步,可不共享数据。

如果尽在单线程内访问数据。就不需要同步。--线程封闭。 是实现线程安全性对简单的方式之一。

应用场景:JDBC.

服务器应用程序中,线程从线程池中获得一个connection 对象。用该对象处理请求,使用完后再将对象返还给连接池。

大多数请求,都似乎由单个线程采用同步的方式来处理,且在connection 对象返回之前,连接池不会再将它分配给其他线程,因此,

这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中.

Java : 局部变量, 和 ThreadLocal 类。仍要确保封闭在线程中的对象不会从线程中逸出。


volatile 和 单个线程程序组合,可提供弱的 安全对象. 后期维护工作量大。


 

栈封闭

栈封闭中,只能通过局部变量才能访问对象。

同步变量也能使对象更易封闭在线程中。

局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

基本类型的局部变量,不会破坏栈封闭。任何方法都无法获得对基本类型的引用。-》基本类型的局部变量始终封闭在线程中。

public int loadTheArk(Collection<Animal> candidates){

        // 基本类型 局部变量,栈封闭,绝对不会逸出
        int numPairs = 0;

        // 引用对象封闭在方法内,注意不要逸出,
        // 如果发布了对集合 animals (或者该对象中的任何数据)的引用,封闭性被破坏。逸出。
        SortedSet<Animal> animals;

        Animal candidate = null;

        animals = new TreeSet<>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal animal : animals) {
            if(candidate == null || !candidate.isPotentialMate(animal)){
                candidate = animal;
            }else {
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }

在线程内部上下文中使用非线程安全的对象,对象仍然是线程安全的。编码人员知道哪些对象需要被封闭到执行线程中,以及被封闭的对象是否是线程安全的.


ThreadLocal

将单线程程序移植到多线程环境中,通过将共享的全局变量转换为 ThreadLocal 对象(全局变量语义允许),就可以维持线程安全性.

一个事务上下文与某个执行中的线程关联起来。通过将事务上下文保存在静态的 ThreadLocal 对象中,可以很容易实现这个功能: 当框架程序需要判断

当前运行的是哪个事务时,只需要从这个ThreadLocal对象中读取事务上下文。避免了在调用每个方法时都要传递执行上下文信息。

注意不要将 所有全局变量都作为ThreadLocal对象,不要作为一种“隐藏”参数的手段。会降低可重用,并在类之间耦合。

维持线程封闭性的规范用法

使线程中的某个值与保存值的对象关联起来。

ThreadLocal get() set() 等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本. get() 总是返回由当前执行线程

在调用set时设置的最新值.

用于防止对可变的单实例变量 singleton 或 全局变量进行共享.

JDBC 连接可能不是安全的, 通过将JDBC连接保存到 ThreadLocal 对象中,每个线程都会拥有属于自己的连接.

public class DemoThreadLocal {

  private static final String DB_URL = "...";

  private static ThreadLocal<Connection> connectionHolder =
      new ThreadLocal<>() {
        public Connection initialValue() {
          try {
            return DriverManager.getConnection(DB_URL);
          } catch (SQLException e) {
            throw new RuntimeException("connection ex");
          }
        }
      };

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

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术.

当某个线程初次调用 ThreadLocal.get() ,就会调用 initialValue 来获取初始值。特定于线程的值保存在 Thread 对象中,当线程终止后,值会作为垃圾回收。


不可变对象

不变性是由构造函数创建的. 满足以下3个条件为不可变对象:

1.对象创建以后状态就不可变

2.对象的所有域都是final类型

3.对象是正确创建的(在对象的创建期间, this 引用没有逸出) 

在不可变对象内部仍可以使用可变对象来管理它们的状态.

public class TreeStorage {
    
    // 尽管保存姓名的Set对象是可变的,但在 Set 对象构造完成后便无法更改。
    // stooges 是一个final 类型的引用变量,因此所有的对象状态都通过一个final域来访问。
    private final Set<String> stooges = new HashSet<>();
    
    public TreeStorage(){
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }
    
    public boolean isStooge(String name){
        return stooges.contains(name);
    }
}

final 类型的域是不能修改的,但如果所引用的对象是可变的,那么这些被引用的对象可以修改的.

Java 类型模型中,final 域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步。


两个 AtomicReference 变量要以原子性的例子中,

不可变对象能提供一种弱形式的原子性, 使用 volatile 类型的引用来确保可见性,发布不可变对象.

需要一组数据以原子方式执行某个操作时,创建一个不可变的类来包含这些数据。

在别的地方 使用volatile 类型的字段引用。

当一个线程将引用设置为新的不可变对象时,其他线程会立即看到新缓存的数据。

// Immutable
public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;
    
    public OneValueCache(BigInteger lastNumber,
                         BigInteger[] factors){
        this.lastNumber = lastNumber;
        // 如果没有在调用 copyOf, 则该对象不是不可变的
        this.lastFactors = Arrays.copyOf(factors,factors.length);
    }
    
    public BigInteger[] getFactors(BigInteger i){
        if(lastNumber == null || !lastNumber.equals(i)){
            return null;
        }else{
            // 如果未调用 copyOf, 则该对象不是不可变的.
            return Arrays.copyOf(lastFactors,lastFactors.length);
        }
    }
}

使用指向不可变容器对象的 volatile 类型引用以缓存最新的结果

与cache 相关的操作不会互相干扰,OneValueCache 是不可变的,在每条对应的代码路径只会访问一次。

public class VolatileCachedFactorizer {

    private volatile OneValueCache cache =
            new OneValueCache(null,null);

    public void service(ServletRequest request, ServletResponse response){
        BigInteger mockExtraction = mockExtract(request);
        BigInteger[] factors = cache.getFactors(mockExtraction);
        if(factors == null){
            factors = calc(mockExtraction);
            cache = new OneValueCache(mockExtraction,factors);
        }
        mockReturn(response,factors);
    }
    
    private BigInteger mockExtract(ServletRequest request){
        return new BigInteger("1");
    }
    
    private void mockReturn(ServletResponse response,BigInteger[] factors){
        
    }
}

2023-02-05 

安全发布

多个线程之间共享对象.此时必须确保安全地进行共享.

将对象引用保存到公有域中,不安全. ( 存在可见性问题,其他线程看到的 Holder 对象将处于不一致的状态,

即便该对象的构造函数中已经正确地构建了不可变条件)

这种不正确的发布将导致其他线程将看到尚未构建完成的对象.

以下对象如果按上面不安全方式发布,会抛出 error.

由于没有使用同步来确保Holder对象对其他线程可见。存在两个问题:

1.除了发布对象的线程外,其他线程可能看到的Holder 是一个失效值,将看到一个空引用或之前的旧值。

2.线程看到的Holder引用的值是最新的,但 Holder 状态是失效的.

构造函数中设置的域值并不是第一次向这些域中写入的值,会有更旧的值失效。

Object的构造函数会在子类构造函数运行之前将默认值写入所有域。

 

还会出现,某个线程在第一次读取域时得到失效值,再次读取这个域是更新值。

 

修正: 不可变对象,final

任何线程 都可以在不需要额外同步的情况下安全访问 不可变对象,即使发布这些对象时未使用同步。

被创建对象中所有final 类型的域。注意,如果final 类型的域指向可变对象,则需要同步。


 

可变对象安全发布,

在发布和使用该对象的线程时 都必须使用同步。

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他对象可见。

常用模式:

一个正确构造的对象,通过以下方式发布:

1.在静态初始化函数中初始化一个对象引用.

2.将对象的引用保存到 volatile 类型的域AtomicReference对象中.

3.将对象的引用保存到某个正确构造对象的final 类型域中.

4.将对象的引用保存到一个 锁 保护的域中


线程安全容器内部同步意味着,在将对象放入到容器,例如Vector 或 SynchronizedList时,满足最后一条。

 

如果线程A 将对象X放入一个线程安全的容器,随后线程B读取这个对象,可以确保B看到A设置的X状态,即使这段读/写X的代码中没有包含显式的同步。

线程安全库中的容器类提供了 以下的安全发布:将该元素安全发布到从所在的容器中访问该元素的线程。

1.通过将一个 键或值 放入 Hashtable, synchronizedMap 或 ConcurrentMap 中,(无论是直接访问或者是通过迭代器访问)

2.Vector, CopyOnWriteArrayList, CopyOnWriteArraySet, synchronizedList 或 synchronizedSet。

3.BlockingQueue 或者 ConcurrentLinkQueue 。 

 Future 和 Exchanger 同样实现安全发布. 

发布一个静态构造的对象,最简单和最安全的方式是使用静态的构造器.

 静态初始化器由 JVM 在类的初始化阶段执行,JVM 内部的同步机制,因此通过该方式初始化的任何对象都可以被安全发布。


可变对象,安全发布只能确保“发布当时” 状态的可见性,

因此不仅在发布对象时需要使用同步,在每次对象访问时同样需要使用同步来确保后续修改操作的可见性.

策略:

1.不可变对象可以通过任意机制发布。

2.事实不可变对象必须安全发布。

3. 可变对象必须通过安全方式发布,且必须是线程安全的或者由某个锁保护起来。


  获得一个对象引用,需要知道:

1.在这个引用上可以执行哪些操作。

2.使用它之前是否需要获得一个锁?

3.是否可以修改它的状态,或者只能读取它。


共享对象的策略:

1.线程封闭。 对象只能由一个线程拥有,对象被封闭在该过程中,且只能由这个线程修改.

2.只读共享:不可变对象或事实不可变对象. 任何线程都不能修改它, 可由多线程并发访问.

3.线程安全共享。线程安全的对象在其内部实现同步,因此多线程可以通过对象的公有接口来进行访问,不需进一步同步。

4.保护对象。被保护的对象只能通过持有特定的锁来访问。包括:封装在其他线程安全对象中的对象;已发布的且由某个特定的锁保护的对象。


不可变对象委托 线程安全容器 发布

public class MonitorVehicleTracker {

  // VehicleLocation 是不可变对象
  private final Map<String, VehicleLocation> locations;
  private final Map<String, VehicleLocation> unmodifiableMap;

  public MonitorVehicleTracker(Map<String, VehicleLocation> locations) {
    this.locations = new ConcurrentHashMap<>(locations);
    this.unmodifiableMap = Collections.unmodifiableMap(locations);
  }

  /**
   * 如果 unmodifiableMap 里的不是不可变对象,则会破坏封装性,
   * 会发布一个指向可变状态的引用,该引用不安全。
   * 返回的是一个不可修改,但却实时的车辆位置视图。
   * 
   * 如果线程A调用getLocations(), 而线程B在随后修改了某些点的位置,
   * 那么在返回给线程A的Map中将反映出这些变化。
   * 
   * 优点(更新的数据) 和 缺点(可能导致不一致的车辆位置视图)
   * @return
   */
  public Map<String, VehicleLocation> getLocations() {
    return this.unmodifiableMap;
  }

  /**
   * 需要的是不发生变化的车辆视图, location Map 对象的浅拷贝,
   * map 内容不可变,只需要复制Map的结构,不用复制它内容.
   * @return
   */  
  public Map<String,VehicleLocation> getLocationsSnapshot(){
    return Collections.unmodifiableMap(new HashMap<>(locations));
  }

  public VehicleLocation getVehicleLocation(String vehicleName) {
    return this.locations.get(vehicleName);
  }

  public void setVehicleLocation(String vehicleName, double x, double y) {
    if (this.locations.replace(vehicleName, new VehicleLocation(x, y)) == null) {
      throw new IllegalArgumentException("invalid vehicle name: " + vehicleName);
    }
  }
}

只要变量是彼此独立的,组合而成的类不会在其包含的多个状态变量上增加任何不利条件时,可将线程安全性委托给多个状态变量。

并且在所有的操作中都不包含无效状态转换.

仅当一个变量参与到包含其他状态变量的不变性条件时,才可以声明为 volatile 类型

public class VisualComponent {
    //每个链表都是线程安全的,且二者都是独立的,因此是安全发布。
    private final List<KeyListener> keyListeners = 
            new CopyOnWriteArrayList<>();
    private final List<MouseListener> mouseListeners = 
            new CopyOnWriteArrayList<>();
    
    public void addKeyListener(KeyListener keyListener){
        this.keyListeners.add(keyListener);
    }
    
    public void removeKeyListener(KeyListener keyListener){
        this.keyListeners.remove(keyListener);
    }    
    // mouse omit    
}

反例:没有维持多个变量间的不变性。可通过加锁机制来维护不变性条件,此外还要避免发布 lower 和 upper,防止客户代码破坏其不变性条件.

public class NumberRange {
    
    // 不变性条件 lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);
    
    public void setLower(int i){
        // attention: 不安全的 "先检查后执行"
        if(i > upper.get()){
            throw new IllegalArgumentException("input upper out of boundary.");
        }
        lower.set(i);
    }
    
    public void setUpper(int i){
        // omit 
    }
    
    public boolean isInRange(int i){
        return (i <= upper.get() && i >= lower.get());
    }    
}

2023-02-06

发布底层的状态变量.

把线程安全性委托给某个对象的底层状态变量,如何能安全发布,且使其他类能修改它们。 取决于在类中对这些变量施加了哪些不变性条件.

(发布可变的变量将对下一步开发,派生子类带来限制,但不会破坏类的线程安全性)

1.状态变量是线程安全的.

2.没有任何不变性条件约束它的值。

3.在变量的操作也不存在任何不允许的状态转换。

满足以上三个条件可以安全发布这个变量.


发布其底层可变状态,还能确保其线程安全不被破坏。

// 发布底层可变状态,可变且线程安全
public class SafePoint {

    private int x;
    private int y;

    public SafePoint(int x,int y){
        this.x = x;
        this.y = y;
    }

    /**
     * 如果将拷贝构造函数实现为 this(p.x, p.y) 会产生静态条件, 而私有构造函数可避免
     * 这是私有构造函数捕获模式 的一个实例.
     */
    public SafePoint(SafePoint safePoint){
        this(safePoint.get());
    }
    private SafePoint(int[] points){
        this(points[0],points[1]);
    }

    /**
     * 必须使用 int[] 把 有关联性的 x,y 放在一起.
     * 否则不同线程修改了某个值,会造成不存在的坐标 (x,y)
     * 
     * */
    public synchronized int[] get(){
        return new int[]{x,y};
    }

    public synchronized void set(int x, int y){
        this.x = x;this.y = y;
    }
}
/**
 * 该类如果在 车辆位置的有效性上施加任何约束, 就不再是线程安全的.
 * 如果需要对车辆位置的变化进行判断,或当位置变化时执行操作,此处方法不合适.
 */
public class PublishingVehicleTracker {
    
    private final Map<String,SafePoint> locations;
    private final Map<String,SafePoint> unmodifiableMap;
    
    public PublishingVehicleTracker(Map<String,SafePoint> locations){
        this.locations =new ConcurrentHashMap<>(locations);
        this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
    }
    
    // “实时”返回
    // 底层Map对象的一个不可变副本
    public Map<String,SafePoint> getLocations(){
        return this.unmodifiableMap;
    }
    
    public SafePoint getLocation(String id){
        return this.locations.get(id);
    }
    
    // 调用者不能增加或删除车辆,可以通过修改Map中的SafePoint值来改变车辆位置。
    public void setLocation(String id,int x, int y){
        if(!locations.containsKey(id)){
            throw new IllegalArgumentException("invalid id: " + id);
        }
        this.locations.get(id).set(x,y);
    }    
}

在现有的安全类中添加功能.

扩展类机制 和 客户端加锁机制, 将派生类的行为与基类耦合在一起。

扩展会破坏实现的封装性。

客户端加锁会破坏同步策略的封装性.

1.优选重用,次之修改源码,再次 扩展类 例如 vector

// safe
public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E x){
        boolean absent = !contains(x);
        if(absent){
            add(x);
        }
        return absent;
    }
}

2.客户端加锁机制,脆弱,会导致扩散和不能控制。

注意加锁位置:

3.最好 composite, 例如,通过将 List 对象的操作委托给底层的 List 实例来实现 List 的操作.


安全基础模块

旧的同步容器,在一些操作例如 addIfAbsent , 采取的是“及时失败” 策略, 

迭代时会存在 迭代期间的计数器关联,但检查是在没有同步的情况下进行,可能会看到失效的计数值.

for each 也一样,需要在迭代过程持有容器锁。关系到容器规模,元素操作,可能会产生死锁。

采取“克隆”容器操作,在副本上操作。 克隆过程中要持有锁。


 在所有对共享容器进行迭代的地方都需要加锁。

以下操作会对容器迭代。ConcurrentModificationException.

容器的 hashCode, equals, 间接执行迭代。

容器作为另一个容器的元素或键值。

containsAll, removeAll, retainAll,

把容器作为参数的构造函数


 字符串拼接会隐式调用 StringBuilder.append(Object), 该方法又调用容器的 toString 方法,标准容器的 toString 将迭代容器。在每个元素上

调用toString() 来生成容器内容的格式化表示。


 并发容器,不会在迭代时抛出 CurrentModificationException. 弱一致性,  isEmpty(), size() 是近似值。

CopyOnWriteArrayList, Set

ConcurrentMap 接口 :ConcurrentHashMap -> 老式同步 HashTable,  Collection.synchronizeMap(xx)

Queue : ConcurrentLinkedQueue,  PriorityQueue,  没有元素时返回空.

BlockingQueue: 生产者--消费者 队列.  没有数据时等待,满数据时阻塞。  


 concurrentHashMap,  不是整个锁定,使用分段锁 (Lock Striping) , 允许多个读取并发访问,一定数量的写并发修改 Map。 高吞吐量。

如果需要原子添加一些映射, 或对Map迭代若干次并在此期间保持元素顺序相同。则需要获得Map锁独占。HashMap, synMap.

public class DemoConcurrentLib {
    
    private CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

    @Deprecated
    private Map<String,String> legacyMap = new Hashtable<>();
    @Deprecated
    private Map<String,String> synchronizedMap = Collections.synchronizedMap(legacyMap);

    private ConcurrentMap<String,String> concurrentMap =
            new ConcurrentHashMap<>();

    private ConcurrentMap<String,String> concurrentMap1 =
            new ConcurrentSkipListMap<>();
    @Deprecated
    private SortedMap insteadMap1 = new TreeMap();

    private ConcurrentSkipListSet<String> concurrentMap2 =
            new ConcurrentSkipListSet<>();
    @Deprecated
    private SortedSet<String> sortedSet =
            new TreeSet<>();

    private Queue<String> queue1 = new ConcurrentLinkedQueue<>();

    private BlockingQueue<String> productCustomer1 = new ArrayBlockingQueue<String>(1);
    private BlockingQueue<String> productCustomer2 = new LinkedBlockingQueue<>();

    public void demo(){
        this.list.addIfAbsent("addIfAbsent");
        concurrentMap.putIfAbsent("k1","kv");
    }

    public static void main(String... args){
    }
}

ForkJoin

FokrJoinPool  可使用 common() 或直接 new ForkJoinPool.  静态字段保存即可,成为单例。 使用多个无意义. 默认使用 runtime.availableProcessor();

work stealing 技术,每个线程为任务保存为双向链式队列。

最佳做法:

1.对一个任务调用join方法会阻塞调用方。直到该任务出结果。因此,要在两个子任务的计算都开始之后再调用它.否则会比顺序算法更慢.

2.不在 RecursiveTask 内部使用 ForkJoinPool 的 invoke 方法,应始终直接调用 compute 或 fork 方法, 只有顺序代码才应该用 invoke 来启动并行计算.

3.对子任务调用fork方法会把它排进ForkJoinPool, 要对左右其中一个调用 compute, 可为其中一个子任务重用同一线程.减少在线程池中多分配一个任务的开销。

4.只有子任务的运行时间都应该比分出新任务所花的时间长. 一个惯用方法是把 输入/输出 放在一个子任务里,计算放在另一个里,这样计算就可以和 输入/输出 同时进行。

forkJoin 框架需要预热或执行几遍才能被 JIT编译器优化。 

public class ForkJoinInSumCalculate extends RecursiveTask<Long> {

  private final long[] numbers;
  private final int start;
  private final int endOpenRange;
  public static final long THRESHOLD = 10l;

  // invoke by client
  public ForkJoinInSumCalculate(long[] numbers) {
    this(numbers, 0, numbers.length);
  }

  // used inner
  private ForkJoinInSumCalculate(long[] numbers, int start, int endOpenRange) {
    this.numbers = numbers;
    this.start = start;
    this.endOpenRange = endOpenRange;
  }

  @Override
  protected Long compute() {

    int length = endOpenRange - start;
    if (length < THRESHOLD) {
      return computeSequential();
    }

    ForkJoinInSumCalculate calcLeft =
        new ForkJoinInSumCalculate(numbers, start, (start + endOpenRange) / 2);
    calcLeft.fork();

    ForkJoinInSumCalculate calcRight =
        new ForkJoinInSumCalculate(numbers, (start + endOpenRange) / 2, endOpenRange);
    Long rightResult = calcRight.compute();

    Long leftResult = calcLeft.join();//读取第一个子任务的结果,如果尚未完成就等待

    return leftResult + rightResult;// 合并结果。
  }

  private long computeSequential() {
    long sum = 0;
    for (int i = start; i < endOpenRange; i++) {
      sum += numbers[i];
    }
    return sum;
  }
}
ForkJoinTask
public class Client {
    public static void main(String... args){
        long start = System.nanoTime();
        long result =   forkJoinSum(10000);
        long interval = System.nanoTime() - start;
        System.out.println("result: " + result + ", interval: " + interval);

    }

    public static long forkJoinSum(long n){
        long[] numbers = LongStream.rangeClosed(1,n).toArray();
        
        // RecursiveTask  的父类.
        ForkJoinTask<Long> task = new ForkJoinInSumCalculate(numbers);
        
        // 使用多个 ForkJoinPool 无意义的,一般实例化一次,然后把实例保存在静态字段中,
        // 使之成为单例,就可以在各地方重用.
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        
        // .invoke(fjt) 就是 任务类定义的结果。
        long result = forkJoinPool.invoke(task);
        return result;
    }
}
Client
    private long sequentialCompute(){
        // mock calculation
       return LongStream.rangeClosed(start,end)
                .reduce((a,b)->a+b).getAsLong();
    }
sequentialCompute

 

正确的封装原则:除非拥有某个线程,否则不能对该线程进行操控. 例如 中断线程或时修改线程优先级.

线程池的线程中断,要由线程池操作.

应用程序拥有服务,不可拥有工作者线程,服务可拥有工作者线程.应用程序不能直接体停止工作者线程.

服务应提供生命周期方法来关闭它自己和它所拥有的线程。例如 ExecutorService 提供了 shutdown 和 shotdownNow 方法.

持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,都应该提供生命周期的方法.


 

取消一个生产者--消费者操作时,需要同时取消生产者和消费者。


 

通常会将 ExecutorService 封装在高级别的服务中,且该服务能提供其自己的生命周期方法.  p128

logService 将管理线程的工作委托给一个 ExecutorService,而不是由其自行管理,通过封装ExecutorService,可将所有权链

从应用程序扩展到服务以及线程,所有权链上的各个成员都将管理它锁拥有的服务或线程的生命周期。

public class LogService {

  private final ExecutorService exec = Executors.newSingleThreadExecutor();
  
  public void start(){}
  
  public void stop() throws InterruptedException{
      try{
          exec.shutdown();
          exec.awaitTermination(TIMEOUT,UNIT);
      }finally{
          writer.close();
      }
  }
  
  public void log(String msg){
      try{
          exec.execute(new WriteTask(msg));
      }catch (RejectedExecutionException e){}
  }
}

 

posted @ 2023-01-25 10:19  君子之行  阅读(5)  评论(0)    收藏  举报