Original: Curly brace MC(wechat official account: Huakuohao-MC) Focus on JAVA basic programming and big data, focus on experience sharing and personal growth.

This is the second article in the concurrent programming series. The previous article covered the relationship between threads and tasks and how to create them. This article describes how multiple threads can correctly access shared mutable resources.

A shared mutable resource is one that each thread can read and write to. How to get multiple threads to correctly modify and read shared variables is an art.

Problem is introduced into

The following code implements a thread counter, which counts how many threads have performed a task.

Start by defining a task

public class Task implements Runnable {
    public static int count = 0;
    public  void  increase(a){
        count++;
    }
    @Override
    public void run(a) { increase(); }}Copy the code

Use threads to drive tasks

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++){
        Thread thread = new Thread(new Task());
        thread.start();
    }
    // Wait for the child thread to finish executing
    Thread.sleep(5000);
    System.out.println(Task.count);
}
Copy the code

The above example code, if you need to execute it more than once, will give you a different result, showing how hidden the errors of concurrent programs can be. I get 1W or 9999 in the local environment, like this:

Problem analysis

In theory, we started 1W threads, but the result could be 9999. There are two reasons for this. The first reason is that two threads can modify and read the variable count at the same time. The second reason is that increment ++ operations in Java are not atomic operations.

When you do count++, you actually do three things: read the current value of count, increment count by one, and write the result to count.

Assumes that the first thread reads the count, after get the count value is 0, then execute the increase in the process of operation, the second thread is to read the count value, then the second thread to get the value is 0, then do the second thread is based on zero since the operation, so that two threads after the result is 1, not 2.

The solution

In fact, the above problem can be summarized as “how to correctly use the shared variable resources of multithreading”, which is the core of concurrent programming. There are usually two solutions to this problem.

The first solution is to lock the shared resource synchronously. Locking can ensure that only one thread is using the shared resource at the same time.

The second option is not to share variables, such as each thread holding a Copy of the shared variable, or only one thread can modify the shared variable and the other threads are read-only.

The introduction of the lock

When we lock a resource or a piece of code, it means that only one thread can execute the code at the same time. When one thread finishes executing and releases the lock resource, other threads have the opportunity to acquire the resource and continue executing.

This process is like many people fighting for a toilet pit. When you grab the toilet, lock it immediately, so that no one can influence you to use it. If you don’t lock it, many people will keep pulling the door open, affecting you.

Classification of lock

In terms of usage, Java provides two types of locks. The first type of lock is called internal lock, also known as synchronized. The second type of lock is called a display lock, or ReentrantLock

Built-in lock – synchronized

The increase() method can be synchronized to ensure that only one thread is using the shared resource count at a time.

public synchronized void  increase(a){
    count++;
}
Copy the code

In Java, each object or class has a single built-in lock, also known as a monitor lock. The lock is automatically acquired when a thread enters a synchronized code block and released when it leaves.

If multiple methods on an object are locked, then they share the same lock. Suppose an object contains the public synchronized void f() method and the public synchronized void g() method. Method, if one thread calls f(), other threads must wait for f() to finish and release the lock before calling f() or g().

Reentrant of built-in locks

A thread can block when trying to acquire a lock held by another thread, but a thread can regain a lock held by itself. For example, if a subclass overrides a synchronized modified method of its parent class and then calls the method in the parent class again, a deadlock will occur if there is no lock reentrant mechanism.

public class Parent{
    public synchronized void doSomething(a){
        //do something..}}public class Children extends Parent{
    public synchronized void doSomething(a){
        // children do something
        super.doSomething(); }}Copy the code

A critical region

In addition to locking the entire method, you can also lock blocks of code. This is called a synchronous control block, also known as a critical section. The goal is to significantly improve program performance by reducing lock granularity.

// It has the same effect as the lock method, but reduces the lock granularity
synchronized(synObject){
    //do something
}
Copy the code

Display the lock – already

For the task counter code above, in addition to the built-in lock, you can also implement a display lock ReentrantLock. Sample code is as follows

public class ReentranLockTask implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static int count;
    public void increase(a){
        / / lock
        lock.lock();
        try{
            count++;
        }finally {
            / / releaselock.unlock(); }}@Override
    public void run(a) { increase(); }}Copy the code

For display locks, there is significantly more code on them than for internal locks, because display locks have to be released manually in addition to declaring the lock themselves, and forgetting to release the lock can be disastrous.

But showing locks has its own features, such as being more flexible and allowing you to clean up thread resources in the event of an exception. But with internal locks, there’s not much you can do.

In addition, when you use a display lock to acquire a resource, you can specify a time range, such as tryLock(long timeout, TimeUnit Unit). If you do not acquire a resource within a specified time, the thread can do something else without being blocked for a long time.

Display lock – Read-write separation lock

The name indicates that there are two locks, one for read and one for write. Read/write locks allow multiple reader threads to execute simultaneously, but only one thread can operate when a writer thread is involved. Read/write locks with many reads and few writes can significantly improve performance because multiple read operations are executed in parallel.

A typical scenario for reading too much and writing too little is caching. The following code example implements two different caches using display and read-write locks respectively. The performance difference between the two caches is noticeable.

Abstract class DemoCache, which defines the basic operations of the cache, shows that the cache of the lock implementation and the cache of the read-write lock implementation inherit from this class.

public abstract class DemoCache abstract String read(String key) throws Exception;
    abstract void write(String key, String value) throws Exception;
}
Copy the code

DemoLockCache (display-lock)

public class DemoLockCache extends DemoCache {
    // Display lock, can also use synchronized
    private ReentrantLock lock = new ReentrantLock();
    / / cache the Map
    private Map<String,String> cacheMap = new HashMap<String,String>();
    
    @Override
    String read(String key) throws Exception {
        lock.lock();
        try{
            String value = cacheMap.get(key);
            Thread.sleep(500);
            return value;
        }finally{ lock.unlock(); }}@Override
    void write(String key, String value) throws Exception {
        lock.lock();
        try{
            cacheMap.put(key,value);
            Thread.sleep(300);
        }finally{ lock.unlock(); }}}Copy the code

DemoReadWriteLockCache: A cache that uses read and write locks to achieve good performance.

public class DemoReadWriteLockCache extends DemoCache {
    // Read/write split lock
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    / / cache the Map
    private Map<String, String> cacheMap = new HashMap<String, String>();

    @Override
    String read(String key) throws InterruptedException {
        / / read lock
        Lock readLock = readWriteLock.readLock();
        readLock.lock();
        try {
            String value = cacheMap.get(key);
            Thread.sleep(500);
            return value;
        } finally{ readLock.unlock(); }}@Override
    void write(String key, String value) throws InterruptedException {
        / / write locks
        Lock writeLock = readWriteLock.writeLock();
        writeLock.lock();
        try {
            cacheMap.put(key, value);
            Thread.sleep(300);
        } finally{ writeLock.unlock(); }}}Copy the code

Create two tasks, one for read operations and one for write operations.

DemoCacheReadTask for read operations

public class DemoCacheReadTask implements Runnable {
    private DemoCache demoCache;

    public DemoCacheReadTask(DemoCache demoCache){
        this.demoCache = demoCache;
    }
    @Override
    public void run(a) {
        String key = Thread.currentThread().getName();
        try {
            demoCache.read(key);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

DemoCacheWriteTask for write operations

public class DemoCacheWriteTask implements Runnable {
    private DemoCache demoCache;

    public DemoCacheWriteTask(DemoCache demoCache){
        this.demoCache = demoCache;
    }
    @Override
    public void run(a) {
        String key = Thread.currentThread().getName();
        String value = key + "value";
        try {
            demoCache.write(key,value);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

The test class DemoCacheTest

public class DemoCacheTest {
    public static void main(String[] args){
        // Cache for non-read-write implementations
        //DemoCache demoCache = new DemoLockCache();
        // Cache for read-write separation implementation
        DemoCache demoCache = new DemoReadWriteLockCache();
        / / read threads
        for (int i = 0; i < 10; i++){
            Thread thread = new Thread(new DemoCacheReadTask(demoCache));
            thread.start();
        }
        / / write threads
        for (int i = 0; i < 3; i++){
            Thread thread = new Thread(newDemoCacheWriteTask(demoCache)); thread.start(); }}}Copy the code

The end of the

This article mainly introduces how to achieve correct access to shared variable resources through locking. These include built-in locks, display locks, and read-write locks. In general, it is recommended to use a built-in lock. If the built-in lock does not meet the requirements, you can consider using a display lock, but do not forget to release the lock manually. Consider using read/write separation locks to improve performance in read/write scenarios. The next article describes how to properly access shared mutable resources without using locks.


Recommended reading:

1. Concurrent programming in Java (I) — tasks and threads

Java8 Stream is really delicious, you never know if you haven’t experienced it

3. Do you know how to use Awk

4. Teach you how to build a set of ELK log search operation and maintenance platform

, END,

Curly braces MC

Java· Big Data · Personal growth

Wechat id: Huakuohao-MC