做平面vi网站,谷歌seo推广服务,去哪个网站做试用好,电子商务网站建设的步骤本文是我们学院课程中名为Java Concurrency Essentials的一部分 。 在本课程中#xff0c;您将深入探讨并发的魔力。 将向您介绍并发和并发代码的基础知识#xff0c;并学习诸如原子性#xff0c;同步和线程安全之类的概念。 在这里查看 #xff01; 目录 1.简介 2.表现… 本文是我们学院课程中名为Java Concurrency Essentials的一部分 。 在本课程中您将深入探讨并发的魔力。 将向您介绍并发和并发代码的基础知识并学习诸如原子性同步和线程安全之类的概念。 在这里查看 目录 1.简介 2.表现 2.1。 阿姆达尔定律 2.2。 线程对性能的影响 2.3。 锁争用 1.简介 本文讨论了多线程应用程序的性能主题。 在定义了性能和可伸缩性这两个术语之后我们将仔细研究阿姆达尔定律。 在本课程的进一步内容中我们将看到如何通过应用不同的技术来减少锁争用如代码示例所示。 2.表现 线程可用于提高应用程序的性能。 其背后的原因可能是我们有多个可用的处理器或CPU内核。 每个CPU内核都可以执行自己的任务因此将大任务划分为一系列相互独立运行的较小任务可以改善应用程序的总运行时间。 这种性能提高的一个示例可以是调整硬盘上文件夹结构中的图像大小的应用程序。 单线程方法将仅遍历所有文件并逐个缩放每个图像。 如果我们的CPU具有多个内核则调整大小过程将仅利用可用内核之一。 例如多线程方法可以让生产者线程扫描文件系统并将所有找到的文件添加到队列中该队列由一堆工作线程处理。 当我们拥有与CPU内核一样多的工作线程时我们确保每个CPU内核都有所要做的事情直到处理完所有映像为止。 多线程可以提高应用程序整体性能的另一个示例是具有大量I / O等待时间的用例。 假设我们要编写一个应用程序以HTML文件的形式将完整的网站镜像到我们的硬盘上。 从一页开始应用程序必须遵循指向同一域或URL部分的所有链接。 从向远程Web服务器发出请求直到收到所有数据之间的时间可能很长我们可以将工作分配到几个线程上。 一个或多个线程可以解析接收到HTML页面并将找到的链接放入队列而其他线程可以将请求发送到Web服务器然后等待答案。 在这种情况下我们将等待时间用于新请求的页面以及已接收页面的解析。 与前面的示例相比如果我们添加的线程数超过了CPU内核的数量则此应用程序甚至可能会获得性能。 这两个例子表明性能意味着可以在更短的时间内完成更多的工作。 当然这是对术语“性能”的经典理解。 但是线程的使用也可以提高我们应用程序的响应速度。 想象一下简单的GUI应用程序它带有一个输入表单和一个“ Process”按钮。 当用户按下按钮时应用程序必须呈现被按下的按钮按钮应像被按下并在释放鼠标时再次升起一样锁定并且必须完成输入数据的实际处理。 如果此处理需要更长的时间则单线程应用程序将无法对进一步的用户输入做出反应即我们需要一个附加线程来处理来自操作系统的事件例如鼠标单击或鼠标指针移动。 可伸缩性是指程序通过向其添加更多资源来提高性能的能力。 想象一下我们将不得不调整大量图像的大小。 由于当前计算机的CPU内核数量有限因此添加更多线程并不能提高性能。 由于调度程序必须管理更多的线程因此性能甚至可能下降并且线程的创建和关闭也会消耗CPU功率。 阿姆达尔定律 最后一部分显示在某些情况下添加新资源可以提高应用程序的整体性能。 为了能够计算出当我们添加更多资源时应用程序可以获得多少性能我们需要确定程序中必须串行化/同步运行的部分以及程序中可以并行运行的部分。 如果我们表示必须与B同步运行的程序部分例如已同步执行的行数并且如果我们表示具有n的可用处理器数那么阿姆达尔定律就可以计算出加速的上限我们的应用程序可能能够实现 图1 如果我们让n接近无穷大则项1-B/ n收敛于零。 因此我们可以忽略该术语并且提速上限针对1 / B收敛其中B是优化之前程序运行时在不可并行代码中花费的分数。 例如如果B为0.5则意味着程序的一半不能并行化则0.5的倒数为2如果B为0.5则倒数为2。 因此即使我们向应用程序中添加无限数量的处理器我们也只能获得大约2倍的加速。 现在我们假设可以重写代码以便仅0.25的程序运行时花费在同步块中。 现在倒数0.25为4这意味着我们构建了一个可以在大量处理器上运行的应用程序其运行速度比仅一个处理器快四倍。 反之我们也可以使用阿姆达尔定律来计算程序运行时必须同步执行以达到给定加速比的分数。 如果我们想实现约100的加速则倒数是0.01这意味着我们应该只在同步代码中花费大约1的运行时。 总结来自阿姆达尔定律的发现我们可以得出结论通过使用附加处理器可以使程序获得的最大速度受到程序花费在同步代码部分中的时间的倒数的限制。 尽管在实践中计算该分数并不总是那么容易即使您考虑大型商业应用程序也不是一件容易的事但法律给我们的提示是我们必须非常仔细地考虑同步并且必须保留程序运行时的各个部分。小必须序列化。 线程对性能的影响 到目前为止本文的著作表明向应用程序添加更多线程可以提高性能和响应能力。 但是另一方面这不是免费的。 线程本身总是会对性能产生影响。 对性能的第一个影响是线程本身的创建。 这需要花费一些时间因为JVM必须从底层操作系统中获取线程的资源并准备调度程序中的数据结构该调度程序决定下一步执行哪个线程。 如果使用与处理器内核一样多的线程则每个线程都可以在自己的处理器上运行并且不会经常被中断。 实际上在您的应用程序运行时操作系统当然可能需要其自己的计算。 因此即使在这种情况下线程也会中断并且必须等到操作系统让它们再次运行。 当您必须使用比CPU内核更多的线程时情况变得更糟。 在这种情况下调度程序可以中断您的线程以便让另一个线程执行其代码。 在这种情况下必须保存正在运行的线程的当前状态必须还原应该在接下来运行的调度线程的状态。 除此之外调度程序本身还必须对其内部数据结构执行一些更新这些更新再次使用CPU功能。 总而言之这意味着每个上下文从一个线程切换到另一个线程会消耗CPU能力因此与单线程解决方案相比会导致性能下降。 具有多个线程的另一个成本是需要同步对共享数据结构的访问。 除了使用关键字sync我们还可以使用volatile在多个线程之间共享数据。 如果有多个线程争用结构化的共享数据那么我们就有争执。 然后JVM必须决定下一步执行哪个线程。 如果这不是当前线程则会引入上下文切换的成本。 然后当前线程必须等待直到可以获取锁为止。 JVM可以自行决定如何实现此等待。 与挂起线程并让另一个线程占用CPU时所需的上下文切换相比当直到可以获取该锁的预期时间很小时自旋等待即尝试一次又一次地获取锁可能比效率更高。 使等待线程重新执行需要另一个上下文切换并增加了锁争用的额外成本。 因此减少由于锁争用而必需的上下文切换的数量是合理的。 以下部分描述了两种减少此争用的方法。 锁争用 如上一节所述争用一个锁的两个或多个线程引入了额外的时钟周期因为争用可能迫使调度程序要么让一个线程旋转等待锁要么让另一个线程以占用处理器的代价占用处理器。两个上下文切换。 在某些情况下可以通过应用以下技术之一来减少锁争用 锁的范围减小了。 减少获取某个锁的次数。 使用硬件支持的乐观锁定操作而不是同步。 尽可能避免同步 避免对象池 2.3.1缩小范围 当锁的保持时间超过必要时间时可以应用第一种技术。 通常可以通过从同步块中移出一条或多条行来减少当前线程保持锁的时间来实现。 执行当前线程越早执行的代码行数越少则可以离开同步块从而让其他线程获得锁。 这也符合阿姆达尔定律因为我们减少了在同步块中花费的运行时间的比例。 为了更好地理解该技术请看下面的源代码 public class ReduceLockDuration implements Runnable {private static final int NUMBER_OF_THREADS 5;private static final MapString, Integer map new HashMapString, Integer();public void run() {for (int i 0; i 10000; i) {synchronized (map) {UUID randomUUID UUID.randomUUID();Integer value Integer.valueOf(42);String key randomUUID.toString();map.put(key, value);}Thread.yield();}}public static void main(String[] args) throws InterruptedException {Thread[] threads new Thread[NUMBER_OF_THREADS];for (int i 0; i NUMBER_OF_THREADS; i) {threads[i] new Thread(new ReduceLockDuration());}long startMillis System.currentTimeMillis();for (int i 0; i NUMBER_OF_THREADS; i) {threads[i].start();}for (int i 0; i NUMBER_OF_THREADS; i) {threads[i].join();}System.out.println((System.currentTimeMillis()-startMillis)ms);}
} 在此示例应用程序中我们让五个线程竞争访问共享Map。 为了一次只允许一个线程访问Map将访问Map并添加新的键/值对的代码放入同步块中。 当我们仔细查看该块时我们看到密钥的计算以及原始整数42到Integer对象的转换必须不同步。 从概念上讲它们属于访问Map的代码但它们在当前线程本地并且实例未被其他线程修改。 因此我们可以将它们移出同步块 public void run() {for (int i 0; i 10000; i) {UUID randomUUID UUID.randomUUID();Integer value Integer.valueOf(42);String key randomUUID.toString();synchronized (map) {map.put(key, value);}Thread.yield();}} 减少同步块会对可以测量的运行时间产生影响。 在我的机器上使用最小化同步块的版本将整个应用程序的运行时间从420ms减少到370ms。 仅通过将三行代码移出同步块就可以使运行时间总共减少11。 引入Thread.yield()语句是为了引起更多的上下文切换因为此方法调用告诉JVM当前线程愿意将处理器提供给另一个等待线程。 这又引发了更多的锁争用否则一个线程可能在没有任何竞争线程的情况下在处理器上运行太长时间。 2.3.2锁拆分 减少锁争用的另一种技术是将一个锁拆分为多个较小范围的锁。 如果您有一个锁来保护应用程序的不同方面则可以应用此技术。 假定我们要收集有关应用程序的一些统计数据并实现一个简单的计数器类该计数器类在每个方面都保留一个原始计数器变量。 由于我们的应用程序是多线程的因此必须同步访问这些变量因为它们是从不同的并发线程访问的。 最简单的方法是在Counter的每个方法的方法签名中使用synced关键字 public static class CounterOneLock implements Counter {private long customerCount 0;private long shippingCount 0;public synchronized void incrementCustomer() {customerCount;}public synchronized void incrementShipping() {shippingCount;}public synchronized long getCustomerCount() {return customerCount;}public synchronized long getShippingCount() {return shippingCount;}} 这种方法还意味着计数器的每个增量都会锁定Counter的整个实例。 其他要增加其他变量的线程必须等待直到释放此单个锁。 在这种情况下更有效的方法是为每个计数器使用单独的锁如下例所示 public static class CounterSeparateLock implements Counter {private static final Object customerLock new Object();private static final Object shippingLock new Object();private long customerCount 0;private long shippingCount 0;public void incrementCustomer() {synchronized (customerLock) {customerCount;}}public void incrementShipping() {synchronized (shippingLock) {shippingCount;}}public long getCustomerCount() {synchronized (customerLock) {return customerCount;}}public long getShippingCount() {synchronized (shippingLock) {return shippingCount;}}} 此实现引入了两个单独的同步对象每个计数器一个。 因此试图增加我们系统中的客户数量的线程只需要与其他线程竞争而其他线程也可以增加客户数量但是它不必与试图增加发货数量的线程竞争。 通过使用以下类我们可以轻松衡量此锁拆分的影响 public class LockSplitting implements Runnable {private static final int NUMBER_OF_THREADS 5;private Counter counter;public interface Counter {void incrementCustomer();void incrementShipping();long getCustomerCount();long getShippingCount();}public static class CounterOneLock implements Counter { ... }public static class CounterSeparateLock implements Counter { ... }public LockSplitting(Counter counter) {this.counter counter;}public void run() {for (int i 0; i 100000; i) {if (ThreadLocalRandom.current().nextBoolean()) {counter.incrementCustomer();} else {counter.incrementShipping();}}}public static void main(String[] args) throws InterruptedException {Thread[] threads new Thread[NUMBER_OF_THREADS];Counter counter new CounterOneLock();for (int i 0; i NUMBER_OF_THREADS; i) {threads[i] new Thread(new LockSplitting(counter));}long startMillis System.currentTimeMillis();for (int i 0; i NUMBER_OF_THREADS; i) {threads[i].start();}for (int i 0; i NUMBER_OF_THREADS; i) {threads[i].join();}System.out.println((System.currentTimeMillis() - startMillis) ms);}
} 在我的机器上使用一个锁的实现平均大约需要56ms而使用两个锁的实现大约需要38ms。 这减少了约32。 另一个可能的改进是通过区分读和写锁来进一步分离锁。 例如 Counter类提供用于读取和写入计数器值的方法。 虽然读取当前值可以由多个线程并行完成但所有写入操作都必须序列化。 java.util.concurrent包提供了此类ReadWriteLock的即用型实现。 ReentrantReadWriteLock实现管理两个单独的锁。 一种用于读访问一种用于写访问。 读锁定和写锁定都提供了用于锁定和解锁的方法。 仅当没有读锁时才获取写锁。 只要不获取写锁就可以在读取器线程上获取读锁。 为了演示起见以下显示了使用ReadWriteLock的计数器类的实现 public static class CounterReadWriteLock implements Counter {private final ReentrantReadWriteLock customerLock new ReentrantReadWriteLock();private final Lock customerWriteLock customerLock.writeLock();private final Lock customerReadLock customerLock.readLock();private final ReentrantReadWriteLock shippingLock new ReentrantReadWriteLock();private final Lock shippingWriteLock shippingLock.writeLock();private final Lock shippingReadLock shippingLock.readLock();private long customerCount 0;private long shippingCount 0;public void incrementCustomer() {customerWriteLock.lock();customerCount;customerWriteLock.unlock();}public void incrementShipping() {shippingWriteLock.lock();shippingCount;shippingWriteLock.unlock();}public long getCustomerCount() {customerReadLock.lock();long count customerCount;customerReadLock.unlock();return count;}public long getShippingCount() {shippingReadLock.lock();long count shippingCount;shippingReadLock.unlock();return count;}} 所有读访问都通过获取读锁来保护而所有写访问都通过相应的写锁来保护。 如果应用程序使用的读取访问次数比写入访问次数多则这种实现甚至可以比以前的实现获得更多的性能改进因为所有读取线程都可以并行访问getter方法。 2.3.3锁条 前面的示例演示了如何将一个锁分为两个单独的锁。 这允许竞争线程仅获取保护他们要操纵的数据结构的锁。 另一方面如果未正确实施此技术还会增加复杂性和死锁的风险。 另一方面锁条是一种类似于锁拆分的技术。 我们没有拆分一个保护不同代码部分或方面的锁而是对不同的值使用了不同的锁。 JDK的java.util.concurrent包中的ConcurrentHashMap类使用此技术来提高严重依赖HashMap的应用程序的性能。 与java.util.HashMap的同步版本相反 ConcurrentHashMap使用16个不同的锁。 每个锁仅保护可用哈希桶的1/16。 这允许希望将数据插入可用哈希桶的不同部分的不同线程同时执行此操作因为它们的操作由不同的锁保护。 另一方面它也引入了为特定操作获取多个锁的问题。 例如如果要复制整个地图则必须获取所有16个锁。 2.3.4原子操作 减少锁争用的另一种方法是使用所谓的原子操作。 以下文章之一将详细解释和评估此原理。 java.util.concurrent包为某些原始数据类型提供了对原子操作的支持。 原子操作是使用处理器提供的所谓的“比较和交换”CAS操作实现的。 如果当前值等于提供的值则CAS指令仅更新某个寄存器的值。 仅在这种情况下旧值才被新值替换。 该原理可用于乐观地增加变量。 如果我们假设线程知道当前值那么它可以尝试使用CAS操作将其递增。 如果事实证明另一个线程同时增加了该值而我们的值不再是当前值则我们请求当前值然后重试。 这可以完成直到我们成功增加计数器。 尽管我们可能需要一些旋转但此实现的优点是我们不需要任何类型的同步。 Counter类的以下实现使用原子变量方法并且不使用任何同步块 public static class CounterAtomic implements Counter {private AtomicLong customerCount new AtomicLong();private AtomicLong shippingCount new AtomicLong();public void incrementCustomer() {customerCount.incrementAndGet();}public void incrementShipping() {shippingCount.incrementAndGet();}public long getCustomerCount() {return customerCount.get();}public long getShippingCount() {return shippingCount.get();}} 与CounterSeparateLock类相比平均总运行时间从39ms减少到16ms。 运行时间减少了约58。 2.3.5避免热点 列表的典型实现将在内部管理一个计数器该计数器保存列表中的项目数。 每当有新项目添加到列表或从列表中删除时此计数器都会更新。 如果在单线程应用程序中使用则此优化是合理的因为列表上的size()操作将直接返回先前计算的值。 如果列表不包含列表中的项目数则size()操作将必须遍历所有项目才能进行计算。 在许多数据结构中常见的优化可能会在多线程应用程序中成为问题。 假设我们想与一堆线程共享该列表的实例这些线程可以从列表中插入和删除项目并查询其大小。 现在counter变量也是共享资源必须同步对其值的所有访问。 计数器已成为实施中的热点。 下面的代码段演示了此问题 public static class CarRepositoryWithCounter implements CarRepository {private MapString, Car cars new HashMapString, Car();private MapString, Car trucks new HashMapString, Car();private Object carCountSync new Object();private int carCount 0;public void addCar(Car car) {if (car.getLicencePlate().startsWith(C)) {synchronized (cars) {Car foundCar cars.get(car.getLicencePlate());if (foundCar null) {cars.put(car.getLicencePlate(), car);synchronized (carCountSync) {carCount;}}}} else {synchronized (trucks) {Car foundCar trucks.get(car.getLicencePlate());if (foundCar null) {trucks.put(car.getLicencePlate(), car);synchronized (carCountSync) {carCount;}}}}}public int getCarCount() {synchronized (carCountSync) {return carCount;}}} CarRepository实现包含两个列表一个用于汽车一个用于卡车。 它还提供了一种返回两个列表中当前汽车和卡车数量的方法。 作为优化每次将新车添加到两个列表之一时它都会增加内部计数器。 该操作必须与专用的carCountSync实例同步。 返回计数值时将使用相同的同步。 为了摆脱这种额外的同步中 CarRepository本来也可以实现通过省略额外的计数器和每个值是通过调用查询时间计算总的汽车数量getCarCount() public static class CarRepositoryWithoutCounter implements CarRepository {private MapString, Car cars new HashMapString, Car();private MapString, Car trucks new HashMapString, Car();public void addCar(Car car) {if (car.getLicencePlate().startsWith(C)) {synchronized (cars) {Car foundCar cars.get(car.getLicencePlate());if (foundCar null) {cars.put(car.getLicencePlate(), car);}}} else {synchronized (trucks) {Car foundCar trucks.get(car.getLicencePlate());if (foundCar null) {trucks.put(car.getLicencePlate(), car);}}}}public int getCarCount() {synchronized (cars) {synchronized (trucks) {return cars.size() trucks.size();}}}} 现在我们需要与getCarCount()方法中的汽车和卡车列表进行同步并计算大小但是getCarCount()添加新汽车期间的额外同步。 2.3.6避免对象池 在Java VM对象的第一个版本中使用new运算符创建仍然是一项昂贵的操作。 这使许多程序员采用了对象池的通用模式。 他们没有一次又一次地创建某些对象而是构造了这些对象的池每次需要一个实例时都会从池中获取一个实例。 使用完对象后将其放回池中并可以由另一个线程使用。 乍看之下在多线程应用程序中使用时可能会遇到问题。 现在对象池在所有线程之间共享并且必须同步对池中对象的访问。 现在这种额外的同步开销可能大于对象创建本身的开销。 当您考虑垃圾收集器收集新创建的对象实例的额外费用时甚至是这样。 与所有性能优化一样此示例再次说明在应用每种可能的改进之前应仔细评估它们。 乍一看似乎很有意义的优化在没有正确实施的情况下甚至可能成为性能瓶颈。 翻译自: https://www.javacodegeeks.com/2015/09/performance-scalability-and-liveness.html