2

Atomic operation - An action that effectively happens all at once or not at all Ex: java.util.concurrent.atomic.AtomicInteger

Mutual exclusion - Prevents simultaneous access to a shared resource Ex: synchronized


With mutual exclusion approach, SynchronizedCounter is thread safe,

class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}

With atomic variable approach, AtomicCounter is thread safe,

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }
}

1) In the above code, Why is atomic variable approach better than mutual exclusion approach?

2) In general, Is the goal of mutual exclusion & atomic variable approach, not the same?

overexchange
  • 15,768
  • 30
  • 152
  • 347
  • Please provide entire context in the question, without needing to click links. – hyde Dec 02 '17 at 11:09
  • @hyde Query edited – overexchange Dec 02 '17 at 11:16
  • 1
    It's "better" because it uses low-level instructions that make it faster than synchronization, and because it's more difficult to introduce a bug using it than when using synchronized. – JB Nizet Dec 02 '17 at 11:19
  • @JBNizet Does *atomic variable* approach provide lock free synchronization? – overexchange Dec 02 '17 at 11:27
  • 1
    If your goal is to only increment an integer, atomically, yes. For more complex usecases (like modifying two references atomically, for example), no. – JB Nizet Dec 02 '17 at 11:37
  • @JBNizet Why terminology like *atomic variable* is required(`AtomicInteger`), when goal of *mutual exclusion* is also the same? – overexchange Dec 02 '17 at 11:40
  • Becaus ethe goal is not mutual exclusion at all. The goal is to increment an integer atomically. – JB Nizet Dec 02 '17 at 11:41
  • @JBNizet *Mutual exclusion* approach can ensure increment of an integer atomically, by securing that critical section, that increments it. – overexchange Dec 02 '17 at 11:47
  • Yes, so what? Are you asking if we could survive without atomic variables? yes, we could. They were only introduced in Java 6, BTW. But they're faster, and safer, as I explained in the first comment. – JB Nizet Dec 02 '17 at 11:48

2 Answers2

2

In your example both classes provide "functionally" equivalent results differing primarily in performance. If all you need is a simple counter an atomic is more appropriate as mutual exclusion will generally be more expensive. The reason for this is that Atomic operations are executed by a single CPU instruction where mutual exclusion requires more expensive higher-level operations typically handled by the OS.

Mutual exclusions allows for the co-ordination of changes across multiple variables. To expand on your example imagine a system that updates two (or more) counters. The counters are initialised as follows;

  • a = 0
  • b = 1

In the table below each row represents a transaction which will result in a desired state. Each column is a passage of time (e.g. CPU cycle).

Correctness for this system is defined as follows;

  1. stale reads are permitted (e.g. a previous transaction in its entirety).
  2. partial reads are invalid (e.g. a mixed view of two or more transactions).

enter image description here

The thick black lines represent a synchronisation point in time where values can be read. With Atomics it would be possible to execute in the order demonstrated which is undesirable. Mutual exclusion trades-off performance for correctness by either blocking or providing a stale read.

To clarify why "correctness" is important imagine "a" is net income and "b" is gross income. It is generally preferred to report something in the past or say "1 moment" than to provide values that do not add up.

Nathan
  • 553
  • 4
  • 11
  • c.incrementandget() is a single CPU instruction? – overexchange Dec 03 '17 at 00:51
  • The entirety of the method is unlikely to be a single instruction however an atomic increment can be achieved with the assembly `lock; xaddl %0, %1`. See the following for Atomic addition; https://en.wikipedia.org/wiki/Fetch-and-add#x86_implementation If you have the hotspot decompiler installed you can print the assembly with the following command; `java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly $MainClass` For additional details on assembly output see; https://wiki.openjdk.java.net/display/HotSpot/PrintAssembly – Nathan Dec 03 '17 at 06:22
1

The difference is that the first implementation with synchronized is blocking while the second one is not. The Comprehensive description of differences and consequences for both approaches are present in the first three chapters of The Art of Multiprocessor Programming book.

Here are some statements from Chapter 3.7

The wait-free and lock-free nonblocking progress conditions guarantee that the computation as a whole makes progress, independently of how the system schedules threads.

Progress conditions for blocking implementations: the deadlock-free and starvation-free properties. These properties are dependent progress conditions: progress occurs only if the underlying platform (i.e., the operating system) provides certain guarantees. In principle, the deadlock-free and starvation-free properties are useful when the operating system guarantees that every thread eventually leaves every critical section. In practice, these properties are useful when the operating system guarantees that every thread eventually leaves every critical section in a timely manner. Classes whose methods rely on lock-based synchronization can guarantee, at best, dependent progress properties. Does this observation mean that lock-based algorithms should be avoided? Not necessarily. If preemption in the middle of a critical section is sufficiently rare, then dependent blocking progress conditions are effectively indistinguishable from their nonblocking counterparts. If preemption is common enough to cause concern, or if the cost of preemption-based delay are sufficiently high, then it is sensible to consider nonblocking progress conditions.

Picking a progress condition for a concurrent object implementation depends on both the needs of the application and the characteristics of the underlying platform. The absolute wait-free and lock-free progress properties have good theoretical properties, they work on just about any platform, and they provide real-time guarantees useful to applications such as music, electronic games, and other interactive applications. The dependent obstruction-free, deadlock-free, and starvation-free properties rely on guarantees provided by the underlying platform. Given those guarantees, however, the dependent properties often admit simpler and more efficient implementations.

The good example of nonblocking and blocking implementations of the same logic in Java is ConcurrentLinkedQueue and LinkedBlockingQueue. While LinkedBlockingQueue looks more attractive because of nonblocking property, sometimes it's more useful to get blocked on enqueue/dequeue waiting for new elements and give scheduling time to other threads instead for getting empty result (null or exception) immediately and spinning in the busy loop of the current thread.

For counter it's definitely makes more sense to choose nonblocking approach which is also faster because of hardware CAS operation support.

Community
  • 1
  • 1
Mikita Harbacheuski
  • 2,193
  • 8
  • 16