后端面试 css cmd ios7 mockito Notify.js vue部署 vue特点 js字符串排序 matlab不等于 python3网络编程 python排序 python运行 java获取当前年月 java环境包 java获取本机ip java系统学习 java集合类型 神龙激活 黑白照片一键变彩色 hexworkshop 电脑基础 mpg格式转换 多面硬币 ps反向选择的快捷键 linux安卓模拟器 德玛上单天赋 上单艾克出装 卸载mysql vs2017密钥 铁血统帅 ps魔棒快捷键 如何去掉抖音水印 英语口语学习软件 文件解密软件 js正则表达式 created 罗技鼠标怎么调灵敏度 苹果视频剪辑 x关机
当前位置: 首页 > 学习教程  > 编程语言

Java多线程实战——Java并发编程基础知识

2020/9/19 15:40:24 文章标签:

文章目录

    • 背景
      • 什么是多线程、高并发、分布式
      • 为什么要引入多线程、高并发、分布式
      • 多线程、高并发有什么问题
    • Java并发编程基础知识
      • 线程安全性
      • 对象的共享
      • 对象的组合
      • 基本同步方法
    • 附录

背景

什么是多线程、高并发、分布式

多线程:从软件或硬件上实现多个线程并发执行来完成任务的一种方法;
分布式:为了解决单个物理服务器性能瓶颈问题而采用的优化手段;
高并发:系统运行的一种状态,即用来解决短时间内遇到大量操作请求

为什么要引入多线程、高并发、分布式

多线程:聚焦于如何使用编程语言将CPU调度能力最大化;
分布式:从物理资源的角度去将不同的机器组成一个整体对外的服务,从而实现高并发、高吞吐的系统;
高并发:从业务角度描述系统的能力,即允许大量用户同一时间访问服务;

多线程、高并发有什么问题

安全性问题:(竞态条件)在多线程环境中,多个线程对同一个变量进行操作,则该变量最终确定的值取决于运行时多个线程操作的交替执行顺序,这不是我们希望看到的情况。
如下图示例,每次运行结果都无法准确预知结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

活跃性问题:形式之一就是无意中造成的无限循环,从而使循环之后的代码无法得到执行;其他形式,如线程A在等待线程B释放其持有的资源,而线程B永远都不释放该资源,则A就会永久地等待下去(死锁、饥饿、活锁等来解决)。

性能问题:包括多个方面,例如服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,或者可伸缩性较低等。因为多线程程序中,线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作,这种操作带来极大的开销(保存和恢复执行上下文,丢失局部性,CPU时间将更多地花在线程调度而不是线程运行上);当线程共享数据时,必须使用同步机制,这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效。

Java并发编程基础知识

线程安全性

  • 什么是线程安全

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

在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施

无状态对象一定是线程安全的。
例如下面的例子:

/**
 * 无状态对象一定是线程安全的:
 *   因为无状态对象不包含任何域,也不包含任何对其他类中域的引用,
 *   所以此类对象所涉及的变量都是存在于线程私有的栈内存的局部变量,
 *   即访问StatelessFactorizer的线程不会影响另一个访问同一个StatelessFactorizer的线程的计算结果,
 *   即这两个线程没有共享状态。
 */
@ThreadSafe
public class StatelessFactorizer implements Servlet {

  public void service(ServletRequest req, ServletResponse resp) {
    BigInteger i  = extractFromRequest(req);
    BigInteger[] factors = factor(i);
    encodeIntoResponse(resp, factors);
  }

  public BigInteger extractFromRequest(ServletRequest req) {
    return new BigInteger("");
  }

  public BigInteger [] factor(BigInteger bigInteger) {
    return new BigInteger[] {};
  }

  public void encodeIntoResponse(ServletResponse resp, BigInteger [] bigIntegers) {}

}
  • 原子性

竞态条件:在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况。
当在无状态的类中添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。
例如:给一个无状态的类添加一个计数器,该计数器通过使用线程安全类AtomicLong来管理,从而保证了代码的线程安全性,下面是一个例子:

/**
 * java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象
 * 引用上的原子状态转换。能够确保所有对计数器状态对访问操作都是原子的。由于Servlet的状态就是计数器的状态,
 * 并且计数器是线程安全的,因此Servlet也是线程安全的
 */
@ThreadSafe
public class CountingFactorizer implements Servlet {

  private final AtomicLong count = new AtomicLong(0);
  public long getCount() {
    return count.get();
  }

  public void service(ServletRequest req, ServletResponse resp) {
    BigInteger i  = extractFromRequest(req);
    BigInteger[] factors = factor(i);
    count.incrementAndGet();
    encodeIntoResponse(resp, factors);
  }

}

然而,当状态变量的数量由一个变为多个时,并不会像状态变量数量由零个变为一个那么简单。

  • 加锁机制

当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。
例如下面这个例子就不是线程安全的:

/**
 * 该类的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的数值。
 * 但是,在线程A获取这两个值的过程中,线程B可能修改了它们,这样线程A也会发现不变性条件被破坏了
 */
public class UnsafeCachingFacorizer implements Servlet {

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

  public void service(ServletRequest req, ServletResponse resp) {
    BigInteger i  = extractFromRequest(req);
    if (i.equals(lastNumber.get()))
      encodeIntoResponse(resp, lastFactors.get());
    else {
      BigInteger[] factors = factor(i);
      lastNumber.set(i);
      lastFactors.set(factors);
      encodeIntoResponse(resp, factors);
    }
  }

}

内置锁
针对上面多个状态相互关联,需要将这些关联的变量进行原子操作。

Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

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

每个Java对象都可以拥有同步锁,这些锁被称为内置锁。Java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁。但是这往往会有性能问题,后面会提出解决办法。

重入

“重入"意味着获取锁的操作的粒度是"线程”,而不是"调用"。
重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。
下面是一个说明重入锁作用的例子:
如果内置锁不是可重入的,则这段代码将发生死锁

public class Widget {
  public synchronized void doSomething() {
    // ...
  }
}

public class LoggingWidget extends Widget {
  public synchronized void doSomething() {
    System.out.println(toString() + ": calling doSomething.");
    super.doSomething();
  }
}
  • 活跃性与性能

通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)
当执行时间较长的计算或可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。

对象的共享

  • 可见性

内存可见性:当一个线程修改了对象状态之后,其他线程能够看到发生的状态变化。
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
关于内存可见性,需要说明一下JVM的内存模型,见下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CEB8bJtb-1600500646317)(quiver-image-url/D7799B673687D7AA8C2CEDC0EB5E9770.jpg =631x445)]
从上图可以看到Java多线程中,每个线程都有自己的工作内存,变量的赋值、取值等操作都是直接与工作内存进行关联,关于这些操作如何执行则由内存模型来定义,通过这些定义的变量的访问规则,可以解决多线程对有状态对象的竞态条件带来的问题。

例如下面就是一个说明多线程可见性引发但一个问题:

/**
 * 变量的不可见性:
 *   代码中,主线程和读线程都将访问共享变量ready和number。
 *   主线程启动读线程,然后将number设为42,并将ready设为true。
 *   读线程一直循环直到发现ready的值变为true,然后输出number的值。
 *   虽然看起来会输出42,但事实上很可能输出0,或者根本无法终止
 */
public class NoVisibility {
  private static boolean ready;
  private static int number;

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

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

下面是一个说明失效数据问题的例子:

/**
 * 非线程安全的可变整数类
 */
public class MutableInteger {
  private int value;

  public int get() {
    return value;
  }
  public void set(int value) {
    this.value = value;
  }

}

/**
 * 线程安全的可变整数类
 */
public class SynchronizedInteger {
  private int value;

  public synchronized int get() {return value;}
  public synchronized void set(int value) {this.value = value;}
}

加锁与可见性

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。
从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。但是,在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,即volatile变量是一种比synchronized关键字更轻量级的同步机制。
下面是volatile变量的一种典型用法:用于检查某个状态标记以判断是否退出循环。

volatile boolean asleep;
  // ...
    while(!asleep) 
      countSomeSheep();
  • 发布与逸出

“发布”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。

逸出:当某个不该不应该发布的对象被发布时,这种情况就被称为逸出。

不要在构造过程中使this引用逸出。 ——安全的对象构造过程

下面是使用工厂方法来防止this引用在构造过程中逸出的例子:

/**
 * 第三章:对象的共享
 *   安全对象的构造过程 -> 不要在构造过程中使用this引用逸出
 *   如果想在构造函数中注册一个事件监听器或启动线程,则可以使用一个私有的构造函数和
 *   一个公共的工厂方法,从而避免不正确的构造过程
 */
public class SafeListener {

  private final EventListener listener;

  private SafeListener() {
    listener = new EventListener() {
      public void onEvent(Event e) {
        doSomething(e);
      }
    };
  }

  public static SafeListener newInstance(EventSource source) {
    SafeListener safe = new SafeListener();
    source.registerListener(safe.listener);
    return safe;
  }

}
  • 线程封闭

栈封闭
由于Java语义中,局部变量都是线程私有的。所以,栈封闭也被称作线程内部使用或者线程局部使用。

对于基本类型的局部变量,无论如何都不会破坏栈封闭性。
在维持对象引用的栈封闭性时,程序员需要多做一些工作以确保引用的对象不会逸出。

ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来,ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

/**
 * 在实现应用程序框架时大量使用了ThreadLocal。例如,在EJB调用期间,
 * J2EE容器需要将一个事务上下文(Transaction Context)与某个执行中的线程关联起来。
 * 通过将事务上下文保存在静态的ThreadLocal对象中,可以很容易地实现这个功能:当框架
 * 代码需要判断当前运行的是哪一个事务时,只需从这个ThreadLocal对象中读取事务上下文。
 */
  • 不变性

如果某个对象在被创建后其状态就不能被修改,则这个对象就被称为不可变对象。

参考前面提到的无状态对象一定是线程安全的,因为它没有可供竞争的状态

不可变对象一定是线程安全的。
当满足以下条件时,对象才是不可变当:

  • 对象创建以后其状态据不能修改;
  • 对象的所有域都是final类型;
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)。

Final域

final类型的域是不能修改的(但如果final域引用的对象是可变的,那么这些被引用的对象是可以修改的)。
在Java内存模型中,final域还有着特殊的语义。final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

关于final关键字:

  • 修饰类:被修饰的类不能被继承(除非这个类真的在以后不会用来继承或者出于安全的考虑,否则尽量不要将类设计为final类)
  • 修饰方法:被修饰的方法不会被重写(类的private方法会被隐式的指定为final方法)
  • 修饰变量:
    1. 如果修饰的是基本数据类型的变量,其数值一旦初始化之后就不能改变;
    2. 如果修饰的是引用变量,则在其初始化之后就不能再让其指向另一个对象

示例:使用volatile类型来发布不可变对象
下面是对前面因数分解的优化,通过不可变对象来提供一种弱形式的原子性。

/**
 * 对数值及其因数分解结果进行缓存对不可变容器类:
 *   每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据
 */
public class OneValueCache {
  private final BigInteger lastNumber;
  private final BigInteger[] lastFactors;

  public OneValueCache(BigInteger i, BigInteger[] factors) {
    lastNumber = i;
    lastFactors = Arrays.copyOf(factors, factors.length);
  }

  public BigInteger[] getFactors(BigInteger i) {
    if (lastNumber == null || !lastNumber.equals(i))
      return null;
    else
      return Arrays.copyOf(lastFactors, lastFactors.length);
  }

}
/**
 * VolatileCachedFactorizer使用了OneValueCache来保存缓存的数值及其因数。
 * 当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到缓存的数据.
 */
public class VolatileCachedFactorizer {

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

  public void service(ServletRequest req, ServletResponse resp) {
    BigInteger i = extractFromRequest(req);
    BigInteger[] factors = cache.getFactors(i);
    if (factors == null) {
      factors = factor(i);
      cache = new OneValueCache(i, factors);
    }
    encodeIntoResponse(resp, factors);
  }

}
  • 安全发布

安全发布的常用模式

  • 在静态初始化函数中
    初始化一个对象引用;
  • 将对象的引用保存到volatile类型的域或者AtomicRefrance对象中;
  • 将对象的引用保存到某个正确构造对象的final类型域中;
  • 将对象的引用保存到一个由锁保护的域中

在线程安全容器内部的同步意味着,在将对象放入到某个容器,将满足上述最后一条需求,即线程安全库中的容器提供了以下的安全发布保证:

  • 通过将一个键或者值放入HashTable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问);
  • 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程;
  • 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程;

事实不可变对象、可变对象、安全地共享对象

对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布;
  • 事实不可变对象必须通过安全方式来发布;
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来;

线程封闭: 对象被封闭在该线程中,并且只能由这个线程修改;
只读共享:
线程安全共享: 线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步同步;
保护对象: 被保护的对象只能
通过持有特定的锁来访问。

对象的组合

每一次内存访问都去分析确保线程安全,比较耗时耗力,所以将现有的一些线程安全组件组合为更大规模的组件或程序。

  • 设计线程安全的类

  • 实例封闭

线程封闭:确保对象只能在单个线程访问,或者通过一个锁来保护对象的所有访问。

实例封闭:当一个对象被封装到另一个对象中时,被封装对象的所有访问方式都是已知的。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
————《Java并发编程实战3》

例如下面就是一个通过封闭机制来确保线程安全的例子:

/**
 * PersonSet对状态由HashSet来管理,但是HashSet不是线程安全对。
 * 由于mySet是私有对并且不会逸出,因此HashSet被封闭在PersonSet中,
 * 唯一能访问mySet的方式就是addPerson和containsPerson,在执行
 * 它们时都要获得PersonSet上的锁。PersonSet的状态完全由它的内置锁保护,
 * 因而PersonSet是一个线程安全的类。
 */
public class PersonSet {

  private final Set<Person> mySet = new HashSet<>();

  public synchronized void addPerson(Person person) {
    mySet.add(person);
  }

  public synchronized boolean containsPerson(Person person) {
    return mySet.contains(person);
  }

}

Java类库中,一些基本容器类并非线程安全的,例如ArrayList和HashMap,但类库提供了装饰器工厂方法(例如Collections.synchronizedList及其类似方法),使得这些非线程安全的类可以在多线程环境中安全地使用。这些工厂方法通过装饰器模式将容器类封装在一个同步的装饰器对象中,而装饰器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在装饰器中),则它就是线程安全的。
————《Java并发编程实战》

  • 线程安全性的委托???

  • 在现有的线程安全类中添加功能

客户端加锁机制

/**
 * 非线程安全的"若没有则添加"(不要这么做):
 *   问题在于程序在错误的锁上进行了同步。无论List使用哪一个锁来保护它的状态,
 *   可以确定的是,这个锁并不是ListHelper上的锁,这意味着putIfAbsent相对于
 *   List的其他操作来说并不是原子的,因此就无法确保putIfAbsent执行时另一个线程
 *   不会修改链表。
 *
 * @param <E>
 */
public class ListHelper<E> {
  public List<E> list = Collections.synchronizedList(new ArrayList<>());

  //...
  public synchronized boolean putIfAbsent(E x) {
    boolean absent = !list.contains(x);
    if (absent)
      list.add(x);
    return absent;
  }
  
}

/**
 * 要使得putIfAbsent()能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁。
 * 客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户端
 * 代码。要使用客户端加锁,必须指导对象X使用的是哪一个锁。
 *
 * @param <E>
 */
public class ListHelperBetter<E> {

  public List<E> list = Collections.synchronizedList(new ArrayList<>());

  public boolean putIfAbsent(E t) {
    synchronized (list) {
      boolean absent = !list.contains(t);
      if (absent)
        list.add(t);
      return absent;
    }
  }
}

基本同步方法

  • 同步容器类

同步容器类包括Vector和Hashtable,这些同步的封装器是由Collections.synchronizedXxx等工厂方法创建的。其实现方式是:将它们对状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

同步容器类的问题

迭代器与ConcurrentModificationException

同步容器类(或for-each循环)迭代都是采用迭代器Iterator的方式进行迭代。

迭代器处理并发修改时的机制:将计数器的变化与容器关联起来,如果在迭代器期间计数器被修改,那hasNext或next将抛出ConcurrentModificationException异常。

下面是for-each循环语法对List容器进行迭代的一个例子:

// 通过Iterator来迭代List
List<Widget> widgetList = Collections.synchronizedList(new ArrayList<Widget>());
// ...
// 可能抛出ConcurrentModificationException
for (Widget w : widgetList)
  doSomething(w);

隐藏迭代器
类似于下面这样:

/**
 * addTenthings方法可鞥会抛出ConcurrentModificationException,因为在生成调试信息过程中,
 * toString对容器进行迭代
 */
public class HiddenIterator {

  private final Set<Integer> set = new HashSet<>();

  public synchronized void add(Integer i) {
    set.add(i);
  }

  public synchronized void remove(Integer i) {
    set.remove(i);
  }

  public void addTenThings() {
    Random random = new Random();
    for (int i = 0; i < 10; i++)
      add(random.nextInt());
    System.out.println("DEBUG: added ten elements to " + set);
  }

  public static void main(String[] args) {
    HiddenIterator hiddenIterator = new HiddenIterator();
    hiddenIterator.addTenThings();
  }

}
  • 并发容器

通过并发容器类来改进同步容器的性能。

  • 因为同步容器将所有对容器状态对访问都串行化,以实现它们对线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器对锁时,吞吐量将严重降低。
  • 并发容器是针对多个线程并发访问设计的

ConcurrentHashMap

同步容器类在执行每个操作期间都持有一个锁;
ConcurrentHashMap使用一种粒度更细对加锁机制来实现更大程度共享,即分段锁,此时,任意数量的读取线程以及执行读取操作的线程和执行写入操作的线程都可以并发地访问Map

  • 同步工具类
    同步工具类可以是任何一个对象,只要它根据其自身的状态来协调控制线程的批量执行,阻塞队列以及其他类型(比如:信号量、栅栏以及闭锁)都可以作为同步工具类。

闭锁
闭锁:是一种约束多个线程在达到目标状态时同时启动的工具类
作用:相当于一扇门,在闭锁到达目标状态之前,没有一个线程会先启动,当达到目标状态时,闭锁限制打开,所有线程同时启动。
例子:CountDownLatch是一种灵活的闭锁实现,其中的countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,表示所有需要等待对事件都已经发生,在此之前,await会一直阻塞直到计数器为零,或者等待中的线程中断或等待超时。

/**
 * TestHarness创建一定数量的线程,利用它们并发地执行指定的任务。它使用两个闭锁,分别表示
 * "起始门"和"结束门"。起始门计数器的初始值为1,结束门计数器的初始值为工作线程的数量。每个
 * 工作线程首先要做的最后一件事情是将调用结束门的countDown方法减1
 */
public class TestHarness {

  public static long timeTasks(int nThreads, final Runnable task) throws InterruptedException {
    final CountDownLatch startGate = new CountDownLatch(1);
    final CountDownLatch endGate = new CountDownLatch(nThreads);

    for (int i = 0; i < nThreads; i++) {
      Thread t = new Thread() {
        public void run() {
          try {
            startGate.await();
            try {
              task.run();
            } finally {
              endGate.countDown();
            }
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      };
      t.start();
    }

    long start = System.nanoTime();
    startGate.countDown();
    endGate.await();
    long end = System.nanoTime();
    return end - start;
  }

}

FutureTask

FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于以下3种状态:等待运行,正在运行和运行完成。
FutureTask在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算。

信号量

计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。
Semaphore可以用于实现资源池,例如数据库连接池。当信号量为0时,所有请求资源的行为将被阻塞,直到信号量增加。
下面是一个使用Semaphore为容器设置边界的例子:

/**
 * 使用Semaphore将任何一种容器变成有界阻塞容器,信号量的计数值初始化为容器容量的最大值。
 * add操作在向底层容器中添加一个元素之前,首先要获取一个信号量,如果没有添加元素,则会立即释放一个信号量;
 * remove操作释放一个信号量,使更多的元素能够添加到容器中。
 * @param <T>
 */
public class BoundedHashSet<T> {
  private final Set<T> set;
  private final Semaphore sem;

  public BoundedHashSet(int bound) {
    this.set = Collections.synchronizedSet(new HashSet<>());
    sem = new Semaphore(bound);
  }

  public boolean add(T o) throws InterruptedException {
    sem.acquire();
    boolean wasAdded = false;
    try {
      wasAdded = set.add(o);
      return wasAdded;
    } finally {
      if (!wasAdded)
        sem.release();
    }
  }

  public boolean remove(Object o) {
    boolean wasRemoved = set.remove(o);
    if (wasRemoved)
      sem.release();
    return wasRemoved;
  }

}

栅栏
栅栏类似于闭锁,它能阻塞一组线程直到某个事情发生,或者等待一组相关的操作结束。闭锁是一次性对象,一旦进入终止状态,就不能重置。

栅栏与闭锁关键区别:所有线程必须同时到达栅栏位置,才能继续执行;闭锁用于等待事件,而栅栏用于等待其他线程。
什么时候使用栅栏:

  1. 通过栅栏的自动化计算来模拟细胞,为每个元素(细胞)都分配一个独立线程是不现实的,合理做法是将问题分解成一定数量的子问题,为每个子问题分配一个线程来进行求解,之后再将所有结果合并起来。

附录

  • 参考文章
    [1]. 《Java多线程并发程序实战》
    [2]. 分布式、多线程、高并发是什么

本文链接: http://www.dtmao.cc/news_show_200284.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?