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

synchronized

public class SimpleTest {
    public static void main(String[] args) {
        synchronized (SimpleTest.class) {
            System.out.println("hello world"); }}}Copy the code
  • It’s a very simple one up heresynchronizedThe use of. Let’s look at the corresponding bytecode section

  • synchronizedIn fact, the bytecode layer isMONITORENTERandMONITOREXIT; It is worth noting that before the MONITOR instruction we had an instruction on the operation stack. That’s what we aresynchronizedWhat follows the parentheses

  • If we change it a little bit, we can see that the object is different. We can understand the concept of locking from a stack perspective.

  • References to the keyword concurrency synchronized tend to linger, and we overcame the keyword volatile above. That leaves synchronized. First let’s look at the concept

  • Synchronized locks. In multithreaded development, synchronized code can only be owned by one thread at a time, and the modified code cannot be interrupted. Serial execution in multithreading is guaranteed. We can think of it as atomicity

  • We will not expand the difference between synchronized and Lock. To summarize, the former is a Java language keyword, the latter is a Java class; In addition, the former native lock the latter need to unlock and unlock themselves.

  • The synchronized keyword modifies objects in the following situations

Modify the object role
The code block The code block being decorated is a synchronized code block. The scope of the code block lock depends on the modifier object
Common methods Synchronizing a method locks the object on which the method is called
A static method Synchronizing a method locks the Class object on which the method is called
decorator Lock the Class object
Modify the object Lock Java objects
  • The summary above is quite abstract. Actually,synchronizedThere are only two cases where the scope of the lock is different
  • One is to lock the Class object. One is to lock Java objects.

Lock Java objects

class S1{
    int age=2;
}
public class SynchronizedTest {
    static int index = 0;
    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<Thread>();
        for (int i = 0; i < 10000; i++) {
            final S1 s1 = new S1();
            Thread thread = new Thread(new Runnable() {
                public void run(a) {
                    synchronized(s1) { index++; }}}); thread.start(); threadList.add(thread); }for (Thread thread : threadList) {
            try {
                thread.join();
            } catch(InterruptedException e) { e.printStackTrace(); } } System.out.println(index); }}Copy the code
  • The code above creates 10,000 threads to increment. Per-thread increment was not safe, but we added itsynchronizedIndex ++ is not interrupted, there is only one thread executing at all times and the final result is index=10000; There is a high probability that index=10000 can be found when running, but there are still some cases where index<10000;
  • Remember our column in volatile? Do you feel strange here? The index variable is not volatile, but index=10000 is highly likely; Isn’t memory invisible between threads? Index =10000 indicates that a thread has changed its value and the latest value will be synchronized by other threads. After all, threads in our code execute concurrently. It’s important to point out heresynchronizedThread visibility is also available.synchronizedEnsures that only one thread is synchronizing at all times.
  • So why does index<10000 still happen? The problem is s1.synchronized(s1)This code represents the lock s1 object. And we’re regenerating s1 objects every time so we’re locking differently every time. S1 (0x a ff) The lock on this object is invalid for s1(0x100FF). So index<10000

Lock the Class


class S1{
    int age=2;
}
public class SynchronizedTest {
    static int index = 0;
    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<Thread>();
        for (int i = 0; i < 10000; i++) {
            final S1 s1 = new S1();
            Thread thread = new Thread(new Runnable() {
                public void run(a) {
                    synchronized(S1.class) { index++; }}}); thread.start(); threadList.add(thread); }for (Thread thread : threadList) {
            try {
                thread.join();
            } catch(InterruptedException e) { e.printStackTrace(); } } System.out.println(index); }}Copy the code
  • The code is basically the same, it’s just lockedS1.classThis class information. This time each thread came in and their faith came from the same place. Equivalent to the first case, each lock answers to a different leader. In the second case, each lock answers to the same leader. Then the previous lock will affect the later lock and the later lock will fail and wait in the queue.
  • Modifying ordinary methods is equivalent to locking objects; Modifying static methods is equivalent to locking Class.

DCL and volatile

  • DCL (Double Check Lock) refers to the single-column mode, I believe that many people’s single-column mode is the difference between lazy and hungry. I also recommend the hungry Man single-column mode because it sacrifices memory for the benefit of security and reliability in high concurrency scenarios. Why is the single column pattern and volatile relevant? That’s the problem with slobs
public class OnFactory {
    private static OnFactory onFactory;

    public static OnFactory getInstance(a) {
        if (null == onFactory) {
            onFactory = new OnFactory();
        }
        returnonFactory; }}Copy the code
  • The single column mode above should be a common lazy single column mode. So let’s go ahead and get it and see if we get the same object each time.

public class FactoryTest {
    public static void main(String[] args) {
        Set<OnFactory> sets = new HashSet<OnFactory>();
        for (int i = 0; i < 1000; i++) { sets.add(OnFactory.getInstance()); } System.out.println(sets.size()); }}Copy the code
  • The final output is of size 1, indicating that our single-column mode is fine. You’d be naive to think there’s no problem. There’s no problem that it’s a thread. But there are problems when accessing multiple threads.
  • The problem is when multiple threads are executing the judgment logic at the same timeonFactory==nullSo these threads are going to create objects. So it’s not a single column anymore.

public class OnFactory {
    private static volatile OnFactory onFactory;

    public static OnFactory getInstance(a) {
        if (null == onFactory) {
            synchronized (OnFactory.class) {
                if (null == onFactory) {
                    onFactory = newOnFactory(); }}}returnonFactory; }}Copy the code
  • This is the double check implemented by using synchronized and volatile together.

    doubt

  • Do you have such a question after reading the double check above? It seems that volatile is not so important in double Check. Because synchronized also has memory visibility, it ensures that variables are synchronized between threads and that data assignments are serial operations due to locking. Why volatile? That leaves one remaining feature of volatile, which disables instruction reordering

  • Where is the instruction reorder in double check? The new OnFactory is not atomic, so let’s look at the compiled bytecode

  • Bytecode analysis shows that a new object actually corresponds to four instructions.
instruction role
NEW Creates an object and pushes its reference to the top of the stack
DUP Copy the value at the top of the stack and push it to the top of the stack.
INVOKESPECIAL Call superclass constructor, instance initializer, private method
ASTORE Stores the top of the stack reference value to the specified local variable
  • That is to say wenew OnFactoryA line of code actually has four instructions; If we do not volatile then instruction reordering will occur. Let’s see, NEW is the root of the following three instructions, so NEW definitely doesn’t reorder. One is to record the reference location, one is to initialize the memory data of the reference location, and one is to store the reference address in the local variable table. It’s like one person doing your name registration, one person decorating you, one person cleaning out your photos. These three are completely out of order so reordering is going to happen.

  • When an instruction reorder occurs, it is likely that another thread will get the object, but the object has not been initialized, and we need to use uninitialized data, and an error will be reported. But one detail here is that the CPU is very fast, and it’s very difficult to simulate reordering when we’re just creating an object in the code above. For the sake of simulation, I’m going to sleep before creating the object so that the CPU can easily reorder the instructions. We can easily have semi-finished objects without volatile
public class OnFactory {
    private Integer age=null;
    private static OnFactory onFactory;

    public Integer getAge(a) {
        return age;
    }

    public static void setInstance(a) {
        onFactory = null;
    }
    public static OnFactory getInstance(a) {
        if (null == onFactory) {
            synchronized (OnFactory.class) {
                if (null == onFactory) {
                    //System.out.println("init......" );
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    onFactory = new OnFactory();
                    onFactory.age=8; }}}returnonFactory; }}Copy the code

public class FactoryTest {
    public static void main(String[] args) {
        while (true){
            OnFactory.setInstance();
            List<Thread> threadList = new ArrayList<Thread>();
            final Map<OnFactory,Object> map = new Hashtable<OnFactory,Object>();
            for (int i = 0; i < 1000; i++) {
                Thread thread = new Thread(new Runnable() {
                    public void run(a) {
                        final OnFactory instance = OnFactory.getInstance();
// System.out.println(instance);map.put(instance, instance.getAge().toString()); }}); thread.start(); threadList.add(thread); }for (Thread thread : threadList) {
                try {
                    thread.join();
                } catch(InterruptedException e) { e.printStackTrace(); }}if (map.size() > 1) { System.out.println(map.size()); }}}}Copy the code
  • The above is a modified version of the error, intended to simulate the dangers of a DCL without volatile

  • Another thing I didn’t test was using the M1 JDK with the macbook Pro 2021 using the same code. Zulu JDK or MAC memory is too fast.

conclusion

  • All in all, highly concurrent Java programming is not as easy as we think. We have only theoretically analyzed how to avoid concurrency problems, but in practice it is often impossible to lock resources using synchronized alone. Even if we segmented locking, it still causes system bottleneck. Meanwhile, in distributed locking, we also have distributed locking to replace synchronized. About distributed locking, I introduced the advantages and disadvantages of redis, ZooKeeper and mysql.
  • Back to the topic synchronized is a lock that is supported natively in Java. One of the biggest advantages of synchronized is that it does not require manual management of locks and locks are automatically released. Much simpler than Lock. Volatile is a key that is rarely used in development. But an object that cannot be ignored in concurrency, keeping memory visible and disallowing reordering