Java多线程编程 Java多线程是如何保证Java类的安全

教程分享 > Java教程 (1926) 2024-04-17 12:33:22

引言

线程安全是像Java这样的语言/平台中的类的重要质量,我们经常在线程之间共享对象。由于缺乏线程安全性而导致的问题非常难以调试,因为它们零星且几乎不可能有意再现。你如何测试你的对象以确保它们是线程安全的?这是我如何做的。



这里我们用一个书(book)类来说明
public class Books {
  final Map<Integer, String> map =
    new ConcurrentHashMap<>();
  int add(String title) {
    final Integer next = this.map.size() + 1;
    this.map.put(next, title);
    return next;
  }
  String title(int id) {
    return this.map.get(id);
  }
}
首先,我们在那里放置一本书,书架返回它的ID。 然后我们可以通过它的ID读取书名:
Books books = new Books();
String title = "Elegant Objects";
int id = books.add(title);
assert books.title(id).equals(title);

这个类似乎是线程安全的,因为我们使用线程安全的ConcurrentHashMap而不是更原始的和非线程安全的HashMap,对吧? 我们来测试一下:
class BooksTest {
  @Test
  public void addsAndRetrieves() {
    Books books = new Books();
    String title = "Elegant Objects";
    int id = books.add(title);
    assert books.title(id).equals(title);
  }
}
测试通过,但它只是一个单线程测试。让我们尝试从几个并行线程(我使用Hamcrest)做同样的操作:
class BooksTest {
  @Test
  public void addsAndRetrieves() {
    Books books = new Books();
    int threads = 10;
    ExecutorService service =
      Executors.newFixedThreadPool(threads);
    Collection<Future<Integer>> futures =
      new ArrayList<>(threads);
    for (int t = 0; t < threads; ++t) {
      final String title = String.format("Book #%d", t);
      futures.add(service.submit(() -> books.add(title)));
    }
    Set<Integer> ids = new HashSet<>();
    for (Future<Integer> f : futures) {
      ids.add(f.get());
    }
    assertThat(ids.size(), equalTo(threads));
  }
}

首先,我通过创建一个线程池Executors。然后我提交类型十个对象Callable通过submit()。他们每个人都会在书架上添加一本新的独特书籍。所有这些都将以某种不可预知的顺序由池中的这10个线程中的一些执行。

然后,我通过类型对象列表获取其执行者的结果Future。最后,我计算创建的唯一书籍ID数量。如果数字是10,则不存在冲突。我正在使用该Set集合以确保ID列表仅包含唯一元素。

测试通过我的笔记本电脑。但是,它不够强大。这里的问题是,它不是真的Books从多个并行线程中进行测试。在我们的呼叫之间传递的时间submit()足够大,可以完成执行books.add()。这就是为什么现实中只有一个线程会同时运行。我们可以通过修改代码来检查一下:

AtomicBoolean running = new AtomicBoolean();
AtomicInteger overlaps = new AtomicInteger();
Collection<Future<Integer>> futures =
  new ArrayList<>(threads);
for (int t = 0; t < threads; ++t) {
  final String title = String.format("Book #%d", t);
  futures.add(
    service.submit(
      () -> {
        if (running.get()) {
          overlaps.incrementAndGet();
        }
        running.set(true);
        int id = books.add(title);
        running.set(false);
        return id;
      }
    )
  );
}
assertThat(overlaps.get(), greaterThan(0));

有了这段代码,我试着看看线程多长时间重复一次,并行执行一些操作。这绝不会发生,并且overlaps等于零。因此,我们的测试并没有真正测试任何东西。它只是将十本书逐个添加到书架上。如果我将线程数量增加到1000,它们有时会开始重叠。但是我们希望它们重叠,即使只有少数它们。为了解决这个问题,我们需要使用CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean running = new AtomicBoolean();
AtomicInteger overlaps = new AtomicInteger();
Collection<Future<Integer>> futures =
  new ArrayList<>(threads);
for (int t = 0; t < threads; ++t) {
  final String title = String.format("Book #%d", t);
  futures.add(
    service.submit(
      () -> {
        latch.await();
        if (running.get()) {
          overlaps.incrementAndGet();
        }
        running.set(true);
        int id = books.add(title);
        running.set(false);
        return id;
      }
    )
  );
}
latch.countDown();
Set<Integer> ids = new HashSet<>();
for (Future<Integer> f : futures) {
  ids.add(f.get());
}
assertThat(overlaps.get(), greaterThan(0));
 

现在,每个线程在触摸书籍之前,都会等待给予的许可latch。当我们通过submit()他们全部提交他们并等待。然后我们释放闩锁,countDown()他们都开始同时进行。现在,在我的笔记本电脑中,overlaps即使threads是10 时,也等于3-5 。

最后一次assertThat()崩溃!我没有像以前那样获得10本书ID。这是7-9,但从来没有10.班,显然,是不是线程安全的!

但在我们修复课程之前,让我们来简化测试。让我们用RunInThreads从Cactoos,这确实是我们在前面已经做了完全一样的,但引擎盖下:

class BooksTest {
  @Test
  public void addsAndRetrieves() {
    Books books = new Books();
    MatcherAssert.assertThat(
      t -> {
        String title = String.format(
          "Book #%d", t.getAndIncrement()
        );
        int id = books.add(title);
        return books.title(id).equals(title);
      },
      new RunsInThreads<>(new AtomicInteger(), 10)
    );
  }
}
 

第一个参数assertThat()Func(一个功能接口)的一个实例,接受AtomicInteger(的第一个参数RunsInThreads)并返回Boolean。该函数将在10个并行线程上执行,使用上面演示的相同的基于锁存器的方法。

RunInThreads似乎是紧凑和方便的,我已经在一些项目中使用它。

顺便说一句,为了使Books线程安全,我们只需要添加synchronized到它的方法add()



 
https://www.leftso.com/article/376.html

相关文章
java多线程编程_java多线程安全_java多线程实现安全锁CAS机制,CAS在java多线程中相当于数据库的乐观锁,synchronized相当于数据库的乐观锁。
Java基础多线程之主线程等待子线程结束,Java基础编程之多线程入门学习篇。主要讲解几种方法来实现Java多线程中主线程等待子线程结束的最快方式。
线程安全是像Java这样的语言/平台中的类的重要质量,我们经常在线程之间共享对象。由于缺乏线程安全性而导致的问题非常难以调试,因为它们零星且几乎不可能有意再现。你如何测试你的对象以确保它们是线程...
Java多线程生命周期
本文将讲述排队锁的使用场景,什么情况适合使用排队锁?Java 怎么使用排队锁?
本文将讲述CLH锁的使用场景,什么情况适合使用CLH锁?Java 怎么使用CLH锁?
本文将讲述什么是自旋锁?自旋锁的使用场景,什么情况适合使用自旋锁?Java 怎么使用自旋锁?
本文将讲述MCS锁的使用场景,什么情况适合使用MCS锁?Java 怎么使用MCS锁?
spring boot 2.0 入门之单元测试多线程。spring boot 2.0 项目含多线程异步处理业务单元测试执行主线程结束不等待子线程结束。
1. 线程的安全性问题:线程安全和非线程安全: 一个类在单线程环境下能够正常运行,并且在多线程环境下,使用方不做特别处理也能运行正常,我们就称其实线程安全的
问题描述最近写JavaFX程序遇到了下面的错误:Exception in thread "pool-2-thread-1" java.lang.IllegalStateException: No...
线程池创建 /** * 队列用线程 * @return */ @Bean(name = "queuePool") public Thread...
Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。
垃圾回收(GC)一直是Java受欢迎背后的重要特性之一。垃圾回收是Java中用于释放未使用的内存的机制。本质上,它追踪所有仍在使用的对象,并将剩下的标记为垃圾。Java的垃圾回收被认为是一种自动...