brief introduction
Part One Concurrent Programming - Thread Safety We learned that thread security can be achieved by locking mechanism to protect shared objects.
Synchronized is a built-in locking mechanism provided by java. Synchronize code blocks with synchronized keywords. Threads automatically acquire locks before entering the synchronized code block and release locks when exiting the synchronized code block. The built-in lock is a mutex lock.
In this paper, we will study synchronized in depth.
Use
Synchronization method
Synchronized non-static method
public class Synchronized { private static int count; private synchronized void add1() { count++; System.out.println(count); } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync.add1(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync.add1(); } }); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(count); } }
The results are in line with expectations: synchronized acts on non-static methods, locking instance objects, and sync objects as shown above, so threads can run correctly, and count always results in 2000.
public class Synchronized { private static int count; private synchronized void add1() { count++; System.out.println(count); } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); Synchronized sync1 = new Synchronized(); Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync.add1(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync1.add1(); } }); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(count); } }
The result is not as expected: as shown above, for non-static methods, the lock is instantiated objects, so when sync and sync1 run simultaneously, there will still be thread security problems, because the lock is two different instantiated objects.
Synchronized static method
public class Synchronized { private static int count; private static synchronized void add1() { count++; System.out.println(count); } private static synchronized void add11() { count++; System.out.println(count); } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); Synchronized sync1 = new Synchronized(); Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { Synchronized.add1(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { Synchronized.add11(); } }); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(count); } }
The results are in line with expectations: when locking static methods, class objects are locked. So calling ADD1 and add11 in different threads still yields the correct results.
Synchronized code block
Lock the current instance object
public class Synchronized { private static int count; private void add1() { synchronized (this) { count++; System.out.println(count); } } private static synchronized void add11() { count++; System.out.println(count); } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); Synchronized sync1 = new Synchronized(); Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync.add1(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync1.add1(); } }); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(count); } }
The results are not in line with expectations: when synchronized synchronized method blocks lock instance objects, as in the above example, calling this method in different instances will still cause thread security problems.
Lock other instance objects
public class Synchronized { private static int count; public String lock = new String(); private void add1() { synchronized (lock) { count++; System.out.println(count); } } private static synchronized void add11() { count++; System.out.println(count); } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); Synchronized sync1 = new Synchronized(); Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync.add1(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync1.add1(); } }); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(count); System.out.println(sync.lock == sync1.lock); } }
The results are not in line with expectations: when synchronized synchronized method blocks lock other instance objects, as in the above example, calling this method in different instances still presents thread security problems.
public class Synchronized { private static int count; public String lock = ""; private void add1() { synchronized (lock) { count++; System.out.println(count); } } private static synchronized void add11() { count++; System.out.println(count); } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); Synchronized sync1 = new Synchronized(); Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync.add1(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync1.add1(); } }); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(count); System.out.println(sync.lock == sync1.lock); } }
The result is expected: When synchronized synchronized method blocks are synchronized, although locks are other instance objects, they are already in the previous instance, because String = "" is stored in the constant pool, and the lock is actually the same object, so it is thread-safe.
Lock class object
public class Synchronized { private static int count; private void add1() { synchronized (Synchronized.class) { count++; System.out.println(count); } } private static synchronized void add11() { count++; System.out.println(count); } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); Synchronized sync1 = new Synchronized(); Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync.add1(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { sync1.add1(); } }); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(count); } }
The results are in line with expectations: when synchronized synchronized method blocks lock class objects, it is thread-safe to call this method in different instances as in the above example.
Lock mechanism
public class Synchronized { private static int count; public static void main(String[] args) throws InterruptedException { synchronized (Synchronized.class) { count++; } } }
Decompile the class file using javap-v Synchronized. class.
You can see that synchronized actually implements the locking mechanism through monitorenter and monitorexit. At the same time, only one thread can enter the monitoring area. This ensures the synchronization of threads.
Normally, instruction 4 enters the surveillance area, instruction 14 exits the surveillance area, and instruction 15 jumps directly to instruction 23 return.
However, in exceptional cases, exceptions will jump to instruction 18, and then execute to instruction 20 monitor exit to release the lock in order to prevent the situation that is not released when an exception occurs.
This is also a virtue of synchronized: regardless of code execution, you will never forget to actively release the lock.
To learn more about Monitors, you can Click View
Lock upgrade
Because monitor relies on the implementation of Motx lock of operating system, it is a heavy operation, and needs to switch the system to the kernel state, which is very expensive. So bias lock and lightweight lock are introduced in jdk1.6.
synchronized has four states: unlocked - > biased - > lightweight - > heavyweight.
Unlocked
Without locking resources, all threads can access and modify them. But at the same time, only one thread can modify successfully.
Biased Lock
In the case of weak lock competition, a thread usually acquires the same lock several times. In order to reduce the cost of acquiring the lock, a bias lock is introduced, and the threadID of the thread acquiring the lock is recorded in the java object header.
- When a thread finds that the threadID of the object header exists. Determine whether it is the same thread as the current thread.
- If so, there is no need to add or unlock again.
- If not, determine whether threadID survives. Not surviving: Set to unlock state, other threads compete to set biased locks. Survival: Find threadID stack information to determine whether locks need to continue. If you need to hold the lock, you upgrade the threadID thread to a lightweight lock. Unlock without holding, set to unlock and wait for other threads to compete.
Because the revocation of biased locks is still heavy, leading to access to the security point, so when the competition is fierce, it will affect performance, and biased locks can be disabled using - XX:-UseBiasedLocking=false.
Lightweight Locking
When the bias lock is upgraded to a lightweight lock, other threads try to acquire the lock by setting the object header in CAS mode.
- Lock Record is first set in the stack frame of the current thread to store a copy of mark word in the current object header.
- Copy the contents of mark word to lock record and try to use cas to point the pointer of mark word to lock record
- If the replacement succeeds, the biased lock is acquired
- If the replacement is unsuccessful, a certain number of spin retries will occur.
- When a certain number of spins or new threads compete for locks, lightweight locks expand into heavy locks.
CAS
CAS is compare and swap. It is an optimistic locking mechanism. Usually there are three values
- V: Actual value in memory
- A: Old expectations
- B: New values to be modified
When V is equal to A, V is replaced by B. That is, when the actual value in memory is equal to our expected value, it is replaced by a new value.
CAS may encounter ABA problems, that is, the value of A in memory changes to B and then to A, when A is a new value and should not be replaced.
This problem can be avoided by: A-1, B-2, A-3.
Heavy Lock
Spin consumes CPU, so if a thread spins for a period of time or when a new thread competes for a lock, the lightweight lock will expand to a heavy lock.
Heavy-duty locks are implemented by monitor. The bottom layer of monitor is actually implemented by mutex lock (mutex lock) of operating system.
The cost of switching from user mode to kernel mode is high.
summary
We learned this article together.
- Several uses of synchronized: synchronized method, synchronized code block. It's actually a synchronization class or a synchronization instance object.
- Lock upgrade: no lock, bias lock, lightweight lock, heavyweight lock and its expansion process.
synchronized as a built-in lock, although it helps us solve the thread security problem, but it brings performance loss, so it must not be abused. When using, please pay attention to the scope of synchronization block. Usually, the smaller the scope of action, the smaller the impact on performance (pay attention to balancing the cost of acquiring and releasing locks, not to narrow the scope of action, but frequent acquisition and release).