Thinking in Java——笔记(21)

Concurrency


  • However, becoming adept at concurrent programming theory and techniques is a step up from everything you’ve learned so far in this book, and is an intermediate to advanced topic.
  • In practice, however, it’s much easier to write concurrent programs that only appear to work, but given the right conditions, will fail.
  • In fact, you may not be able to write test code that will generate failure conditions for your concurrent program.
  • With concurrency, you’re on your own, and only by being both suspicious and aggressive can you write multithreaded code in Java that will be reliable.
  • You never start a thread yourself doesn’t mean you’ll be able to avoid writing threaded code.
  • Web servers often contain multiple processors, and concurrency is an ideal way to utilize these processors.
  • Basically, knowing about concurrency makes you aware that apparently correct programs can exhibit incorrect behavior.

The many faces of concurrency

  • You’re forced to understand all issues and special cases in order to use concurrency effectively.

Faster execution

  • If you want a program to run faster, break it into pieces and run each piece on a separate processor.
  • If you have a multiprocessor machine, multiple tasks can be distributed across those processors, which can dramatically improve throughput.
  • However, concurrency can often improve the performance of programs running on a single processor.
  • If the program is written using concurrency, however, the other tasks in the program can continue to execute when one task is blocked, so the program continues to move forward.
  • It makes no sense to use concurrency on a single-processor machine unless one of the tasks might block.
  • One of the most compelling reasons for using concurrency is to produce a responsive user interface.
  • By creating a separate thread of execution to respond to user input, even though this thread will be blocked most of the time, the program guarantees a certain level of responsiveness.
  • One very straightforward way to implement concurrency is at the operating system level, using processes.
  • A multitasking operating system can run more than one process (program) at a time by periodically switching the CPU from one process to another, while making it look as if each process is chugging along on its own.
  • The fundamental difficulty in writing multithreaded programs is coordinating the use of these resources between different thread-driven tasks, so that they cannot be accessed by more than one task at a time.
  • This is an ideal example of concurrency. Each task executes as a process in its own address space, so there’s no possibility of interference between tasks.
  • There are generally quantity and overhead limitations to processes that prevent their applicability across the concurrency spectrum.
  • Instead of forking external processes in a multitasking operating system, threading creates tasks within the single process represented by the executing program.

Improving code design

  • Multithreaded systems often have a relatively small size limit on the number of threads available, sometimes on the order of tens or hundreds.
  • In Java, you can generally assume that you will not have enough threads available to provide one for each element in a large simulation.
  • Java’s threading is preemptive, which means that a scheduling mechanism provides time slices for each thread, periodically interrupting a thread and context switching to another thread so that each one is given a reasonable amount of time to drive its task.
  • In general, threads enable you to create a more loosely coupled design; otherwise, parts of your code would be forced to pay explicit attention to tasks that would normally be handled by threads.

Basic threading

  • A thread is a single sequential flow of control within a process.
  • One of the great things about threading is that you are abstracted away from this layer, so your code does not need to know whether it is running on a single CPU or many.

Defining tasks

  • A task’s run( ) method usually has some kind of loop that continues until the task is no longer necessary, so you must establish the condition on which to break out of this loop.
  • Often, run( ) is cast in the form of an infinite loop, which means that, barring some factor that causes run( ) to terminate, it will continue forever.
  • To achieve threading behavior, you must explicitly attach a task to a thread.

The Thread class

  • The traditional way to turn a Runnable object into a working task is to hand it to a Thread constructor.
  • Calling a Thread object’s start( ) will perform the necessary initialization for the thread and then call that Runnable’s run( ) method to start the task in the new thread.
  • This swapping is automatically controlled by the thread scheduler. If you have multiple processors on your machine, the thread scheduler will quietly distribute the threads among the processors.
  • The thread-scheduling mechanism is not deterministic.
  • You cannot plan on any consistent threading behavior. The best approach is to be as conservative as possible while writing threaded code.
  • Each Thread "registers" itself so there is actually a reference to it someplace, and the garbage collector can’t clean it up until the task exits its run( ) and dies.
  • A thread creates a separate thread of execution that persists after the call to start( ) completes.

Using Executors

  • Executors provide a layer of indirection between a client and the execution of a task; instead of a client executing a task directly, an intermediate object executes the task.
  • Executors allow you to manage the execution of asynchronous tasks without having to explicitly manage the lifecycle of threads.
  • An ExecutorService knows how to build the appropriate context to execute Runnable objects.
  • An ExecutorService object is created using a static Executors method which determines the kind of Executor it will be.
  • Very often, a single Executor can be used to create and manage all the tasks in your system.
  • A CachedThreadPool will generally create as many threads as it needs during the execution of a program and then will stop creating new threads as it recycles the old ones, so it’s a reasonable first choice as an Executor.
  • If more than one task is submitted to a SingleThreadExecutor, the tasks will be queued and each task will run to completion before the next task is begun, all using the same thread.
  • A SingleThreadExecutor serializes the tasks that are submitted to it, and maintains its own (hidden) queue of pending tasks.
  • By serializing tasks, you can eliminate the need to serialize the objects.

Producing return values from tasks

  • If you want the task to produce a value when it’s done, you can implement the Callable interface rather than the Runnable interface.
  • Callable is a generic with a type parameter representing the return value from the method call( ) (instead of run( )), and must be invoked using an ExecutorService submit( ) method.
  • The overloaded Executors.callable( ) method takes a Runnable and produces a Callable. ExecutorService has some "invoke" methods that run collections of Callable objects.

Sleeping

  • Java SE5 introduced the more explicit version of sleep( ) as part of the TimeUnit class, which provides better readability by allowing you to specify the units of the sleep( ) delay.
  • If you must control the order of execution of tasks, your best bet is to use synchronization controls (described later) or, in some cases, not to use threads at all, but instead to write your own cooperative routines that hand control to each other in a specified order.

Priority

  • Although the order in which the CPU runs a set of threads is indeterminate, the scheduler will lean toward running the waiting thread with the highest priority first.
  • Lower-priority threads just tend to run less often.
  • Trying to manipulate thread priorities is usually a mistake.
  • You can read the priority of an existing thread with getPriority( ) and change it at any time with setPriority( ).
  • Although the JDK has 10 priority levels, this doesn’t map well to many operating systems.

Yielding

  • When you call yield( ), you are suggesting that other threads of the same priority might be run.
  • In general, however, you can’t rely on yield( ) for any serious control or tuning of your application.

Daemon threads

  • A "daemon" thread is intended to provide a general service in the background as long as the program is running, but is not part of the essence of the program.
  • Thus, when all of the non-daemon threads complete, the program is terminated, killing all daemon threads in the process. Conversely, if there are any non-daemon threads still running, the program doesn’t terminate.
  • You must set the thread to be a daemon by calling setDaemon( ) before it is started.
  • It is possible to customize the attributes (daemon, priority, name) of threads created by Executors by writing a custom ThreadFactory.
  • Each of the static ExecutorService creation methods is overloaded to take a ThreadFactory object that it will use to create new threads.
  • If a thread is a daemon, then any threads it creates will automatically be daemons.
  • You should be aware that daemon threads will terminate their run( ) methods without executing finally clauses.
  • Daemons are terminated "abruptly" when the last of the non-daemons terminates. So as soon as main( ) exits, the JVM shuts down all the daemons immediately, without any of the formalities you might have come to expect.

Coding variations

  • In very simple cases, you may want to use the alternative approach of inheriting directly from Thread.
  • You should be aware that starting threads inside a constructor can be quite problematic, because another task might start executing before the constructor has completed, which means the task may be able to access the object in an unstable state.
  • Sometimes it makes sense to hide your threading code inside your class by using an inner class.

Terminology

  • There’s a distinction between the task that’s being executed and the thread that drives it.
  • You create tasks and somehow attach a thread to your task so that the thread will drive that task.
  • In Java, the Thread class by itself does nothing. It drives the task that it’s given.
  • If the interface is clearly nothing more than a generic encapsulation of its methods, then the "it-does-this-thing-able" naming approach is appropriate, but if it intends to express a higher concept, like Task, then the concept name is more helpful.
  • It makes sense from an implementation standpoint to separate tasks from threads.
  • To stay at a higher level of abstraction, you must use discipline when writing code.
  • If you are discussing a system at a conceptual level, you could just use the term "task" without mentioning the driving mechanism at all.

Joining a thread

  • If a thread calls t.join( ) on another thread t, then the calling thread is suspended until the target thread t finishes.

Catching exceptions

  • Because of the nature of threads, you can’t catch an exception that has escaped from a thread.
  • Thread.UncaughtExceptionHandler is a new interface in Java SE5; it allows you to attach an exception handler to each Thread object. Thread.UncaughtExceptionHandler.uncaughtException( ) is automatically called when that thread is about to die from an uncaught exception.

Sharing resources

  • You now have the possibility of two or more tasks interfering with each other.

Improperly accessing resources

  • It is atomic, which means that simple operations like assignment and value return happen without the possibility of interruption.
  • race condition: two or more tasks race to respond to a condition and thus collide or otherwise produce inconsistent results.
  • This is part of the problem with multithreaded programs—they can appear to be correct even when there’s a bug, if the probability for failure is very low.
  • Increment is not an atomic operation in Java. So even a single increment isn’t safe to do without protecting the task.

Resolving shared resource contention

  • For concurrency to work, you need some way to prevent two tasks from accessing the same resource, at least during critical periods.
  • To solve the problem of thread collision, virtually all concurrency schemes serialize access to shared resources.
  • Suggestions can be made to the thread scheduler via yield( ) and setPriority( ), but these suggestions may not have much of an effect, depending on your platform and JVM implementation.
  • To prevent collisions over resources, Java has built-in support in the form of the synchronized keyword. When a task wishes to execute a piece of code guarded by the synchronized keyword, it checks to see if the lock is available, then acquires it, executes the code, and releases it.
  • To control access to a shared resource, you first put it inside an object. Then any method that uses the resource can be made synchronized.
  • If a task is in a call to one of the synchronized methods, all other tasks are blocked from entering any of the synchronized methods of that object until the first task returns from its call.
  • When you call any synchronized method, that object is locked and no other synchronized method of that object can be called until the first one finishes and releases the lock.
  • Note that it’s especially important to make fields private when working with concurrency; otherwise the synchronized keyword cannot prevent another task from accessing a field directly, and thus producing collisions.
  • The JVM keeps track of the number of times the object has been locked. If the object is unlocked, it has a count of zero. As a task acquires the lock for the first time, the count goes to one. Each time the same task acquires another lock on the same object, the count is incremented.
  • There’s also a single lock per class (as part of the Class object for the class), so that synchronized static methods can lock each other out from simultaneous access of static data on a class-wide basis.
  • If you have more than one method in your class that deals with the critical data, you must synchronize all relevant methods.
  • Every method that accesses a critical shared resource must be synchronized or it won’t work right.

Using explicit Lock objects

  • The Lock object must be explicitly created, locked and unlocked; thus, it produces less elegant code than the built-in form.
  • Right after the call to lock( ), you must place a try-finally statement with unlock( ) in the finally clause.
  • The return statement must occur inside the try clause to ensure that the unlock( ) doesn’t happen too early and expose the data to a second task.
  • With explicit Lock objects, you can maintain proper state in your system using the finally clause.
  • You’ll usually only use the explicit Lock objects when you’re solving special problems.
  • The explicit Lock object also gives you finer-grained control over locking and unlocking than does the built-in synchronized lock.

Atomicity and volatility

  • You should only try to use atomicity instead of synchronization if you are a concurrency expert.
  • The JVM is allowed to perform reads and writes of 64-bit quantities (long and double variables) as two separate 32-bit operations, raising the possibility that a context switch could happen in the middle of a read or write, and then different tasks could see incorrect results.
  • You do get atomicity (for simple assignments and returns) if you use the volatile keyword when defining a long or double variable.
  • The volatile keyword also ensures visibility across the application. If you declare a field to be volatile, this means that as soon as a write occurs for that field, all reads will see the change.
  • volatile fields are immediately written through to main memory, and reads occur from main memory.
  • If multiple tasks are accessing a field, that field should be volatile; otherwise, the field should only be accessed via synchronization.
  • Your first choice should be to use the synchronized keyword.
  • If you define a variable as volatile, it tells the compiler not to do any optimizations that would remove reads and writes that keep the field in exact synchronization with the local data in the threads.
  • volatile also restricts compiler reordering of accesses during optimization.
  • The atomic operations that are supposed to be safe are the reading and assignment of primitives.
  • It’s still easily possible to use an atomic operation that accesses your object while it’s in an unstable intermediate state.

Atomic classes

  • It should be emphasized that the Atomic classes were designed to build the classes in java.util.concurrent, and that you should use them in your own code only under special circumstances, and even then only when you can ensure that there are no other possible problems.

Critical sections

  • Sometimes, you only want to prevent multiple thread access to part of the code inside a method instead of the entire method.
  • Note that the synchronized keyword is not part of the method signature and thus may be added during overriding.
  • This is typically the reason to use a synchronized block instead of synchronizing the whole method: to allow other tasks more access (as long as it is safe to do so).

Synchronizing on other objects

  • When the lock is acquired for the synchronized block, other synchronized methods and critical sections in the object cannot be called.
  • Sometimes you must synchronize on another object, but if you do this you must ensure that all relevant tasks are synchronizing on the same object.

Thread local storage

  • Thread local storage is a mechanism that automatically creates different storage for the same variable, for each different thread that uses an object.
  • If you have five threads using an object with a variable x, thread local storage generates five different pieces of storage for x.
  • The creation and management of thread local storage is taken care of by the java.lang.ThreadLocal class.
  • ThreadLocal objects are usually stored as static fields. When you create a ThreadLocal object, you are only able to access the contents of the object using the get( ) and set( ) methods.
  • ThreadLocal guarantees that no race condition can occur.

Terminating tasks

  • In real threading problems, the possibility for failure may be statistically small, so you can easily fall into the trap of believing that things are working correctly.
  • There are likely to be hidden problems that haven’t occurred to you, so be exceptionally diligent when reviewing concurrent code.

Terminating when blocked

  • sleep( ) is just one situation where a task is blocked from executing, and sometimes you must terminate a task that’s blocked.

  • A thread can be in any one of four states:

    New: it becomes eligible to receive CPU time.
    Runnable: a thread can be run when the time-slicing mechanism has CPU cycles available for the thread.
    Blocked: the scheduler will simply skip it and not give it any CPU time.
    Dead: A thread is no longer schedulable and will not receive any CPU time.

  • Becoming blocked: You’ve put the task to sleep;You’ve suspended the execution of the thread; waiting for some I/O to complete;call a synchronized method.

Interruption

  • If you’re using Executors, you can hold on to the context of a task when you start it by calling submit( ) instead of execute( ).
  • I/O and waiting on a synchronized lock are not interruptible.
  • You can interrupt a call to sleep( ) (or any call that requires you to catch InterruptedException). However, you cannot interrupt a task that is trying to acquire a synchronized lock or one that is trying to perform I/O.
  • Blocked nio channels automatically respond to interrupts.
  • One of the features added in the Java SE5 concurrency libraries is the ability for tasks blocked on ReentrantLocks to be interrupted, unlike tasks blocked on synchronized methods or critical sections.

Checking for an interrupt

  • If you call interrupt( ) to stop a task, your task needs a second way to exit in the event that your run( ) loop doesn’t happen to be making any blocking calls.
  • You check for the interrupted status by calling interrupted( ).
  • Clearing the interrupted status ensures that the framework will not notify you twice about a task being interrupted.
  • A class designed to respond to an interrupt( ) must establish a policy to ensure that it will remain in a consistent state.
  • The creation of all objects that require cleanup must be followed by try-finally clauses so that cleanup will occur regardless of how the run( ) loop exits.
  • It relies on the client programmer to write the proper try-finally clauses.

Cooperation between tasks

  • If two tasks are stepping on each other over a shared resource (usually memory), you use a mutex to allow only one task at a time to access that resource.
  • Now the issue is not about interfering with one another, but rather about working in unison, since portions of such problems must be solved before other portions can be solved.
  • The key issue when tasks are cooperating is handshaking between those tasks.
  • In this case guarantees that only one task can respond to a signal.

wait() and notifyAll()

  • So wait( ) suspends the task while waiting for the world to change, and only when a notify( ) or notifyAll( ) occurs—suggesting that something of interest may have happened—does the task wake up and check for changes.
  • When a task enters a call to wait( ) inside a method, that thread’s execution is suspended, and the lock on that object is released.
  • Because wait( ) releases the lock, it means that the lock can be acquired by another task, so other synchronized methods in the (now unlocked) object can be called during a wait( ).
  • The only place you can call wait( ), notify( ), or notifyAll( ) is within a synchronized method or block.
  • You can ask another object to perform an operation that manipulates its own lock.
  • In order for the task to wake up from a wait( ), it must first reacquire the lock that it released when it entered the wait( ). The task will not wake up until that lock becomes available.
  • You must surround a wait( ) with a while loop that checks the condition(s) of interest.
  • When two threads are coordinated using notify( )/wait( ) or notifyAll( )/wait( ), it’s possible to miss a signal.

notify() vs. notifyAll()

  • Only one task of the possible many that are waiting on a lock will be awoken with notify( ), so you must be certain that the right task will wake up if you try to use notify( ).
  • All tasks must be waiting on the same condition in order for you to use notify( ), because if you have tasks that are waiting on different conditions, you don’t know if the right one will wake up.
  • If you use notify( ), only one task must benefit when the condition changes.
  • Only the tasks that are waiting on a particular lock are awoken when notifyAll( ) is called for that lock.

Producers and consumers

  • However, in more complex situations, multiple tasks may be waiting on a particular object lock, so you don’t know which task should be awakened. Thus, it’s safer to call notifyAll( ), which wakes up all the tasks waiting on that lock.
  • Because the lock must be owned in order for notifyAll( ) to be called, it’s guaranteed that two tasks trying to call notifyAll( ) on one object won’t step on each other’s toes.
  • In a typical producerconsumer implementation, you use a first-in, first-out queue in order to store the objects being produced and consumed.

Using explicit Lock and Condition objects

  • You can suspend a task by calling await( ) on a Condition.
  • When external state changes take place that might mean that a task should continue processing, you notify the task by calling signal( ), to wake up one task, or signalAll( ), to wake up all tasks that have suspended themselves on that Condition object.
  • The Condition object contains no information about the state of the process.
  • The Lock and Condition objects are only necessary for more difficult threading problems.

Producer-consumers and queues

  • In many cases, you can move up a level of abstraction and solve task cooperation problems using a synchronized queue, which only allows one task at a time to insert or remove an element.
  • You’ll usually use the LinkedBlockingQueue, which is an unbounded queue; the ArrayBlockingQueue has a fixed size, so you can only put so many elements in it before it blocks.
  • Blocking queues can solve a remarkable number of problems in a much simpler and more reliable fashion than wait( ) and notifyAll( ).
  • The coupling between the classes that would exist with explicit wait( ) and notifyAll( ) statements is eliminated because each class communicates only with its BlockingQueues.

Using pipes for I/O between tasks

  • It’s often useful for tasks to communicate with each other using I/O. Threading libraries may provide support for inter-task I/O in the form of pipes.
  • The pipe is basically a blocking queue, which existed in versions of Java before BlockingQueue was introduced.
  • The PipedReader is interruptible, whereas if you changed, the interrupt( ) would fail to break out of the read( ) call.

Deadlock

  • You get a continuous loop of tasks waiting on each other, and no one can move.

  • The real problem is when your program seems to be working fine but has the hidden potential to deadlock.

  • Preventing deadlock through careful program design is a critical part of developing concurrent systems.

  • A program can appear to run correctly but actually be able to deadlock.

  • Deadlock can occur if four conditions are simultaneously met:

    a. Mutual exclusion. At least one resource used by the tasks must not be shareable.
    b. At least one task must be holding a resource and waiting to acquire a resource currently held by another task.
    c. A resource cannot be preemptively taken away from a task. Tasks only release resources as a normal event.
    d. A circular wait can happen.

  • You only need to prevent one of them from occurring to prohibit deadlock.

  • There is no language support to help prevent deadlock; it’s up to you to avoid it by careful design.

New library components

  • The java.util.concurrent library in Java SE5 introduces a significant number of new classes designed to solve concurrency problems.

CountDownLatch

  • This is used to synchronize one or more tasks by forcing them to wait for the completion of a set of operations being performed by other tasks.
  • You give an initial count to a CountDownLatch object, and any task that calls await( ) on that object will block until the count reaches zero.
  • A CountDownLatch is designed to be used in a one-shot fashion; the count cannot be reset. If you need a version that resets the count, you can use a CyclicBarrier instead.
  • A typical use is to divide a problem into n independently solvable tasks and create a CountDownLatch with a value of n.
  • It happens that Random.nextInt( ) is thread-safe, but alas, you shall have to discover this on a case-by-case basis, using either a Web search or by inspecting the Java library code.

CyclicBarrier

  • A CyclicBarrier is used in situations where you want to create a group of tasks to perform work in parallel, and then wait until they are all finished before moving on to the next step.
  • A CountDownLatch is a one-shot event, whereas a CyclicBarrier can be reused over and over.
  • A CyclicBarrier can be given a "barrier action," which is a Runnable that is automatically executed when the count reaches zero—this is another distinction between CyclicBarrier and CountdownLatch.

DelayQueue

  • This is an unbounded BlockingQueue of objects that implement the Delayed interface.
  • An object can only be taken from the queue when its delay has expired. The queue is sorted so that the object at the head has a delay that has expired for the longest time.
  • If no delay has expired, then there is no head element and poll( ) will return null.
  • DelayQueue is thus a variation of a priority queue.

PriorityBlockingQueue

  • This is basically a priority queue that has blocking retrieval operations.
  • You don’t have to think about whether the queue has any elements in it when you’re reading from it, because the queue will simply block the reader when it is out of elements.

The greenhouse controller with ScheduledExecutor

  • The ScheduledThreadPoolExecutor provides just the service necessary to solve the problem. Using either schedule( ) (to run a task once) or scheduleAtFixedRate( ) (to repeat a task at a regular interval), you set up Runnable objects to be executed at some time in the future.

Semaphore

  • A normal lock (from concurrent.locks or the built-in synchronized lock) only allows one task at a time to access a resource.
  • A counting semaphore allows n tasks to access the resource at the same time.
  • You can also think of a semaphore as handing out "permits" to use a resource, although no actual permit objects are used.

Exchanger

  • An Exchanger is a barrier that swaps objects between two tasks.
  • When the tasks enter the barrier, they have one object, and when they leave, they have the object that was formerly held by the other task.
  • Exchangers are typically used when one task is creating objects that are expensive to produce and another task is consuming those objects; this way, more objects can be created at the same time as they are being consumed.

Simulation

  • Using concurrency, each component of a simulation can be its own task, and this makes a simulation much easier to program.

Bank teller simulation

  • This classic simulation can represent any situation where objects appear randomly and require a random amount of time to be served by a limited number of servers.
  • It’s possible to build the simulation to determine the ideal number of servers.
  • All control systems have stability issues; if they react too quickly to a change, they are unstable, and if they react too slowly, the system moves to one of its extremes.

The restaurant simulation

  • SynchronousQueue is a blocking queue that has no internal capacity, so each put( ) must wait for a take( ), and vice versa.
  • The management of complexity using queues to communicate between tasks greatly simplifies the process of concurrent programming by inverting the control: The tasks do not directly interfere with each other. Instead, the tasks send objects to each other via queues.
  • The receiving task handles the object, treating it as a message rather than having the message inflicted upon it.

Performance tuning

Comparing mutex technologies

  • It is fairly clear that using Lock is usually significantly more efficient than using synchronized, and it also appears that the overhead of synchronized varies widely, while Locks are relatively consistent.
  • So the percentage of time in the body will probably be significantly bigger than the overhead of entering and exiting the mutex, and could overwhelm any benefit of speeding up the mutex.
  • The synchronized keyword produces much more readable code than the lock try/finally-unlock idiom that Locks require.
  • It makes sense to start with the synchronized keyword and only change to Lock objects when you are tuning for performance.
  • Atomic objects are only useful in very simple cases, generally when you only have one Atomic object that’s being modified and when that object is independent from all other objects.
  • It’s safer to start with more traditional mutexing approaches and only attempt to change to Atomic later, if performance requirements dictate.

Lock-free containers

  • In Java 1.2, the Collections class was given various static "synchronized" decoration methods to synchronize the different types of containers.
  • Java SE5 has added new containers specifically to increase thread-safe performance, using clever techniques to eliminate locking.
  • A modification is performed on a separate copy of a portion of the data structure (or sometimes a copy of the whole thing), and this copy is invisible during the modification process.
  • Only when the modification is complete is the modified structure atomically swapped with the "main" data structure, and after that readers will see the modification.
  • In CopyOnWriteArrayList, a write will cause a copy of the entire underlying array to be created.
  • CopyOnWriteArraySet uses CopyOnWriteArrayList to achieve its lock-free behavior.
  • ConcurrentHashMap and ConcurrentLinkedQueue use similar techniques to allow concurrent reads and writes, but only portions of the container are copied and modified rather than the entire container.

Performance issues

  • 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.
  • You can see that a synchronized ArrayList has roughly the same performance regardless of the number of readers and writers.

Optimistic locking

  • You do not actually use a mutex when you are performing a calculation, but after the calculation is finished and you’re ready to update the Atomic object, you use a method called compareAndSet( ).
  • You hand it the old value and the new value, and if the old value doesn’t agree with the value it finds in the Atomic object, the operation fails.
  • Using an Atomic instead of synchronized or Lock, you might gain performance benefits.
  • If compareAndSet( ) fails, you must decide what to do; this is very important because if you can’t do something to recover, then you cannot use this technique and must use conventional mutexes instead.

ReadWriteLocks

  • The ReadWriteLock allows you to have many readers at one time as long as no one is attempting to write.
  • The only way to know whether a ReadWriteLock will benefit your program is to try it out.

Active objects

  • Each object maintains its own worker thread and message queue, and all requests to that object are enqueued, to be run one at a time.
  • So with active objects, we serialize messages rather than methods, which means we no longer need to guard against
    problems that happen when a task is interrupted midway through its loop.
  • Synchronization still happens, but it happens on the message level, by enqueuing the method calls so that only one can happen at a time.
原文地址:https://www.cnblogs.com/apolloqq/p/6266333.html