Thread-safe, simple lock creation demo code

Thread safety means that the program can still maintain correct behavior and data integrity when multiple threads access it concurrently. That is, multiple threads can access an object or method at the same time without damaging the consistency and correctness of the data. The implementation of thread safety needs to follow some rules, such as using synchronization mechanism to ensure mutual exclusion of multiple threads’ access to shared resources and avoiding race conditions. Thread safety is very important for multi-threaded programming because the interaction and competition between multiple threads may cause unexpected errors and exceptions in the program.

Situations where thread safety issues arise in multi-threading include:

1. Resource competition: Multiple threads reading and writing the same shared resource at the same time may cause some threads to read incorrect data or overwrite data modified by other threads, resulting in data inconsistency or data loss.

2. Deadlock: There is a situation where multiple threads are waiting for each other, in which each thread is waiting for other threads to release resources, causing all threads to be unable to continue executing, eventually causing the program to crash.

3. Inter-thread communication problem: When multiple threads need to communicate, if there is no appropriate synchronization mechanism, some threads may wait or block continuously, thus affecting the execution efficiency of the program or causing deadlock.

4. Instruction rearrangement problem: In a multi-threaded environment, the CPU may rearrange instructions in order to improve execution efficiency. If the order of instructions executed by different threads is not guaranteed, the data read by some threads may be incorrect. This causes program errors.

5. Cache consistency problem: Since each thread has its own cache, when multiple threads read and write shared variables, there may be cache inconsistency, resulting in data errors.

Competition for resources

public class Main {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();
        Thread thread1 = new Thread(new MyRunnable(sharedResource));
        Thread thread2 = new Thread(new MyRunnable(sharedResource));
        thread1.start();
        thread2.start();
    }
}

class SharedResource {
    private int count = 0;

    public void increment() {
        count + + ;
    }

    public int getCount() {
        return count;
    }
}

class MyRunnable implements Runnable {
    private final SharedResource sharedResource;

    public MyRunnable(SharedResource sharedResource) {
        this.sharedResource = sharedResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i + + ) {
            sharedResource.increment();
        }
        System.out.println("Thread " + Thread.currentThread().getName() + " finished, count = " + sharedResource.getCount());
    }
}

This example code defines a shared resource SharedResource, which contains a counter count, and two MyRunnable threads, both of which share the counter. In the run method of MyRunnable, the value of the counter is incremented by calling the sharedResource.increment() method. Although this method seems simple, a race condition can occur when calling it from two threads executing at the same time. If two threads simultaneously read the value of count, increment it, and then write the new value of count, it is possible for the counter to have an incorrect value.

So how to solve this thread safety problem?

We can use a locking mechanism to solve this problem for this code.

There are several ways to create a lock. One is to use the synchronized keyword to add a lock to a code block or class, and the other is to use the Lock class to manually add a lock.

public class ThreadSafe {

        public static void main(String[] args) {
            SharedResource sharedResource = new SharedResource();
            MyRunnable myRunnable = new MyRunnable(sharedResource);
            Thread thread1 = new Thread(myRunnable,"Thread1");
            Thread thread2 = new Thread(myRunnable,"Thread2");
            thread1.start();
            thread2.start();
        }

}
    class SharedResource {
        private int count = 0;

        public void increment() {
            count + + ;
        }

        public int getCount() {
            return count;
        }
    }

    class MyRunnable implements Runnable {
        private final SharedResource sharedResource;

        public MyRunnable(SharedResource sharedResource) {
            this.sharedResource = sharedResource;
        }

        @Override
        public void run() {

                for (int i = 0; i < 100; i + + ) {
                    synchronized (this) {
                        sharedResource.increment();
                        System.out.println(Thread.currentThread().getName() + "|" + sharedResource.getCount());

                        try {
                            Thread.sleep(5);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }


        }
    }

The above is to use the synchronized keyword to lock the code block. In synchronized(), you need to add a monitor for it. This monitor needs to fetch the same object in different threads, so in the above example, you can use this to fetch it. to the myRunnable object or use the shareResource object as the monitor. It should be noted that if different threads obtain different monitors, then this lock cannot play a thread-safe role. For example, if you create two Thread objects and the monitor uses this, then this lock cannot play a thread-safe role.

public class ThreadSafe {

        public static void main(String[] args) {
            SharedResource sharedResource = new SharedResource();
            MyRunnable myRunnable = new MyRunnable(sharedResource);
            Thread thread1 = new Thread(myRunnable,"Thread1");
            Thread thread2 = new Thread(myRunnable,"Thread2");
            thread1.start();
            thread2.start();
        }

}
    class SharedResource {
        private int count = 0;

        public void increment() {
            count + + ;
        }

        public int getCount() {
            return count;
        }
    }

    class MyRunnable implements Runnable {
        private final SharedResource sharedResource;

        public MyRunnable(SharedResource sharedResource) {
            this.sharedResource = sharedResource;
        }

        @Override
        public void run() {

                for (int i = 0; i < 100; i + + ) {
                    Resourceincrement(sharedResource);
                    System.out.println(Thread.currentThread().getName() + "|" + sharedResource.getCount());
                        try {
                            Thread.sleep(5);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }

                }

        }
         public synchronized void Resourceincrement(SharedResource sharedResource){
            sharedResource.increment();
         }
    }


The above code uses the synchronized keyword to set a lock for the method. You can use the synchronized keyword to modify methods that need to access public resources. It will automatically add a this monitor, so you need to pay attention when using it.

public void run() {
            Lock lock = new ReentrantLock();
                for (int i = 0; i < 100; i + + ) {
                    lock.lock();
                    try {
                            Thread.sleep(5);
                            sharedResource.increment();
                            System.out.println(Thread.currentThread().getName() + "|" + sharedResource.getCount());
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }finally {
                        lock.unlock();
                    }

                }

        }

You can also use Lock to manually add a lock. Lock is an interface under the JUC (java.util.concurrent) package. It is a thread synchronization tool introduced in Java 1.5 to ensure safe access to shared resources under multi-threads.

When accessing shared resources under multi-threading, lock before access and unlock after access. The unlocking operation is generally placed in the finally block.

The bottom layer of Lock uses the AQS abstract queue synchronizer, which mainly uses several methods to maintain the use of locks.

tryAcquire: Will try to acquire a lock through CAS.

addWaiter: Add the current thread to the doubly linked list (waiting queue)

acquireQueued: Through spin, determine whether the current queue node can acquire the lock.

lock makes heavy use of CAS + spin. Therefore, according to the characteristics of CAS, lock is recommended to be used in situations with low lock conflicts. At present, after Java 1.6, the official has done a lot of lock optimization for synchronized (bias lock, spin, lightweight lock). When not necessary, it is recommended to use synchronized for synchronization operations.

Deadlock

Deadlock refers to two or more threads waiting for each other to release resources and are in a state where they cannot continue to execute. To avoid deadlock, you can use the following methods:

  1. Avoid multiple threads holding multiple locks at the same time.

  2. Unify the lock acquisition order to prevent different threads from acquiring locks in different orders.

  3. Try to narrow the scope of the lock as much as possible, avoid occupying the lock for a long time, and release unnecessary locks as soon as possible.

  4. Try to avoid calling other uncontrolled methods while holding a lock.