Are SharedPreferences thread safe? What is the difference between commit and apply?

Posted by semtex on Wed, 08 Sep 2021 04:06:23 +0200

1. Preface

I met this problem when I interviewed iqiyi today. It's really ignorant because:

  • Usually, when using, a single App does automatic login, so it has always been operated by one thread, so I haven't thought about whether SharedPreferences is safe under multithreading.
  • For commit and apply, I read some blogs saying that commit is better, so I have always used commit, so I really haven't paid much attention to it.

For these two questions, here is a rearrangement.

2. Problem solving

2.1 small cases

In order to answer this question, we can do a small case, which is to use multiple threads to operate concurrently. Let's see whether it is safe.

public class MainActivity extends AppCompatActivity {
    private Button button;
    private SharedPreferences preferences;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.button);
        preferences = this.getPreferences(MODE_PRIVATE);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                doTest(3);
            }
        });
    }

    private void doTest(int threadNumber){
        for (int i = 0; i < threadNumber; i++) {
            new Thread(new MyRunnable()).start();
        }
    }

    // Locking ensures that the editing phase of each thread is atomic
    private synchronized void  editPreferences(){
        int key = preferences.getInt("key", 0);
        SharedPreferences.Editor edit = preferences.edit();
        Log.d("TAG", "val = " + key);
        edit.putInt("key", key + 1);
        edit.commit();
    }

    class MyRunnable implements Runnable{

        @Override
        public void run() {
            while(true){
                try {
                    Thread.sleep(200);
                    editPreferences();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

result:

So far, the test results are: thread safe in multi-threaded environment. Take a look Official website Description of:

From the above description, we can also see that it is thread safe! The apply method is to submit directly to the memory space, and then submit to the hard disk in an asynchronous way, without notifying any failure information. Commit is a method of synchronization. Failure to commit will block until it can be committed.

Operations in a process are safe.

2.2 viewing source code

We can see from tracing the source code:

Simply read the notes below, that is, commit and apply can be used to ensure consistency, and do not frequently modify the property value when using SharedPreferences, because it may slow down the App. And SharedPreferences does not support multi process operations.

Therefore, shared preferences is usually used to store some configuration files, because the configuration is generally not changed frequently by users. It can also be used to log in automatically, similarly.

The above comments say that commit and apply can be used to ensure consistency. Here you can take a look at the source code of these two methods:

2.2.1 commit() method

Because SharedPreferences she and SharedPreferences.Editor are interfaces, you need to find their related implementation classes. You can press Shift several times to find a class:

Then we find the commit method of the internal class EditorImpl:

public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
	
	// synchronized commit to memory
    MemoryCommitResult mcr = commitToMemory();
	
	// synchronized disk write
    SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
    try {
    	// CountDownLatch
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

The synchronized keyword is widely used to ensure thread safety for commit to memory and disk write operations. Finally, blocking operation is used to wait for the rest of the threads to complete. Therefore, the commit operation is thread safe under multithreading. And notice that try catch is used to ensure that the submission process is uninterrupted.

2.2.2 apply() method

public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }

            if (DEBUG && mcr.wasWritten) {
                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                        + " applied after " + (System.currentTimeMillis() - startTime)
                        + " ms");
            }
        }
    };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
        @Override
        public void run() {
            awaitCommit.run();
            QueuedWork.removeFinisher(awaitCommit);
        }
    };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

It is obvious that the apply method has no return value, and the user cannot know whether the submission is successful. Intuitively, there are two more Runnable implementations. The submission process adopts sub thread submission, which is asynchronous submission.

Comparison summary:

  • Both commit and apply are atomic operations, and commit cannot be interrupted.
  • commit has a corresponding return value to know whether the operation is successful. apply has no return value.
  • commit submission is a synchronous process, and its efficiency will be slower than that of apply asynchronous submission.
  • The commit method submits the modified data to the memory and then synchronously submits it to the hardware disk. Therefore, when multiple concurrent commit are submitted, they will wait for the processing commit to be saved to the disk before operation, thus reducing the efficiency.
  • apply is to submit the modified data to memory, and then asynchronously submit it to hardware disk.

References

Topics: Android