Welcome to pay attention to the public number [Ccww technology blog], original technical articles launched at the first time

Previous articles:

  • “Improve your ability, raise your salary” – The full AQS of Java Concurrency
  • Java Multithreaded concurrency series
  • .

Introduction of Synchronized

Thread safety is very important in concurrent programming. The main causes of thread safety problems are:

  • Critical resources, with shared data
  • Multiple threads work together to share data

The Java keyword synchronized provides support for preventing critical resource access conflicts in multi-threaded scenarios, ensuring that only one thread can execute a method or a code block operation to share data at the same time.

That is, when executing code using the synchronized keyword, it checks to see if the lock is available, obtains the lock, executes the code, and finally releases the lock. Synchronized can be used in three ways:

  • Synchronized method: synchronized current instance object, before entering the synchronization code to obtain the current instance lock
  • Synchronized static method: the synchronized class object of the current class. The lock of the current class object is acquired before entering the synchronized code
  • Synchronized block: An object in synchronized parentheses that locks a given object and acquires the lock before entering the synchronized library

Synchronized methods

The synchronized keyword is not used as follows:

public class ThreadNoSynchronizedTest {

    public void method1(a){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method1");
    }

    public void method2(a) {		
        System.out.println("method2");
    }

    public static void main(String[] args) {
        ThreadNoSynchronizedTest  tnst= new ThreadNoSynchronizedTest();

        Thread t1 = new Thread(new Runnable() {			
            @Override
            public void run(a) { tnst.method1(); }}); Thread t2 =new Thread(new Runnable() {			
            @Override
            public void run(a) { tnst.method2(); }}); t1.start(); t2.start(); }}Copy the code

In the code above, method1 has 2s more latency than method2, so in the case of t1 and T2 threads executing simultaneously, the result is:

method2

method1

When using the synchronized keyword in method1 and method2, the code looks like this:

public synchronized void method1(a){
	try {
		Thread.sleep(2000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	System.out.println("method1");
}
	
public synchronized void method2(a) {		
	System.out.println("method2");
}
Copy the code

Method2 must wait for method1 to finish executing because method1 holds the lock.

method1

method2

Therefore, synchronized lock is the current object, and the synchronized method of the current object can only execute one of them at the same time. The other synchronized methods need to suspend and wait, but the execution of non-synchronized methods is not affected. The following synchronized methods are equivalent to synchronized code blocks (enclosing the entire method synchronized(this)).

public synchronized void method1(a){}public  void method2(a) {		
	synchronized(this) {}}Copy the code

Synchronized static method

Synchronized static methods are methods that act on the entire class, equivalent to the class as a lock, the code example is as follows:

public class TreadSynchronizedTest {

    public static synchronized void method1(a){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        System.out.println("method1");
    }

    public static void method2(a) {		
        synchronized(TreadTest.class){
            System.out.println("method2"); }}public static void main(String[] args) {		
        Thread t1 = new Thread(new Runnable() {			
            @Override
            public void run(a) { TreadSynchronizedTest.method1(); }}); Thread t2 =new Thread(new Runnable() {			
            @Override
            public void run(a) { TreadSynchronizedTest.method2(); }}); t1.start(); t2.start(); }}Copy the code

Because class is used as a lock, method1 and method2 compete, and synchronized(threadtest.class) in method2 is the same as adding synchronized directly to void in the declaration of method2. The result of executing the above code is still the result of printing method1 first:

method1

method2

Synchronized code block

Synchronized code block is applied to code blocks that process critical resources. Codes that do not need access to critical resources do not need to compete for resources, which reduces competition among resources and improves code performance. Example code is as follows:

public class TreadSynchronizedTest {

    private Object obj = new Object();

    public void method1(){
        System.out.println("method1 start");
    	synchronized(obj){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("method1 end");
         }
    }

    public void method2() {
        System.out.println("method2 start"); Thread.sleep(10); thread.sleep (10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized(obj){ System.out.println("method2 end");
        }
    }

    public static void main(String[] args) {
        TreadSynchronizedTest tst = new TreadSynchronizedTest();
        Thread t1 = new Thread(new Runnable() {			
            @Override
            public void run() { tst.method1(); }}); Thread t2 = new Thread(newRunnable() {			
            @Override
            public void run() { tst.method2(); }}); t1.start(); t2.start(); }}Copy the code

The result is as follows:

method1 start

method2 start

method1 end

method2 end

In the code above, I’m going to print method2 start, and then I’m going to print the sync block. Because obj is now in method1, method2 has to wait until method1 completes, so I’m going to print method1 end. And then I’m printing method2 end.

Principle of Synchronized

Synchronized is a lock implemented by the JVM where the lock is acquired and released by the Monitorenter and Monitorexit directives, respectively.

Monitorenter and Monitorexit, as well as the ACC_SYNCHRONIZED flag, are added to the bytecode file.

When the method is invoked, the calling instruction will check whether the ACC_SYNCHRONIZED access flag of the method is set. If so, the executing thread will acquire monitor first, execute the method body after the method is successfully obtained, and release monitor after the method is executed.

During method execution, the same Monitor object is no longer available to any other thread. There is essentially no difference, except that method synchronization is done implicitly, without bytecode.

After Java1.6, sychronized implementation is divided into biased lock, lightweight lock and heavyweight lock. Biased lock is enabled by default in Java1.6, while lightweight lock will expand to heavyweight lock in the case of multi-threaded competition, and the data related to lock is stored in the object header.

  • Biased lock: Used when only one thread accesses a synchronized block, the lock is acquired by CAS operation
  • Lightweight locks: Biased locks are upgraded to lightweight locks when there are multiple threads alternately accessing fast synchronization. When a thread fails to acquire a lightweight lock, there is a race, the lightweight lock expands to a heavyweight lock, the current thread spins (continuously acquiring the lock through CAS operations), and subsequent threads that acquire the lock directly block.
  • Heavyweight lock: Failure to acquire the lock causes the thread to block directly, thus causing a thread context switch, with the worst performance.

Lock Optimization – Adaptive Spinning

As we know from the lightweight lock acquisition process, when a thread fails to perform a CAS operation in the lightweight lock acquisition process, it obtains the heavyweight lock through spin. The problem is that spin is cpu-consuming, and if the lock is never acquired, the thread will always be spinning, wasting CPU resources.

One of the easiest ways to solve this problem is to specify the number of spins, such as loop it 10 times, and block if the lock has not been acquired. But the JDK takes a smarter approach — adaptive spin, which simply means that a thread spins more next time if it succeeds, and less if it fails.

Lock Optimization – Lock Coarsening

The concept of lock coarsening should be easy to understand. It is to merge multiple locking and unlocking operations into one, and expand multiple consecutive locks into a larger range of locks. Here’s an example:

public class StringBufferTest {
     StringBuffer stringBuffer = new StringBuffer();
     public void append(a){
         stringBuffer.append("a");
         stringBuffer.append("b");
         stringBuffer.append("c"); }}Copy the code

Each call to stringBuffer.append requires locking and unlocking. If the virtual machine detects a series of locking and unlocking operations on the same object in a row, it will combine them into a larger locking and unlocking operation at the first append. Unlock after the last append method.

Lock Optimization – Lock Elimination

Lock elimination is the removal of unnecessary lock operations. According to code escape, a piece of code is considered thread-safe and does not need to be locked if it is determined that no data on the heap will escape from the current thread. Take a look at this program:

public class SynchronizedTest02 {

     public static void main(String[] args) {
         SynchronizedTest02 test02 = new SynchronizedTest02();        
         for (int i = 0; i < 10000; i++) {
             i++;
         }
         long start = System.currentTimeMillis();
         for (int i = 0; i < 100000000; i++) {
             test02.append("abc"."def");
         }
         System.out.println("Time=" + (System.currentTimeMillis() - start));
     }

     public void append(String str1, String str2) {
         StringBuffer sb = newStringBuffer(); sb.append(str1).append(str2); }}Copy the code

Although the append of a StringBuffer is a synchronous method, the StringBuffer in this program is a local variable and does not escape from the method, so it is thread safe to remove the lock.

Sychronized shortcomings

Sychronized makes the resources that are not locked enter the Block state, and then switch to the Running state after the resources are contested. This process involves switching between the user mode and kernel mode of the operating system, and costs a lot.

Java1.6 has been optimized for synchronized, increasing the transition from bias to lightweight to heavyweight locks, but performance remains low after the final shift to heavyweight locks.

Is everyone still ok? If you like, move your hands to show 💗, point a concern!! Thanks for your support!

Welcome to pay attention to the public number [Ccww technology blog], original technical articles launched at the first time