Synchronization on immutable objects (in java)

2019-01-28 01:55发布

问题:

Code snippet - 1

class RequestObject implements Runnable
{
    private static Integer nRequests = 0;

    @Override
    public void run()
    {       
        synchronized (nRequests)
        {
            nRequests++;
        }
    }
}

Code snippet - 2

public class Racer implements Runnable
{
    public static Boolean won = false;    

    @Override
    public void run()
    {
        synchronized (won)
        {
            if (!won)
            won = true;
        }
    }        
}

I was having a race condition with the first code snippet. I understood that this was because I was obtaining a lock on an immutable object(of type Integer).

I have written a second code snippet which is again impervious to 'Boolean' being immutable. But this works(no race condition is displayed in an output run). If I have understood the solution to my previous question properly the below is is one possible way in which things can go wrong

  1. Thread 1 gets a lock on an object(say A) pointed by won
  2. Thread 2 tries to now get a lock on the object pointed to by won and gets in the wait queue for A
  3. Thread 1 goes into the synchronized block, verifies that A is false and creates a new object(say B) by saying won = true(A thinks it won the race).
  4. 'won' now points to B. Thread 1 releases the lock on object A(no longer pointed to by won)
  5. Now, thread-2 which was in the wait queue of object A gets woken up and gets a lock on object A which is still false(immutably so). It now goes into the synchronized block and assumes that it has also won, which is not correct.

Why is the second code snippet working fine all the time??

回答1:

    synchronized (won)
    {
        if (!won)
        won = true;
    }

Here you have a transient race condition which you don't notice because it goes away after the first execution of the run method. After that the won variable constantly points to the same instance of Boolean representing true, which thus serves properly as a mutex lock.

This is not to say that you should ever write such code in real projects. All lock objects should be assigned to final variables to make sure they never change.



回答2:

Whether or not an object is immutable has nothing to do with whether it's suitable as lock object in a synchronized statement. It is important, however, that the same object be used by all threads entering the same set of critical regions (hence it may be wise to make the object reference final), but the object itself can be modified without affecting it's "lockiness". Additionally, two (or more) different synchronized statements can use different reference variables and still be mutually exclusive, so long as the different reference variables all refer to the same object.

In the above examples the code in the critical region replaces one object with another, and that is a problem. The lock is on the object, not the reference, so changing the object is a no-no.



回答3:

I was having a race condition with the first code snippet. I understood that this was because I was obtaining a lock on an immutable object(of type Integer).

Actually, that is not the reason at all. Obtaining the lock on an immutable object, will "work" just fine. The problem is that it probably won't do anything useful ...

The real reason that the first example breaks is that you are locking the wrong thing. When you do execute this - nRequests++ - what you are actually doing is equivalent to this non-atomic sequence:

    int temp = nRequests.integerValue();
    temp = temp + 1;
    nRequests = Integer.valueOf(temp);

In other words, you are assigning a different object reference the static variable nRequests.

The problem is that in your snippet the threads will be synchronizing on a different object each time an update is made to the variable. That's because each thread changes the reference to the object that is to be locked.

In order to synchronize properly, all of the threads need to lock the same object; e.g.

class RequestObject implements Runnable
{
    private static Integer nRequests = 0;
    private static final Object lock = new Object();

    @Override
    public void run()
    {       
        synchronized (lock)
        {
            nRequests++;
        }
    }
}

In fact, the second example suffers from the same problem as the first. The reason you are not noticing it in your testing is that the transition from won == false to won == true happens just once ... so the probability that the potential race condition will actually eventuate is much, much smaller.



回答4:

In fact, your second code is also not thread safe. Please use the code below to check for yourself (you will find out that the first print statement will be 2 sometimes which means that there are two threads inside the synchronized block!). The bottom line: Code snippet - 1 & Code snippet - 2 are basically the same and thus are not thread safe...

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class Racer implements Runnable {
    public static AtomicInteger counter = new AtomicInteger(0);
    public static Boolean won = false;    

    @Override
    public void run() {
        synchronized (won) {
            System.out.println(counter.incrementAndGet()); //should be always 1; otherwise race condition
            if (!won) {
                won = true;
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(counter.decrementAndGet()); //should be always 0; otherwise race condition
        }
    }   

    public static void main(String[] args) {
        int numberOfThreads = 20;
        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);

        for(int i = 0; i < numberOfThreads; i++) {
            executor.execute(new Racer());
        }

        executor.shutdown();
    }
}