Thoughts on thread safety triggered by a java interview question called "the hardest in history"

1. The most difficult question in history

Recently I happened to see a java interview question called the most difficult java interview question in history. This question gave me some new thinking about thread safety. Let me share this question with you:

public

class

TestSync2

implements

Runnable

{

int
 b 
=

100
;

synchronized

void
 m1
()

throws

InterruptedException

{
        b 
=

1000
;

Thread
.
sleep
(
500
);

//6

System
.
out
.
println
(
"b="

+
 b
);

}

synchronized

void
 m2
()

throws

InterruptedException

{

Thread
.
sleep
(
250
);

//5
        b 
=

2000
;

}

public

static

void
 main
(
String
[]
 args
)

throws

InterruptedException

{

TestSync2
 tt 
=

new

TestSync2
();

Thread
 t 
=

new

Thread
(
tt
);

//1
        t
.
start
();

//2
        tt
.
m2
();

//3

System
.
out
.
println
(
"main thread b="

+
 tt
.
b
);

//4

}

@Override

public

void
 run
()

{

try

{
            m1
();

}

catch

(
InterruptedException
 e
)

{
            e
.
printStackTrace
();

}

}
}

I recommend that you don't rush to read the answer below, try to see what the answer to this question is? When I first looked at this question, my first reaction was that it was something that old iron came up with. Such a messy code call is really amazing. Of course, this is a question about multithreading. The lowest-level error is that some people are not familiar with .start() and .run. They directly think that run will occupy the main thread after .start(), so the answer is equal to:

main thread b
=
2000
b
=
2000

A more advanced error: understand start(), but ignore or don't know synchronized, where you are thinking about the use of sleep(), it is possible to get the following answer:

main thread b
=
1000
b
=
2000

All in all, I asked a lot of people, and most of them couldn't get the correct answer at the first time. In fact, the correct answer is as follows:

main thread b
=
2000

b
=
1000
or
main thread b
=
1000

b
=
1000

Before explaining this answer, I encountered a lot of these questions in interviews. I vaguely remember that when I learn C++ again, I test addresses and pointers. When I learn Java, I test i++, ++i. "a" == b is equal to True? This kind of problem is not uncommon. I think everyone knows that you can’t solve this problem by rote memorization, because there are too many changes. Therefore, to do this kind of more ambiguous problem, you must understand the meaning. Fang got the solution. Especially with multi-threading, if you don’t know the principle, not only will you fail in the interview, even if you are lucky, how you can’t handle thread safety issues well at work can only lead to losses for your company.

This question involves two points:

  • synchronized
  • Several statuses of the thread: new, runnable(thread.start()), running, blocking(Thread.Sleep())
    If you are not familiar with these few students, don't worry, I will talk about it below, let me explain the whole process:
  1. Create a new thread t, at this time thread t is in the new state.
  2. Call t.start() to put the thread in runnable state.
  3. There is a controversy here. Is the t thread executed first or tt.m2 executed first? We know that the thread t is still runnable at this time, and it has not been scheduled by the cpu at this time, but our tt.m2() is our local Method code, tt.m2() must be executed first.
  4. Execute tt.m2() to enter the synchronized code block and start to execute the code. The sleep() here is useless to confuse everyone's vision, at this time b=2000.
  5. When executing tt.m2(). There are two situations:
    Situation A: It is possible that the t thread is already executing, but because m2 enters the synchronized code block first, at this time t enters the blocking state, and then the main thread will execute the output. At this time, there is another dispute. Who will execute it first? Is the t thread executed first or the main thread? Some friends here will come up with the third point. It must be output first. Isn't t thread blocked? It must be too late to schedule the CPU? Many people have overlooked a point. Synchronized is actually a lot of optimizations after 1.6. Among them, there is a spin lock, which can ensure that you do not need to give up the CPU. It may happen that this part of the time coincides with the output of the main thread, and it is It may happen before, b is equal to 1000, at this time the main thread output will actually have two situations. 2000 or 1000.

Situation B: It is possible that t has not been executed yet, as soon as tt.m2() is executed, he will execute it. There are still two situations at this time. b=2000 or 1000

6. In either case in the t thread, 1000 will definitely be output at the end, because there is no place to modify 1000 at this time.

The whole process is as follows:
Thoughts on thread safety triggered by a java interview question called "the hardest in history"

2. Thread safety

Regarding the code of the above question, although it is difficult to appear in our actual scenario, any colleague has written a similar one. At that time, it may be your own, so I want to talk about thread safety. thing.

2.1 What is thread safety

We use a sentence in "java concurrency in practice" to express: When multiple threads access an object, if you do not need to consider the scheduling and alternate execution of these threads in the runtime environment, there is no need for additional synchronization, or The caller performs any other coordination operations, and the behavior of calling this object can get the correct result, then this object is thread-safe.

From the above we can learn:

  1. In what kind of environment: multi-threaded environment.
  2. In what kind of operation: multiple threads are scheduled and executed alternately.
  3. What happened: The correct result can be obtained.
  4. Who: Thread safety is used to describe whether an object is thread safe.

    2.2 Thread safety

We can divide thread safety into five levels according to the safety of java shared objects: immutable, absolute thread safety, relative thread safety, thread compatibility, and thread opposition:

2.2.1 Immutable

Immutable (immutable) objects in Java must be thread-safe, because the scheduling and alternate execution of threads will not cause any changes to the object. Also immutable are custom constants. The objects in final and constant pools are also immutable.

In the general enumeration class in java, String is a common immutable type. The same enumeration class is used to implement the singleton mode, which is inherently thread safe. In the String object, you can call replace() or subString(). Can't modify his original value

2.2.2 Absolute thread safety

Let’s take a look at the definition of Brian Goetz’s "Java Concurrent Programming Practice": When multiple threads access a certain class, no matter what scheduling method the runtime environment uses or how these threads will alternate, and the main code Without any additional synchronization or coordination, this class can show correct behavior, then this class is called thread-safe.

Zhou Zhiming mentioned in <<In-depth understanding of the java virtual machine>> that Brian Goetz's absolute thread-safe class definition is very strict. To achieve an absolutely thread-safe class usually requires a lot of effort, and sometimes it is impractical. The price. At the same time, he also cited the example of Vector. Although both Vectorget and remove are modified by synchronized, it still shows that Vector is not absolutely thread safe. Briefly introduce this example:

public

Object
 getLast
(
Vector
 list
)

{

return
 list
.
get
(
list
.
size
()

-

1
);
}
public

void
 deleteLast
(
Vector
 list
)

{
    list
.
remove
(
list
.
size
()

-

1
);
}

If we use multiple threads to execute the above code, although remove and get are guaranteed to be synchronized, this problem will occur. It is possible that the last element has been removed, but list.size() has already been obtained at this time. In fact, the get An exception will be thrown because that element has been removed.

2.2.3 Relative safety

Zhou Zhiming believes that this definition can be appropriately weakened, limiting the "behavior of calling this object" to "a separate operation on the object", so that a relatively thread-safe definition can be obtained. It needs to ensure that the individual operations on this object are thread-safe. We do not need to do additional operations when calling, but for some specific sequential calls, additional synchronization means are required. We can modify the above Vector call to:

public

synchronized

Object
 getLast
(
Vector
 list
)

{

return
 list
.
get
(
list
.
size
()

-

1
);
}
public

synchronized

void
 deleteLast
(
Vector
 list
)

{
    list
.
remove
(
list
.
size
()

-

1
);
}

In this way, we, as the caller, add additional synchronization means, and its Vector is in line with our relative safety.

2.2.4 Thread compatibility

Thread compatibility means that its objects are not thread-safe, but synchronization methods can be used correctly by the caller. For example, we can lock the ArrayList, which can also achieve the effect of Vector.

2.2.5 Thread opposition

Thread opposition refers to code that cannot be used concurrently in a multithreaded environment regardless of whether the caller has taken synchronization measures. Because the Java language is inherently multi-threaded, this kind of multi-threaded code that excludes thread opposition is rarely seen, and it is usually harmful and should be avoided as much as possible.

2.3 How to solve thread safety

There are generally several ways to solve thread safety: mutual exclusion blocking (pessimistic, locking), non-blocking synchronization (similar to optimistic locking, CAS), no synchronization is required (the code is well written, no need to consider synchronization at all)

Synchronization means that when multiple threads concurrently access shared data, it is ensured that the shared data is only used by one thread (or some, when using semaphores) at the same time.

2.3.1 Exclusive synchronization

Mutual exclusion is a pessimistic method, because he is worried that someone will destroy his data when he visits, so he needs to use some means to make this data unique during this time period, and not let other people have it. Opportunity for contact. Critical section (CriticalSection), mutex (Mutex) and semaphore (Semaphore) are the main mutual exclusion implementations. ReentrantLock and synchronized are generally used to achieve synchronization in Java. In actual business, it is recommended to use synchronized. The code in the first section actually uses synchronized. Why is it recommended to use synchronized?

  • If we show that we use lock, we have to manually unlock the unlock() call, but many people may actually forget during the actual development process, so it is recommended to use synchronized. Lock fails in terms of ease of programming.
  • Synchronized optimized after jdk1.6 will shift from bias to lock, lightweight lock, spin adaptive lock, and finally to heavyweight lock. And Lock is a weight lock. In the future version of jdk, the key optimization is also synchronized. Lock also fails in the convenience of performance.

If you need to wait for interruptible, wait for timeout, fair lock and other functions in your business, then you can choose this ReentrantLock.

Of course, the exclusive lock in our Mysql database is actually the realization of mutual exclusive synchronization. When the exclusive lock is added, other transactions cannot access its data.

2.3.2 Non-blocking synchronization

Non-blocking synchronization is an optimistic method. In the optimistic method, he will try the operation first. If no one is competing, it will succeed, otherwise it will compensate (usually an endless loop of retry or jump out after multiple cycles). The most important problem in mutually exclusive synchronization is the performance problems caused by thread blocking and wake-up, and the optimistic synchronization strategy solves this problem.

However, there is a problem above. The two operations of operation and detection of competition must ensure atomicity, which requires the support of our hardware equipment. For example, the cas operation in our java is actually the instruction of the underlying hardware of the operation.

After JDK1.5, the CAS operation can be used in Java programs. The operation is provided by several methods such as compareAndSwapInt() and compareAndSwapLong() in the sun.misc.Unsafe class. The virtual machine has made special for these methods internally. Processing, the result of just-in-time compilation is a platform-related processor CAS or the like, there is no method call process, or it can be considered unconditionally inlined

2.3.3 No synchronization

To ensure thread safety, synchronization is not necessary, and there is no causal relationship between the two. Synchronization is just a means to ensure the correctness of shared data during contention. If a method does not involve shared data, it naturally does not need any synchronization measures to ensure correctness, so some codes are inherently safe on the spot. Generally divided into two categories:

  • Reentrant code: Reentrant code is also called pure code. It can be interrupted at any time. After the control is restored, the program will not make any errors. The result of reentrant code is generally predictable:
public

int
 sum
(){

return

1
+
2
;

}

For example, this kind of code is reentrant code, but it rarely appears in our own code

  • Thread local storage: Generally speaking, this is a method we use more. We can ensure that the class is stateless and all variables exist in our methods, or save it through ThreadLocal.

    2.4 Some other experiences with thread safety

The above is more official, but the following is summarized from some real experiences:

  • When using some object as a singleton, you need to determine whether the object is thread-safe: For example, when we use SimpleDateFormate, many beginners do not pay attention to using it as a singleton tool class, which leads to our business abnormal. You can refer to my other article: Can you really convert dates in Java?
  • If it is found that it is not a singleton, it needs to be replaced, such as HashMap with ConcurrentHashMap and queue with ArrayBlockingQueue.
  • Pay attention to the deadlock. If you use the lock, you must remember to release the lock. At the same time, you must pay attention to the order of using the lock. This is not only a stand-alone lock, but also a distributed lock. Be sure to pay attention: a thread locks A first and then locks B , Another thread locks B first and then locks A. Therefore, in general, distributed locks will add a timeout period to avoid deadlocks caused by failure to release the lock due to network problems.
  • Lock granularity: The same is not only about stand-alone locks, but also includes distributed locks. Don't use the entry method for convenience. Start locking without analysis, which will seriously affect performance. The same can not be too fine-grained, stand-alone locks will increase context switching, and distributed locks will increase network calls, which will lead to a decrease in our performance.
  • Appropriate introduction of optimistic lock: For example, we have a requirement to deduct money from users. In order to prevent excessive deductions, we will use pessimistic locks to lock at this time, but the efficiency is relatively low, because users actually deduct money at the same time. You can use optimistic lock, add the version field in the user's account table, first query the version, and then check whether the current version is consistent with the database version when updating.
  • If you want to use a non-thread-safe object in a multi-threaded environment, the data can be placed in ThreadLocal, or created only in the method. Although our ArrayList is not thread-safe, it is generally done in the method when we use it. Use List list = new ArrayList() to ensure thread safety in an unsynchronized way.
  • Chairman Mao once said: You have grain in your hands, so you don't panic. Learn a lot of multi-threading knowledge, this is also the most important, of course, you can pay attention to my public account to make progress together.

    At last

This article started with the most difficult interview question in history, and introduced one of the most important thread safety in our work. I hope you can read Chapter 13 of Thread Safety and Lock Optimization in Zhou Zhiming's "In-Depth Understanding of JVM Virtual Machines". I believe there will be a new improvement after reading. As the author himself is limited, please correct me if there is any mistake.

Finally, make an advertisement. If you think this article has an article for you, you can follow my technical public account. Recently, the author has collected a lot of the latest learning materials videos and interview materials, and you can receive them after paying attention.

If you think this article is helpful to you, or if you have any questions and want to provide 1v1 free VIP service, you can follow my public account, and you can receive the latest java learning materials videos and the latest interview materials for free. Following and forwarding are my greatest support, O(∩_∩)O:

Thoughts on thread safety triggered by a java interview question called "the hardest in history"

Guess you like

Origin blog.51cto.com/14980978/2544831