本文是我们学院课程中名为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 Map<String, Integer> map = new HashMap<String, 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 Map<String, Car> cars = new HashMap<String, Car>();private Map<String, Car> trucks = new HashMap<String, 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 Map<String, Car> cars = new HashMap<String, Car>();private Map<String, Car> trucks = new HashMap<String, 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

性能,可伸缩性和活力相关推荐

  1. mysql大规模读写性能_十招搞定 MySQL 大规模数据库的性能和伸缩性优化

    点击图片报名参加广州&珠海源创会 在需要支持移动/平板电脑应用及普通桌面浏览器访问的时代,网站的普及率和有效性很大程度上取决于其可用性和性能.一个访问缓慢的网站会使得访问者或潜在的客户流失,并 ...

  2. 十招搞定 MySQL 大规模数据库的性能和伸缩性优化

    在需要支持移动/平板电脑应用及普通桌面浏览器访问的时代,网站的普及率和有效性很大程度上取决于其可用性和性能.一个访问缓慢的网站会使得访问者或潜在的客户流失,并导致商业的失败.一个访问速度相当快的网站将 ...

  3. 高性能 高可用 可弹性伸缩_性能,可伸缩性和活力

    高性能 高可用 可弹性伸缩 本文是我们名为Java Concurrency Essentials的学院课程的一部分. 在本课程中,您将深入探讨并发的魔力. 将向您介绍并发和并发代码的基础知识,并学习诸 ...

  4. Reporting Services 的伸缩性和性能表现规划(转载)

    简介 Microsoft? SQL Server? Reporting Services 是一个将集中管理的报告服务器具有的伸缩性和易管理性与基于 Web 和桌面的报告交付手段集于一身的报告平台.Re ...

  5. 性能调优之Java系统级性能监控及优化

    性能调优之Java系统级性能监控及优化 对于性能调优而言,通常我们需要经过以下三个步骤:1,性能监控:2,性能剖析:3,性能调优 性能调优:通过分析影响Application性能问题根源,进行优化Ap ...

  6. 如何优化 Java 性能?

    对于 Java 性能比较关心的同学大概都知道<Java Performance>这本书,一般而言,很多同学在日常写 Java Code 的时候很少去关心性能问题,但是在我们写 Code 的 ...

  7. PHP性能调优---PHP-FPM配置及使用总结

    转载至: https://www.cnblogs.com/kenshinobiy/p/7470635.html PHP-FPM配置及使用总结: php-FPM是一个PHP FastCGI的管理器,它实 ...

  8. IIS6下PHP的ISAPI和FastCGI性能比较 期待ii7

    在Windows IIS 6.0下配置PHP,通常有CGI.ISAPI和FastCGI三种配置方式,这三种模式都可以在IIS 6.0下成功运行,下面我就讲一下这三种方式配置的区别和性能上的差异. 1. ...

  9. (转)Windows 性能监视器工具-perfmon

    Windows 性能监视器工具 如果需要在一台计算机上监视多个 Report Server 实例,可以同时或单独监视这些实例.选择要包括的实例是计数器添加过程的一部分.有关使用 Windows 附带的 ...

最新文章

  1. 算法代码中的循环矩阵在哪体现_循环移位可视化理解
  2. 【C++】【TinyXml】xml文件的读写功能使用——写xml文件
  3. 2021暑假实习-SSM超市积分管理系统-day08笔记
  4. json 和 table控件
  5. P2717-寒假作业【逆序对,树状数组】
  6. 【渝粤教育】国家开放大学2018年秋季 0699-22T阅读与写作 参考试题
  7. ECS主动运维2.0,体验升级,事半功倍
  8. unoconv执行出错unable to connect or start own listener
  9. Python中应该使用%还是format来格式化字符串?
  10. 一加7T Pro曝光:10月10日发布
  11. 工欲善其事必先利其器,用Emmet提高HTML编写速度
  12. 【区间dp】【Gym 100712L】01串切割问题 + 线段树优化
  13. 永磁同步电机的原理介绍
  14. Word多级列表不显示和奇数页显示章节标题问题
  15. 我的推荐系统学习之路
  16. CentOS下 Meld安装(文件和文件夹比较)的两种方式
  17. Centos清理内存 内存回收释放及内存使用查看的相关命令
  18. P1658 购物(贪心算法)
  19. kafka踩坑、实践篇
  20. JavaWeb EMS员工信息管理系统(servlet+jsp版本)

热门文章

  1. [编程入门]带参数宏定义练习:定义一个带参的宏,使两个参数的值互换,并写出程序,输入两个数作为使用宏时的实参。输出已交换后的两个值。
  2. idea打war的问题
  3. 前后端分离项目部署上线详细教程
  4. 使用Servlet上传多张图片——实体层(ProductInfo.java)
  5. RPC远程过程调用之Hessian 基于HTTP
  6. springboot:BeanPostProcessor示例及分析
  7. redis集群搭建报错-(error) CLUSTERDOWN The cluster is down
  8. java安全——数字签名+代码签名
  9. 八爪鱼 是java做的吗_章鱼扫描仪:Java构建工具和恶意软件
  10. 用jackson转json_用Jackson编写大JSON文件