Java Concurrent Programming Notes - Chapter 2 Thread Safety

public class UnsafeCountingFactorizer implements Servlet{
    private long count = 0;
    public long getCount(){ return count;}
    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigIntefer[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp, factors);
    }
}

Unfortunately, the UnsafeCountingFactorizer is not thread-safe, and the following situations are possible in the unsafe code shown below:

   每个线程读到的值都为9, 接着执行递增操作,并且都将计数器的初始值设为10,发生了递增操作丢失的情况

Note: The same thread class UnsafeCountingFactorizer, the same object reference of this class, and then use the object reference to create multiple thread Instances

Unsafe code:

UnsafeCountingFactorizer factorizer = new UnsafeCountingFactorizer();
// 下面的两个线程启动后,可能会导致问题
new Thread(factorizer).start();
new Thread(factorizer).start();

To summarize:

==The essence of the "check first and then execute" type of race condition==——Based on a potentially invalid observation structure to make a judgment or perform a certain calculation, under the current race condition, after observing the structure, observe The result may become invalid (modified by another thread), so various problems can arise (unexpected exception, data overwritten, file corrupted, etc.)

  1. Built-in locks are mutually exclusive synchronization methods

Java provides a built-in lock mechanism to achieve synchronization through mutual exclusion. After the synchronized keyword is compiled, two bytecode instructions, monitorenter and monitorexit, are formed before and after the synchronization block. Both bytecodes require A parameter of type reference to specify the object to lock and unlock.
If the synchronized in the Java program explicitly specifies the object parameter, it is the reference of the object ; if it is not specified, the corresponding object instance or Class object is taken as the lock object according to whether the synchronized modification is an instance method or a class method .

According to the requirements of the virtual machine specification, when executing the monitorenter command, first try to acquire the lock of the object. If the object is not locked, or the current thread already owns the lock on that object, the lock counter is incremented by 1. Accordingly, the lock counter is decremented by 1 when the monitorexit instruction is executed. When the counter is 0, the lock is released. . If the acquisition of the object lock fails, the current thread will block and wait until the object lock is released by another thread

In the behavior description of monitorenter and monitorexit in the virtual machine specification, there are two points that need special attention:

  1. The synchronized synchronized block is reentrant for the same thread, and there is no problem of locking itself
  2. A synchronized block blocks the entry of other threads after the incoming thread finishes executing. As mentioned in Chapter 12, Java threads are mapped to the native threads of the operating system. If you want to block or wake up a thread, you need the operating system to help you. This requires switching from user mode to kernel mode. Therefore, state transitions require a lot of processor events. For simple synchronized blocks (such as getter() or setter() methods modified by synchronized), state transitions may take longer than user code execution. Therefore, synchronized is a heavyweight operation in the Java language. Experienced programmers will only use this operation when it is really necessary , and the virtual machine itself will also perform some optimizations, such as before notifying the operating system to block the thread. Add a spin waiting process to avoid frequent cutting into the core state.

In addition to synchronized, we can also use the reentrant lock in the java.until.concurrent (hereinafter referred to as JUC) package to achieve synchronization. In basic usage, ReentrantLock is very similar to synchronized, and both have the same thread reentrancy characteristics. It's just that there are some differences in the code writing. One is a mutex at the API level, and the other is a mutex at the native syntax level. But ReentrantLock has added some advanced features, mainly the following 3 items:

1. 等待可中断-指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,该特性对处理时间非常长的同步块很有帮助

2. 公平锁-多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,**ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求来使用公平锁**

3. 锁绑定多个条件-一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外增加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可

Note: The biggest impact of mutual exclusion synchronization on performance is the implementation of blocking. The operations of suspending and resuming threads need to be transferred to the kernel mode to complete, and these operations have brought a lot of pressure to the concurrent performance of the system. At the same time, the development team of the virtual machine also noticed that in many applications, the lock state of ==== shared data lasts for a short period of time ====, and it is not worthwhile to suspend and resume threads for this period of time. If the physical machine has more than one processor, allowing two or more threads to execute in parallel at the same time, we can let the thread that requested the lock "wait a little bit" later, but not give up the execution time of the processor, and see if the Whether the thread with the lock will release the lock soon, in order to let the thread wait, we only need to let the thread execute a busy loop (spin), this technology is the so-called spin lock , the spin lock is in jdk1.4.2 It has been introduced, but it is turned off by default. You can use the -XX+UseSpinning parameter to turn it on. In jdk1.6, it has been changed to be turned on by default. If the lock is occupied for a short time, the effect of spin waiting will be very Well, on the contrary, if the lock is held for a long time, the spinning thread will only consume processor resources in vain, and will not do any useful work, but will lead to performance waste. Therefore, the number of spins has exceeded the limit and still has not been successfully obtained, and the traditional method should be used to suspend the thread. The default value of the number of spins is 10 times. The user can use the parameter -XX: PreBlockSpin to change, in jdk1.6 An adaptive spin lock is introduced, which means that the spin time is no longer fixed, but is determined by the previous spin time on the same lock and the state of the owner of the lock

  1. Non-blocking synchronous
    CAS: It depends on the processor instruction Compare-and-Swap, referred to as CAS
    . The method in the class Atomic in the java.util.concurrent package uses the CAS operation of the Unsafe class. The following code implements the atomic auto-increment operation:
public class AtomicTest{
    public static AtomicInteger race = new AtomicInteger(0);
    public static void increase(){
        race.incrementAndGet()//该方法使用了Unsafe类的CAS操作
    }
    private static final int THREADS_COUNT =20;
    public static void main(String[] args) throws Exception{
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i<THREADS_COUNT; i++){
            threads[i] = new Thread(new Runnable(){
                @Override
                public void run(){
                    for(int i =0; i<10000;i++){
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(race);
    }
}

Running result: 200000

The other cannot

public class VolatileTest{
    public static volatile int race = 0;
    public static void increase(){
        race++;
    }
    private static final int THREADS_COUNT =20;
    public static void main(String[] args) throws Exception{
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i<THREADS_COUNT; i++){
            threads[i] = new Thread(new Runnable(){
                @Override
                public void run(){
                    for(int i =0; i<10000;i++){
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        //等待所有累加线程都结束
        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(race);

    }
}

Running result: Every time you run the program, the output result is different, it is a number less than 200000

  1. No synchronization
    scheme Thread Local Storage (Thread Local Storage) implements the function of thread local storage
    through the java.lang.ThreadLocal class. Each thread's Thread object has a ThreadLocalMap object, which stores a group of ThreadLocal objects.

Note: Synchronization means: when multiple threads concurrently access shared data, ensure that only one (or some, when using semaphores) shared data is used at the same time.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325615208&siteId=291194637