Java Concurrency in the common pitfalls

 

You Rui core courses through java study notes, we can see that a lot of the code of professional knowledge, learning to share your reference.

1 Introduction

In this tutorial, we will see some of Java's most common concurrency problems. We will also learn how to avoid them and the main reason.

2. Using thread-safe objects

2.1. Shared Object

The main thread to communicate by sharing access to the same object. Therefore, when the object is read changes may produce unexpected results. Also, while it may make changes to the object in a state of damage or inconsistent.

The main way we avoid this concurrency issues and build reliable code is to use immutable objects. This is because their state can not be modified by multiple threads of interference.

But we can not always use immutable objects. In these cases, we must find the variable objects to be thread-safe method.

2.2. The set of thread-safe

Like any other object, internally maintains a collection of state. This is accomplished by multiple threads simultaneously change the set of changes. Therefore, we can in a multithreaded environment, a method for the safe use of the collection is synchronized them:

1

2

Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

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

Typically, synchronization will help us achieve mutually exclusive. More specifically, these collections can only be accessed by a thread. Therefore, we can avoid the collection in an inconsistent state.

2.3. Experts multi-threaded collection

Let us now consider a scenario, we need more to read rather than write. Synchronous collection by using our application performance might suffer significant consequences. If two threads to read collections simultaneously, a thread must wait for another thread to complete.

Therefore, the Java offers concurrent collections, such as CopyOnWriteArrayList and ConcurrentHashMap, can be accessed simultaneously by multiple threads:

1

2

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

Map<String, String> map = new ConcurrentHashMap<>();

CopyOnWriteArrayList for the operation such as adding or deleting a variable like to create a separate copy of the underlying array to achieve thread safety through. Despite its write performance is worse than Collections.synchronizedList, but when we need to read than write for a long time, it provides better performance for us.

It is thread-safe on ConcurrentHashMap nature, and higher performance than Collections.synchronizedMap wrapper around non-thread-safe Map of. It is actually mapped security threads in security mapping allows different activities take place at the same time his son mapping.

2.4 The use of non-thread-safe type

We often use such as built-in objects like SimpleDateFormat to parse and format dates objects. SimpleDateFormat class when performing actions change its internal state.

We need to be very careful, because they are not thread-safe. Due to race conditions, their state may become inconsistent in multi-threaded applications.

So how do we safely use SimpleDateFormat? We have several options:

  • • Create a new instance of the use of each SimpleDateFormat
  • Limit the use of the number of objects ThreadLocal <SimpleDateFormat> object created. It ensures that each thread has its own instance of SimpleDateFormat
  • Use keywords or lock synchronization to synchronize concurrent access by multiple threads

SimpleDateFormat only one example of them. We can use these techniques for any non-thread-safe type.

3. Conditions of Competition

When two or more threads access to shared data and they are trying to change them at the same time, a race condition occurs. Therefore, a race condition may cause run-time error or unexpected results.

3.1. Examples of conditions race

Let us consider the following code:

1

2

3

4

5

6

7

8

9

10

11

class Counter {

    private int counter = 0;

 

    public void increment() {

        counter++;

    }

 

    public int getValue() {

        return counter;

    }

}

Counter Each call class is designed such that an incremental approach will be applied to the counter. However, if you reference a Counter object from multiple threads, the interference between threads may prevent this from happening as expected.

We can Counter ++ statement is broken down into three steps:

  • Retrieves the current value of the counter
  • · The retrieved value is increased by 1
  • · The increase in the value of the storage counter back

Now, let us assume that two threads, Thread1 and thread2, while calling the incremental approach. They staggered action may follow in the following order:

  • · Thread1 read the current value of the counter; 0
  • · Thread2 read the current value of the counter; 0
  • · Thread1 increase retrieved value; the result is 1
  • · Thread2 increase retrieved value; the result is 1
  • · Thread1 stores the result in the counter; now the result is 1

· Thread2 stores the result in the counter; now the result is 1

3.2 based synchronization solution

We can resolve inconsistencies by synchronizing key code:

1

2

3

4

5

6

7

8

9

10

11

class SynchronizedCounter {

    private int counter = 0;

 

    public synchronized void increment() {

        counter++;

    }

 

    public synchronized int getValue() {

        return counter;

    }

}

Allow only one thread synchronization method of the object of use, so this forces the read and write consistent counter.

3.3. Built-in solutions

We can replace the above code is built AtomicInteger object. This class provides methods other atoms except for an increase integer, is a better solution than writing your own code. Therefore, we can call its methods without the need for synchronization:

1

2

AtomicInteger atomicInteger = new AtomicInteger(3);

atomicInteger.incrementAndGet();

在这种情况下,SDK可以为我们解决问题。 否则,我们也可以编写自己的代码,将关键部分封装在自定义线程安全的类中。 这种方法有助于我们最大程度地减少代码的复杂性并最大程度地提高代码的可重用性。

4.收藏品的比赛条件

4.1. 问题

我们可以陷入的另一个陷阱是,认为同步收集比实际提供的保护更多。

让我们检查下面的代码:

1

2

3

4

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

if(!list.contains("foo")) {

    list.add("foo");

}

我们列表的每个操作都是同步的,但是多个方法调用的任何组合都不会同步。 更具体地说,在两个操作之间,另一个线程可以修改我们的集合,从而导致不良结果。

例如,两个线程可以同时进入if块,然后更新列表,每个线程将foo值添加到列表中。

4.2。 列表解决方案

我们可以使用同步保护代码避免一次被多个线程访问:

1

2

3

4

5

synchronized (list) {

    if (!list.contains("foo")) {

        list.add("foo");

    }

}

我们没有在功能中添加同步关键字,而是创建了一个与列表有关的关键部分,该部分一次只允许一个线程执行此操作。

我们应该注意,我们可以在list对象的其他操作上使用synchronized(list),以保证一次只有一个线程可以对此对象执行任何操作。

4.3。 内置解决方案

对于ConcurrentHashMap

现在,出于相同的原因,考虑使用地图,即仅在不存在时才添加条目。

ConcurrentHashMap为此类问题提供了更好的解决方案。 我们可以使用其原子的ifIfAbsent方法:for ConcurrentHashMap

1

2

Map<String, String> map = new ConcurrentHashMap<>();

map.putIfAbsent("foo", "bar");

或者,如果我们想计算该值,则使用其原子的computeIfAbsent方法:

1

map.computeIfAbsent("foo", key -> key + "bar");

我们应该注意,这些方法是Map接口的一部分,它们提供了一种便捷的方法来避免围绕插入编写条件逻辑。 当尝试进行多线程调用时,它们确实可以帮助我们。

5.内存一致性问题

当多个线程对应为相同数据的视图不一致时,将发生内存一致性问题。

根据Java内存模型,除主内存(RAM)外,每个CPU都有自己的缓存。 因此,任何线程都可以缓存变量,因为与主内存相比,它提供了更快的访问。

5.1。 问题

让我们回想一下我们的Counter示例:

1

2

3

4

5

6

7

8

9

10

11

class Counter {

    private int counter = 0;

 

    public void increment() {

        counter++;

    }

 

    public int getValue() {

        return counter;

    }

}

让我们考虑以下情形:线程1递增计数器,然后线程2读取其值。 可能会发生以下事件序列:

  • ·thread1从其自己的缓存中读取计数器值; 计数器为0
  • ·thread1递增计数器并将其写回到其自己的缓存中; 计数器是1
  • ·thread2从其自己的缓存中读取计数器值; 计数器为0

当然,预期的事件顺序也可能发生,并且thread2将读取正确的值(1),但是不能保证一个线程所做的更改每次都会对其他线程可见。

5.2。 解决方案

为了避免内存一致性错误,我们需要建立事前发生的关系。 这种关系只是对一个特定语句的内存更新对另一特定语句可见的保证。

有几种策略可以创建事前发生的关系。 其中之一是同步,我们已经介绍过了。

同步可确保互斥和内存一致性。 但是,这会带来性能成本。

我们还可以通过使用volatile关键字来避免内存一致性问题。 简而言之,对volatile变量的任何更改始终对其他线程可见。

让我们使用volatile重写我们的Counter示例:

1

2

3

4

5

6

7

8

9

10

11

class SyncronizedCounter {

    private volatile int counter = 0;

 

    public synchronized void increment() {

        counter++;

    }

 

    public int getValue() {

        return counter;

    }

}

我们应该注意,我们仍然需要同步增量操作,因为volatile不能确保我们相互排斥。 使用简单的原子变量访问比通过同步代码访问这些变量更有效。

6.滥用同步

同步机制是实现线程安全的强大工具。 它依赖于内部和外部锁的使用。 我们还记得以下事实:每个对象都有一个不同的锁,一次只能有一个线程获得一个锁。

但是,如果我们不注意并为关键代码仔细选择正确的锁,则可能会发生意外行为。

6.1。 同步此参考

方法级同步是许多并发问题的解决方案。 但是,如果使用过多,它也可能导致其他并发问题。 这种同步方法依赖于此引用作为锁,也称为内在锁。

我们可以在以下示例中看到如何将这个引用作为锁将方法级同步转换为块级同步。

这些方法是等效的:

1

2

3

public synchronized void foo() {

    //...

}

 

1

2

3

4

5

public void foo() {

    synchronized(this) {

      //...

    }

}

当线程调用这种方法时,其他线程无法同时访问该对象。 由于所有操作最终都以单线程运行,因此这可能会降低并发性能。 当读取的对象多于更新的对象时,此方法特别糟糕。

此外,我们代码的客户也可能会获得此锁。 在最坏的情况下,此操作可能导致死锁。

6.2。 僵局

死锁描述了两个或多个线程相互阻塞,每个线程都等待获取某个其他线程拥有的资源的情况。

让我们考虑示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

public class DeadlockExample {

 

    public static Object lock1 = new Object();

    public static Object lock2 = new Object();

 

    public static void main(String args[]) {

        Thread threadA = new Thread(() -> {

            synchronized (lock1) {

                System.out.println("ThreadA: Holding lock 1...");

                sleep();

                System.out.println("ThreadA: Waiting for lock 2...");

 

                synchronized (lock2) {

                    System.out.println("ThreadA: Holding lock 1 & 2...");

                }

            }

        });

        Thread threadB = new Thread(() -> {

            synchronized (lock2) {

                System.out.println("ThreadB: Holding lock 2...");

                sleep();

                System.out.println("ThreadB: Waiting for lock 1...");

 

                synchronized (lock1) {

                    System.out.println("ThreadB: Holding lock 1 & 2...");

                }

            }

        });

        threadA.start();

        threadB.start();

    }

}

在上面的代码中,我们可以清楚地看到第一个线程A获取lock1和线程B获取lock2。 然后,线程A尝试获取已由线程B获取的lock2,并且线程B尝试获取已由线程A获取的lock1。 因此,他们两个都不会继续前进,这意味着他们陷入了僵局。

我们可以通过更改其中一个线程的锁定顺序来轻松解决此问题。

我们应该注意,这只是一个例子,还有许多其他例子可能导致僵局。

7.结论

在本文中,我们探讨了在多线程应用程序中可能遇到的并发问题的几个示例。

首先,我们了解到我们应该选择不可变或线程安全的对象或操作。

然后,我们看到了一些竞争条件的示例,以及如何使用同步机制避免它们。 此外,我们了解了与内存相关的竞争条件以及如何避免它们。

尽管同步机制可以帮助我们避免许多并发问题,但是我们可以轻松地滥用它并创建其他问题。 因此,我们研究了这种机制使用不当时可能会遇到的几个问题。

 

 

> 喜欢这篇文章的可以点个赞,欢迎大家留言评论,记得关注我,每天持续更新技术干货、职场趣事、海量面试资料等等
 > 如果你对java技术很感兴趣也可以交流学习,共同学习进步。 
> 不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代

 

文章写道这里,欢迎完善交流。最后奉上近期整理出来的一套完整的java架构思维导图,分享给大家对照知识点参考学习。有更多JVM、Mysql、Tomcat、Spring Boot、Spring Cloud、Zookeeper、Kafka、RabbitMQ、RockerMQ、Redis、ELK、Git等Java干货

 

Guess you like

Origin www.cnblogs.com/youruike1/p/12503787.html