“This is the 10th day of my participation in the First Challenge 2022. For details: First Challenge 2022.”

Today, I would like to talk about my experience in learning Synchronized. Java provides a series of keywords related to concurrent processing in order to solve the problems of atomicity, visibility and order in concurrent programming, including Synchronized. Simply put, this keyword is used to ensure that the class or object it applies to is thread-safe in the case of multiple threads.

Let’s start with an example:

class synchronizedTest {
    public static int inc = 0;

    public void increase(a) {
        inc++;
    }

    public static void main(String[] args) {
        synchronizedTest test = new synchronizedTest();
        for (int i = 0; i < 10; i++) {
        // Initialize 10 threads to perform summation
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) test.increase(); System.out.println(test.inc); }).start(); }}}Copy the code

In this example, we initialize a class and use ten threads to execute the increase() method of the class. Normally, the result should be 10000, but the result is usually less than 10000.

And you can see it’s not going to be 10000, because the in ++ operation is not atomic, it’s actually

inc=inc+1

So this is done in three parts, first take the value of inc, then add 1 to the value, and finally assign the value to inc. This results in the case of multi-threading: If two or more threads reach inc with the same value at the same time, add 1 and assign the value to it. The result is that the value of INC is smaller than expected. What is the solution? Add the synchronized keyword to the increase() method.

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

It can be seen that the final result is correct. The role of synchronized keyword is to ensure that INC is thread-safe. Only one thread can operate it at any time, so there will not be repeated operations like before.

B. synchronized C. synchronized D. synchronized

In the previous example, I used the keyword synchronized to modify a common method. When synchronized modifies a common method, the modified method is called a synchronized method. Its scope is the whole method, and the object is the thread that calls the method. In addition, synchronized can be used to modify static methods and code blocks.

A static method

Decorates a static method whose scope is the entire method and whose object is the thread calling the class.

/** * synchronized static methods */
public static synchronized void staticMethod(a) {
    / /...
}
Copy the code

The code block

A locking object decorated in a code block can be either an object or a class, with some differences

synchronized (xxxx.class||this) { / /... }
Copy the code

To see what the difference is, let’s first look at the instance of the same class being called by multiple threads:

class synchronizedTest {
    
    public synchronized void increase(a) {
        synchronized (synchronizedTest.class) {
            System.out.println(String.format("Current thread :" + Thread.currentThread().getName() +
                    "Execution time :" + new Date()));
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch(InterruptedException e) { e.printStackTrace(); }}}public static void main(String[] args) {
        synchronizedTest test = new synchronizedTest();
        for (int i = 0; i < 10; i++) {
            newThread(() -> { test.increase(); }).start(); }}}Copy the code

The result is obvious: all ten threads share the same lock, and only one thread can call increase() at a time

How about ten threads calling different objects? Modify the code to create class objects in each thread when calling methods:

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            synchronizedTest test = newsynchronizedTest(); test.increase(); }).start(); }}Copy the code

The result is the same as before, which means it’s a different object but it’s still the same lock.

Change the class object to this and do the same:

public synchronized void increase(a) {
    synchronized (this) {
        System.out.println(String.format("Current thread :" + Thread.currentThread().getName() +
                "Execution time :" + new Date()));
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code

The result is the same when you operate on the same object, but the result changes when you operate on different objects

As you can see, each thread has a separate lock, so synchronized classes use the same lock whenever an object is shared or multiple objects are created. Synchronized this locks only the same object. The locks are different between different objects.

Two, the implementation principle

Monitor is an internal object that synchronizes methods and code blocks by entering and exiting the monitor object. At the bytecode level, it is implemented by monitorenter and Monitorexit directives, which ultimately rely on the operating system’s Mutex lock. In my opinion, synchronization should be the sequential access of multiple threads to the locked code block or the object that holds the lock, but I think it is more graphic than synchronization, which is mutually exclusive access to resources, that is, can only be held by a single thread at a time.

Monitorenter inserts at the start of the Synchronized block, monitoreXit inserts at the end of the block, and monitorexit inserts at the exception. Make sure that the lock can be released if an exception occurs.

This article covers Monitor in detail. An important feature of Monitor is that only one thread can enter the critical region (the region modified by Synchronized) defined in Monitor at any one time, which allows monitor to achieve mutual exclusion. Other threads can also block and wake up.

Each object has its own Monitor lock, Monitor. Threads can only execute code if they acquire the lock. As can be seen from the following figure, when a thread wants to acquire the monitor lock for operation, it will enter the EntrySet first and wait. If it gets the lock, it will become the owner. After owning the lock, the thread can do what it wants. The first one is when the task is done, you can exit and give the owner to another thread, or the second one is when the task is not done but it’s interrupted by some external condition and then the thread is put into a WaitSet and waits until it has a chance to pick up the lock and finish the task.

The general process is as follows:

  1. When we enter a method, monitorenter takes ownership of the current monitor. The number of monitor entries is 1, and the current thread is the owner of the monitor.
  2. If you are already the owner of the monitor, if you enter the monitor again, the number of entries will be +1. If you are not the owner and do not have ownership, you will be handed over to the operating system, but this will cause the operating system mode conversion to incur large overhead.
  3. Similarly, when monitorexit is finished, the number of entries to monitorexit will be -1 until it reaches 0 before it can be held by another thread.

Java object header

Synchronized is called a heavyweight lock because it involves switching operating system modes. It does not perform very well, but Synchronized was optimized in JDk1.6. Before learning about optimization, learn about Java object headers. Object composition is divided into three areas: object header, instance data, and alignment fill.

1. The first object

The first part is the Mark Word, the second part is the class Pointer, and the third part is the length of the array if the object type is array.

Mark Word: Stores a lot of information about the object, as shown below:

You can see that the information about the lock carried by the object is stored in MarkWord. This part of the record data is constantly changing according to the state of the lock.

Klass Point: A pointer to an object’s class metadata that the virtual machine uses to determine which class the object is an instance of.

2. Instance data

Instance data is the area of data that an object actually stores, the contents of various types of fields.

3. Align the fill

This part does not have to exist, but serves as a placeholder, mainly because memory management requires that the size of the object must be multiples of 8 bytes, and the object header is exactly that, but the instance data is not necessarily that, so it needs to be aligned to fill completion. So that’s exactly 8 bytes for an empty object.

4. Upgrade the lock

Before JDK1.6, Synchronized was considered as a heavyweight lock and its efficiency was not very high. Therefore, in 1.6, a wave of optimization was launched to upgrade the lock. Lightweight lock and biased lock were generated before heavyweight lock, which enabled spin lock by default and would not cause thread blocking. So the performance is naturally better than the heavyweight lock, but I have limited ability to say very clearly.

For details about the upgrade process, see this article: # Lock upgrade process (partial/lightweight/heavyweight)

And this article makes biased locking very clear: # Difficult biased locking has finally been removed by Java