Home  >  Article  >  Java  >  Detailed explanation of deadlock and memory usage issues of synchronized keyword in Java

Detailed explanation of deadlock and memory usage issues of synchronized keyword in Java

高洛峰
高洛峰Original
2017-01-05 16:10:341971browse

Let’s first look at a detailed explanation of synchronized:
synchronized is a keyword in the Java language. When it is used to modify a method or a code block, it can ensure that at most one thread executes the code at the same time.

1. When two concurrent threads access the synchronized(this) synchronization code block in the same object object, only one thread can be executed at a time. Another thread must wait for the current thread to finish executing this code block before it can execute this code block.

2. However, when a thread accesses a synchronized (this) synchronized code block of object, another thread can still access the non-synchronized (this) synchronized code block in the object.

3. What is especially critical is that when a thread accesses a synchronized (this) synchronized code block of object, other threads will be blocked from accessing all other synchronized (this) synchronized code blocks in object.

4. The third example is also applicable to other synchronization code blocks. That is to say, when a thread accesses a synchronized(this) synchronized code block of object, it obtains the object lock of this object. As a result, other threads' access to all synchronized code parts of the object object is temporarily blocked.

5. The above rules are also applicable to other object locks.
Simply put, synchronized is to declare a lock for the current thread. The thread that owns this lock can execute the instructions in the block, and other threads You can only wait to acquire the lock, and then perform the same operation.
This is very useful, but the author encountered another strange situation.
1. In the same class, there are two methods that use synchronized Keyword declaration
2. When one of the methods is executed, it needs to wait for the other method (asynchronous thread callback) to be executed, so a countDownLatch is used to wait
3. The code is deconstructed as follows:

synchronized void a(){
 countDownLatch = new CountDownLatch(1);
 // do someing
 countDownLatch.await();
}
 
synchronized void b(){
   countDownLatch.countDown();
}

Among them,
a method is executed by the main thread, and method b is called back after being executed by the asynchronous thread.
The execution result is:
The main thread starts to get stuck after executing method a, and no longer continues. It doesn't matter how long you wait.
This is a very classic deadlock problem
a is waiting for b to execute. In fact, regardless of whether b is a callback, b is also waiting for a to execute. Why? synchronized works .
Generally speaking, when we want to synchronize a block of code, we need to use a shared variable to lock it, for example:

byte[] mutex = new byte[0];
 
void a1(){
   synchronized(mutex){
     //dosomething
   }
}
 
void b1(){
 
   synchronized(mutex){
     // dosomething
   }
 
}

If the contents of method a and method b are migrated to a1 and It is easy to understand in the synchronized block of the b1 method.
After a1 is executed, it will indirectly wait (countDownLatch) for the b1 method to be executed.
However, since the mutex in a1 has not been released, it starts to wait for b1. This At this time, even if it is an asynchronous callback to the b1 method, the b method will not be executed because it needs to wait for the mutex to release the lock.
This causes a deadlock!
The synchronized keyword here has the same effect when placed in front of the method. It’s just that the Java language helps you hide the declaration and use of mutex. The mutex used by the synchronized method in the same object is the same. , so even asynchronous callbacks can cause deadlocks, so pay attention to this problem. This level of error is due to improper use of the synchronized keyword. Don't use it indiscriminately, and use it correctly.
So such an invisible mutex object What exactly is it?
It’s easy to think of the instance itself. Because in this way, there is no need to define a new object and lock it. In order to prove this idea, you can write a program to prove it.
The idea is very simple, definition A class has two methods, one method is declared as synchronized, one uses synchronized(this) in the method body, and then starts two threads to call these two methods respectively. If a lock competition occurs between the two methods (wait ), it can be explained that the invisible mutex in the synchronized method declaration is actually the instance itself.

public class MultiThreadSync {
 
  public synchronized void m1() throws InterruptedException{
     System. out.println("m1 call" );
     Thread. sleep(2000);
     System. out.println("m1 call done" );
  }
 
  public void m2() throws InterruptedException{
     synchronized (this ) {
       System. out.println("m2 call" );
       Thread. sleep(2000);
       System. out.println("m2 call done" );
     }
  }
 
  public static void main(String[] args) {
     final MultiThreadSync thisObj = new MultiThreadSync();
 
     Thread t1 = new Thread(){
       @Override
       public void run() {
         try {
           thisObj.m1();
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
       }
     };
 
     Thread t2 = new Thread(){
       @Override
       public void run() {
         try {
           thisObj.m2();
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
       }
     };
 
     t1.start();
     t2.start();
 
  }
 
}

The result output is:

m1 call
m1 call done
m2 call
m2 call done

It means that the sync block of method m2 is waiting for the execution of m1 . This can confirm the above assumption.
Another thing to note is that when sync is added to a static method, since it is a class-level method, the locked object is the class instance of the current class. Similarly You can also write a program to prove it. This is omitted here.
So the synchronized keyword of the method can be automatically replaced with synchronized(this){} when reading, which is easy to understand.

                    void method(){
void synchronized method(){         synchronized(this){
   // biz code                // biz code
}               ------>>>   }
                    }

By Synchronized Let’s talk about memory visibility
In Java, we all know that the keyword synchronized can be used to implement mutual exclusion between threads, but we often forget that it has another role, which is to ensure that variables are visible in memory. Performance - that is, when two threads, reading and writing, access the same variable at the same time, synchronized is used to ensure that after the writing thread updates the variable, the reading thread can read the latest value of the variable when it accesses the variable again.

For example, take the following example:

public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      while (!ready) {
        Thread.yield(); //交出CPU让其它线程工作
      }
      System.out.println(number);
    }
  }
 
  public static void main(String[] args) {
    new ReaderThread().start();
    number = 42;
    ready = true;
  }
}

What do you think the reading thread will output? 42? Under normal circumstances, 42 will be output. However, due to reordering issues, the reading thread may output 0 or nothing.

We know that the compiler may reorder the code when compiling Java code into bytecode, and the CPU may also reorder its instructions when executing machine instructions. Sorting does not break the semantics of the program -

在单一线程中,只要重排序不会影响到程序的执行结果,那么就不能保证其中的操作一定按照程序写定的顺序执行,即使重排序可能会对其它线程产生明显的影响。
这也就是说,语句"ready=true"的执行有可能要优先于语句"number=42"的执行,这种情况下,读线程就有可能会输出number的默认值0.

而在Java内存模型下,重排序问题是会导致这样的内存的可见性问题的。在Java内存模型下,每个线程都有它自己的工作内存(主要是CPU的cache或寄存器),它对变量的操作都在自己的工作内存中进行,而线程之间的通信则是通过主存和线程的工作内存之间的同步来实现的。

比如说,对于上面的例子而言,写线程已经成功的将number更新为42,ready更新为true了,但是很有可能写线程只同步了number到主存中(可能是由于CPU的写缓冲导致),导致后续的读线程读取的ready值一直为false,那么上面的代码就不会输出任何数值。

而如果我们使用了synchronized关键字来进行同步,则不会存在这样的问题,

public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
  private static Object lock = new Object();
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      synchronized (lock) {
        while (!ready) {
          Thread.yield();
        }
        System.out.println(number);
      }
    }
  }
 
  public static void main(String[] args) {
    synchronized (lock) {
      new ReaderThread().start();
      number = 42;
      ready = true;
    }
  }
}

这个是因为Java内存模型对synchronized语义做了以下的保证,

即当ThreadA释放锁M时,它所写过的变量(比如,x和y,存在它工作内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工作内存会被设置为无效,然后ThreadB会重新从主存中加载它要访问的变量到它的工作内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。通过这样的方式来实现ThreadA到ThreadB的线程间的通信。

这实际上是JSR133定义的其中一条happen-before规则。JSR133给Java内存模型定义以下一组happen-before规则,

单线程规则:同一个线程中的每个操作都happens-before于出现在其后的任何一个操作。

对一个监视器的解锁操作happens-before于每一个后续对同一个监视器的加锁操作。

对volatile字段的写入操作happens-before于每一个后续的对同一个volatile字段的读操作。

Thread.start()的调用操作会happens-before于启动线程里面的操作。

一个线程中的所有操作都happens-before于其他线程成功返回在该线程上的join()调用后的所有操作。

一个对象构造函数的结束操作happens-before与该对象的finalizer的开始操作。

传递性规则:如果A操作happens-before于B操作,而B操作happens-before与C操作,那么A动作happens-before于C操作。

实际上这组happens-before规则定义了操作之间的内存可见性,如果A操作happens-before B操作,那么A操作的执行结果(比如对变量的写入)必定在执行B操作时可见。

为了更加深入的了解这些happens-before规则,我们来看一个例子:

//线程A,B共同访问的代码
Object lock = new Object();
int a=0;
int b=0;
int c=0;
 
//线程A,调用如下代码
synchronized(lock){
  a=1; //1
  b=2; //2
} //3
c=3; //4
 
 
//线程B,调用如下代码
synchronized(lock){ //5
  System.out.println(a); //6
  System.out.println(b); //7
  System.out.println(c); //8
}

我们假设线程A先运行,分别给a,b,c三个变量进行赋值(注:变量a,b的赋值是在同步语句块中进行的),然后线程B再运行,分别读取出这三个变量的值并打印出来。那么线程B打印出来的变量a,b,c的值分别是多少?

根据单线程规则,在A线程的执行中,我们可以得出1操作happens before于2操作,2操作happens before于3操作,3操作happens before于4操作。同理,在B线程的执行中,5操作happens before于6操作,6操作happens before于7操作,7操作happens before于8操作。而根据监视器的解锁和加锁原则,3操作(解锁操作)是happens before 5操作的(加锁操作),再根据传递性 规则我们可以得出,操作1,2是happens before 操作6,7,8的。

则根据happens-before的内存语义,操作1,2的执行结果对于操作6,7,8是可见的,那么线程B里,打印的a,b肯定是1和2. 而对于变量c的操作4,和操作8. 我们并不能根据现有的happens before规则推出操作4 happens before于操作8. 所以在线程B中,访问的到c变量有可能还是0,而不是3.


更多详解Java中synchronized关键字的死锁和内存占用问题相关文章请关注PHP中文网!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn