Home  >  Article  >  Java  >  Java Programming Thoughts Learning Class (8) Chapter 21 - Concurrency

Java Programming Thoughts Learning Class (8) Chapter 21 - Concurrency

php是最好的语言
php是最好的语言Original
2018-08-09 15:01:541343browse

  Sequential programming, that is, everything in the program can only perform one step at any time. Concurrent programming, the program can execute multiple parts of the program in parallel.

21.2.1 Define tasks

Threads can drive tasks, so you need a way to describe tasks, which can be provided by the Runnable interface. To define a task, just implement the Runnable interface and write the run() method so that the task can execute your commands.
When a class is derived from Runnable, it must have a run() method, but there is nothing special about this method - it does not create any inherent threading capabilities . To implement threaded behavior, you must explicitly attach a task to a thread.

21.2.3 Using Executor

 FixedThreadPool and CachedThreadPool

  • FixedThreadPool, expensive thread allocation can be performed in advance at once, thus limiting the number of threads. This saves time because you don't have to pay the fixed overhead of creating a thread for each task. In event-driven systems, event handlers that require threads can also be served as you wish by getting the threads directly from the pool. You won't abuse the available resources because the number of Thread objects used by FixedThreadPool is bounded.

Note that in any thread pool, existing threads will be automatically reused when possible.

  • Although this book will use CachedThreadPool, you should also consider using FiexedThreadPool in code that spawns threads. CachedThreadPool typically creates as many threads as required during program execution and then stops creating new threads when it recycles old threads, so it is a reasonable Executor first choice. Only if this approach causes problems should you switch to FixedThreadPool.

  • SingleThreadExecutor is like FixedThreadPool with the number of threads 1. (It also provides an important concurrency guarantee that other threads will not (that is, no two threads will) be called. This will change the locking requirements of the task)
    If submitted to SingleThreadExecutor Multiple tasks, then the tasks will be queued, each task will run to the end before the next task starts, and all tasks will use the same thread. In the example below, you can see that each task is completed in the order in which they were submitted, and before the next task starts. Therefore, SingleThreadExecutor will serialize all tasks submitted to it and will maintain its own (hidden) queue of pending tasks.

21.2.4 Producing return values ​​from tasks

- Runnable is an independent task that performs work, but it does not return a task value. If you want the task to return a value when completed, you can implement the Callable interface instead of the Runnable interface. Callable introduced in Java SE5 is a generic with type parameters. Its type parameters represent the method call() (instead of run() ) and must be called using the ExecutorService.submit() method.

21.2.9 Encoding Variations

  Another idiom you may see is the self-managed Runnable.

This is no special difference from inheriting from Thread, except that the syntax is slightly more obscure. However, implementing an interface allows you to inherit from a different class, whereas inheriting from Thread will not.

Note that the self-managed Runnable is called in the constructor. This example is fairly simple and therefore probably safe, but you should be aware that starting a thread in a constructor can become problematic because another task might start executing before the constructor ends, meaning that the task Ability to access objects in an unstable state. This is another reason to prefer Executor rather than explicitly creating a Thread object.

21.2.13 Thread Group

 Thread group holds a collection of threads. The value of thread groups can be summed up by quoting Joshua Bloch: "It's best to think of thread groups as an unsuccessful attempt, and you can just ignore it."

 If you've spent a lot of time and effort trying to discover the value of thread groups (like me), then you might be surprised why there hasn't been an official statement from Sun on the subject in years. The same question has been asked countless times about other changes in Java since then. The life philosophy of Joseph Stiglitz, the Nobel laureate in economics, can be used to explain this problem. It is called The Theory of Escalating Commitment: "The cost of continuing to make mistakes will be borne by others, while the cost of admitting mistakes will be borne by yourself. Commit."

21.2.14 Catching Exceptions

 Due to the nature of threads, you cannot catch exceptions that escape from the thread. Once an exception escapes the task's run() method, it will propagate out to the console unless you take special steps to catch this erroneous exception.

21.3 Sharing Restricted Resources

You can think of a single-threaded program as a single entity that solves the problem domain and can only do one thing at a time.

21.3.1 Incorrect access to resources

 Because the canceled flag is of type boolean, it is atomic, that is, such as assignment Simple operations like returning values ​​occur without the possibility of interruption, so you won't see this field in an intermediate state in the process of performing these simple operations.

It is important to note that the incrementing process itself requires multiple steps, and the task may be suspended by the pure mechanism during the incrementing process - that is, in Java, incrementing Not an atomic operation. Therefore, even a single increment is not safe without protecting the task.

21.4 Terminate the task

21.4.3 Interrupt

and call shutdownNow() on #Executor, it will send a interrupt() Called to all threads started by it.

#Executor By calling submit() instead of excutor()# to start the task, you can hold the context of the task. submit() will return a generic Future<?>. The key to holding this Future is that you can call on it cancel(), and therefore can be used to interrupt a specific task. If you pass true to cancel(), then it will have permission to call interrupt() on that thread to stop it. Therefore, cancel() is a way to interrupt a single thread started by Excutor.

# SleepBlock() is an interruptible blocking, while IOBlocked and ## #SynchronizedBlocked are uninterruptible blocking. The examples of the above three classes prove that I/O and waiting on synchronized blocks are uninterruptible. Neither I/O nor attempts to call synchronized methods require any InterruptedException handlers. As you can see from the output of the examples for the three classes above, you can interrupt calls to
sleep() (or any call that requires an InterruptedException). However, you cannot interrupt a thread that is trying to acquire a synchronized lock or that is trying to perform an I/O operation. This is a bit annoying, especially when doing I/O tasks, because it means that IO has the potential to lock up your multi-threaded program. Especially for Web-based programs, this is a matter of stakes.

A slightly clumsy but sometimes effective solution to this type of problem is to close the underlying resource on which the task is blocked:

21.5 Cooperation between threads

21.5.1 wait() and notifyAll()



wait() allows you to wait for a certain condition to change, and changing this condition is beyond the scope of the current method. control ability. Often this condition will be changed by another task. You definitely don't want to keep doing an empty loop while your task is testing this condition. This is called a busy wait and is generally a bad use of cycles. Therefore wait() will suspend the task while waiting for changes in the external world, and only when notify() or notifyAll() occurs, That is to say, when something of interest occurs, the task will be awakened and check the changes. Therefore, wait() provides a way to synchronize activities between tasks.

The lock is not released when

sleep() is called. This is also the case when calling yield(). It is important to understand this.
wait(), notify() and notifyAll() have a special aspect, that is, these methods are base class Object A part of , not part of Thread.

 Missed signal.

21.5.2 notify() and notifyAll()

 In the discussion about Java's threading mechanism, there is a confusing description: notifyAll() will wake up "all The task is waiting for you.” Does this mean that any task in the wait() state anywhere in the program will be awakened by any call to notifyAll()? There are examples showing that this is not the case - in fact, when notifyAll() is called for a specific lock, only tasks waiting for this lock will be awakened.

21.6 Deadlock

  The dining philosophers problem proposed by Edsger Dijkstrar is a classic example of deadlock.

To fix the deadlock problem, you must understand that a deadlock occurs when the following four conditions are met at the same time:

  • #mutually exclusive condition. At least one of the resources used by the task cannot be shared. Here, a Chopstick can only be used by one Philosopher at a time.

  • At least one task must hold a resource and is waiting to acquire a resource currently held by another task. That is, for a deadlock to occur, the Philosopher must hold one Chopstick and wait for another.

  • Resources cannot be preempted by tasks, and tasks must treat resource release as a normal event. Philosophers are polite and they don't take Chopsticks from other Philosophers.

  • There must be loop waiting. At this time, one task is waiting for the resources held by other tasks, and the latter is waiting for the pulp held by another task. This goes on until There is a task waiting for resources held by the first task, causing everyone to be locked. In DeadlockingDiningPhilosophers.java, because each Philosopher tries to get the Chopstick on the right first, and then the Chopstick on the left, a loop wait occurs.

 So if you want to prevent deadlock, you only need to destroy one of them. The easiest way to prevent deadlock is to violate condition 4.

21.7 Components in the new class library

21.7.1 CountDownLatch

 Applicable scenarios: It is used to synchronize one or more tasks, forcing them to wait for execution by other tasks A set of operations is completed. That is, one or more tasks need to wait until other tasks, such as the initial part of a problem, are completed.

 You can set an initial value to the

CountDownLatch object. Any method calling wait() on this object will block until the count value reaches 0. When other factors finish their work , you can call countDown() on the access object to reduce the count value. CountDownLatch is designed to be issued only once, and the count value cannot be reset. If you need a version that resets the count, you can use CyclicBarrier.

The task calling

countDown() is not blocked when this call is made. Only the call to await()# will be blocked until the count value is reached0.

#CountDownLatch is typically used to divide a program into n independent solvable tasks and create # with value n ##CountDownLatch. When each task completes, countDown() is called on this latch. Tasks waiting for the problem to be resolved call await() on this latch, suspending themselves until the latch count ends. 21.7.2 CyclicBarrier

 Applicable to situations where you want to create a set of tasks that perform work in parallel and then wait until all tasks are completed before proceeding to the next step ( Looks a bit like Join()). It causes all parallel tasks to be queued at the fence and therefore move forward uniformly.

For example, the program horse racing program: HorseRace.java

21.7.3 DelayQueue

#  ##  

DelayQueue#  ## is an unbounded

BlockingQueue

(synchronous Queue), used to place objects that implement the Delayed interface, in which objects can only be removed from the queue when they expire. This queue is ordered, that is, the head object is the first to expire. If there is no expired object, then the queue has no head element, so poll() will return null (because of this, we cannot place null into this queue). As mentioned above, DelayQueue has become a variant of the priority queue. 21.7.4 PriorityBlockingQueue This is a very basic priority queue with blocking read operations. The blocking nature of this queue provides all the necessary synchronization, so you should note that there is no need for any explicit synchronization here - you don't have to worry about whether there are elements in this queue when you read from it, because this queue When there are no elements, the reader will be blocked directly.

21.7.5 Room temperature controller using ScheduledExecutor

  "Greenhouse control system" can be regarded as a concurrency problem, each desired greenhouse event is a task that runs at a scheduled time.
ScheduledThreadPoolExecutor can solve this problem. Among them, schedule() is used to run the task once, and scheduleAtFixedRate() repeatedly executes the task every specified time. Both methods receive the delayTime parameter. Runnable objects can be set to execute at some point in the future.

21.7.6 Semaphre

21.8 Simulation

21.8.1 Bank Teller

21.8.2 Hotel Simulation

   BlockingQueue: Synchronous queue, when the first element is empty or unavailable, wait (blocking, Blocking) when executing .take().

#SynchronousQueue: It is a blocking queue with no internal capacity, so each put() must wait for a take(), and vice versa (that is, each take() must wait for a take() Wait for a put()). It's like you're handing an object to someone - there's no table on which to place the object, so you can only work if the person reaches out and is ready to receive the object. In this case, the SynchronousQueue represents a location set in front of the diner to reinforce the concept that only one dish can be served at any time.

 One very important thing to observe about this example is the management complexity of using queues to communicate between tasks. This single technique greatly simplifies the process of concurrent programming by inverting control: tasks do not interfere with each other directly, but send objects to each other via queues. The receiving task handles the object and treats it as a message rather than sending messages to it. If you follow this technique whenever possible, your chances of building a robust concurrent system are greatly increased.

21.8.3 Distribution work

21.9 Performance Tuning

21.9.1 Comparing mutex technologies

 Danger of “microbenchmarking”: This term usually refers to performance testing of a feature in isolation and out of context. Of course, you still have to write tests to verify assertions such as "Lock is faster than synchronized", but you need to be aware of what is actually happening during compilation and at runtime when writing these tests.

 Different compilers and runtime systems will differ in this regard, so it's difficult to know exactly what will happen, but we need to prevent the compiler from predicting the possibility of the outcome.

Using Lock is usually much more efficient than using synchronized, and the overhead of synchronized seems to vary too much, while Lock is relatively consistent.
Does this mean you should never use the synchronized keyword? There are two factors to consider here:

  • First, the size of the method body of the mutually exclusive method.

  • The second is that the code generated by the synchronized keyword is more readable than the code generated by the "lock-try/finally-unlock" idiom required by Lock. a lot of.

 Code is read far more often than it is written. When programming, communicating with other people is much more important than communicating with the computer, so the readability of your code is crucial. Therefore, it is of practical significance to start with the synchronized keyword and only replace it with a Lock object during performance tuning.

21.9.2 Lock-free containers

The general strategy for these lock-free windows is: modifications to the container can occur simultaneously with read operations, as long as the reader only You can see the results of completed modifications. Modifications are performed on a separate copy of a portion of the container data structure (sometimes a copy of the entire data structure), and this copy is not visible during the modification process. Only when the modification is completed, the modified structure is automatically exchanged with the main data structure, and then the reader can see the modification.

 Optimistic locking

 As long as you are primarily reading from a lock-free container, it will be much faster than its synchronized counterpart because the overhead of acquiring and releasing locks is eliminated. This is still the case if a small number of writes need to be performed into a lock-free container, but what counts as a "small amount"? This is a very interesting question.

21.11 Summary

An additional benefit of threads is that they provide lightweight execution context switches (about 100 instructions) rather than heavyweight process context switches (thousands of instructions) instruction). Because all threads within a given process share the same memory space, lightweight context switching only changes the program's execution sequence and local variables. Process switches (heavyweight context switches) must change all memory space.

related articles:

Java Programming Thoughts Learning Class (6) Chapter 19 - Enumeration Type

Java Programming Thoughts Learning Class (7) Chapter 20 - Notes

The above is the detailed content of Java Programming Thoughts Learning Class (8) Chapter 21 - Concurrency. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn