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.
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.
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.
- 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.
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.
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."
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.
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.
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.
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.
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.
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.
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.
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:
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.
countDown() is not blocked when this call is made. Only the call to
await()# will be blocked until the count value is reached
0.
#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
For example, the program horse racing program: HorseRace.java
21.7.3 DelayQueue
# ## (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.
"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.
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.
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.
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.
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!