- Java concurrency
- First, use threads
- Implement the Runnable interface
- Implement Callable interface
- Thread class inheritance
- Implement interface VS inheritance Thread
- Second, the basic threading mechanism
- Executor
- Daemon
- sleep()
- yield()
- Third, interrupt
- InterruptedException
- interrupted()
- Executor interrupt operation
- 4. Mutually exclusive synchronization
- synchronized
- ReentrantLock
- To compare
- Used to choose
- Collaboration between threads
- join()
- wait() notify() notifyAll()
- await() signal() signalAll()
- Thread status
- NEW
- RUNABLE
- They are BLOCKED.
- WAITING indefinitely
- TIMED_WAITING
- Death (TERMINATED)
- J.U.C – AQS
- CountDownLatch
- CyclicBarrier
- Semaphore
- J.U.C – Other components
- FutureTask
- BlockingQueue
- ForkJoin
- Example of thread insecurity
- Java memory model
- Main memory vs. working memory
- Intermemory operation
- There are three main features of the memory model
- Principle of antecedent
- Thread safety
- immutable
- The mutex synchronization
- Nonblocking synchronization
- Asynchronous scheme
- 12. Lock optimization
- spinlocks
- Lock elimination
- Lock coarsening
- Lightweight lock
- Biased locking
- Good practice for multithreaded development
- The resources
- First, use threads
First, use threads
There are three ways to use threads:
- Implement Runnable interface;
- Callable interface;
- Inherits the Thread class.
Classes that implement the Runnable and Callable interfaces are treated only as tasks that can be run in threads, not threads, and therefore need to be called through threads. It can be understood that the task is executed by thread driven.
Implement the Runnable interface
You need to implement the run() method in the interface.
public class MyRunnable implements Runnable {
@Override
public void run(a) {
// ...}}Copy the code
Create another Thread instance using the Runnable instance, and then call the start() method of the Thread instance to start the Thread.
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
Copy the code
Implement Callable interface
In contrast to Runnable, Callable can have a return value, which is encapsulated by FutureTask.
public class MyCallable implements Callable<Integer> {
public Integer call(a) {
return 123; }}Copy the code
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
Copy the code
Thread class inheritance
You also need to implement the run() method because the Thread class also implements the Runable interface.
When the start() method is called to start a thread, the virtual machine places the thread in a ready queue to be scheduled, and when a thread is scheduled, its run() method is executed.
public class MyThread extends Thread {
public void run(a) {
// ...}}Copy the code
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
Copy the code
Implement interface VS inheritance Thread
It is better to implement the interface because:
- Java does not support multiple inheritance, so if you inherit Thread, you cannot inherit other classes, but you can implement multiple interfaces.
- The class might just need to be executable, and inheriting the entire Thread class would be too expensive.
Second, the basic threading mechanism
Executor
Executors manage the execution of multiple asynchronous tasks without requiring programmers to explicitly manage the thread lifecycle. Asynchronous here refers to the execution of multiple tasks without interference, without the need for synchronous operations.
There are three main types of executors:
- CachedThreadPool: a task creates a thread;
- FixedThreadPool: all tasks can only use fixed size threads;
- SingleThreadExecutor: Corresponds to a FixedThreadPool of size 1.
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
executorService.shutdown();
}
Copy the code
Daemon
Daemon thread is a thread that provides services in the background while the program is running. It is not an integral part of the program.
When all non-daemon threads end, the program terminates, killing all daemon threads.
Main () is a non-daemon thread.
A thread can be set up as a daemon thread using the setDaemon() method before the thread is started.
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
Copy the code
sleep()
The \ thread. sleep(^ ^ ec) method puts the current Thread to sleep.
Sleep () may throw InterruptedException because exceptions cannot be propagated back to main() across threads and must be handled locally. Other exceptions thrown by the thread also need to be handled locally.
public void run(a) {
try {
Thread.sleep(3000);
} catch(InterruptedException e) { e.printStackTrace(); }}Copy the code
yield()
A call to the static method thread.yield () declares that the current Thread has completed the most important part of its life cycle and can be switched to another Thread for execution. This method is only a suggestion to the thread scheduler, and only a suggestion that other threads with the same priority should run.
public void run(a) {
Thread.yield();
}
Copy the code
Third, interrupt
A thread terminates automatically when it finishes executing, or prematurely if an exception occurs during execution.
InterruptedException
InterruptedException is thrown to terminate a thread prematurely by calling interrupt() on the thread if it is in a blocking, finite wait, or indefinite wait state. However, I/O and synchronized lock blocking cannot be interrupted.
For the following code, start a Thread in main() and then interrupt it. Since thread.sleep () is called in the Thread, an InterruptedException is thrown, terminates the Thread prematurely without executing subsequent statements.
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run(a) {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch(InterruptedException e) { e.printStackTrace(); }}}}Copy the code
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
Copy the code
Main run java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at InterruptExample.lambda$main$0(InterruptExample.java:5) at InterruptExample$$Lambda$1/713338599.run(Unknown Source) at java.lang.Thread.run(Thread.java:745)Copy the code
interrupted()
If a thread’s run() method executes an infinite loop and does not perform an operation such as sleep() that throws InterruptedException, then calling the thread’s interrupt() method will not cause the thread to terminate prematurely.
Calling interrupt() sets the thread’s interrupt flag, and calling interrupted() returns true. Therefore, you can use the interrupted() method in the body of the loop to determine if the thread is interrupted and terminate it early.
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run(a) {
while(! interrupted()) {// ..
}
System.out.println("Thread end"); }}}Copy the code
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new MyThread2();
thread2.start();
thread2.interrupt();
}
Copy the code
Thread end
Copy the code
Executor interrupt operation
Calling Executor’s shutdown() method waits for all threads to execute before closing, but calling shutdownNow() is equivalent to calling interrupt() on a per-thread basis.
The following uses Lambda to create a thread, which is equivalent to creating an anonymous internal thread.
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch(InterruptedException e) { e.printStackTrace(); }}); executorService.shutdownNow(); System.out.println("Main run");
}
Copy the code
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Copy the code
If you want to interrupt only one thread in your Executor, submit a thread using the submit() method, which returns a Future<? > object that interrupts the thread by calling its Cancel (true) method.
Future<? > future = executorService.submit(() -> {// ..
});
future.cancel(true);
Copy the code
4. Mutually exclusive synchronization
Java provides two locking mechanisms to control mutually exclusive access to shared resources by multiple threads, the first being synchronized in the JVM implementation and ReentrantLock in the JDK implementation.
synchronized
1. Synchronize a code block
public void func(a) {
synchronized (this) {
// ...}}Copy the code
It only works on the same object, and if a block of synchronized code is called on both objects, synchronization will not occur.
For the following code, two threads execute using the ExecutorService, and because they call a synchronized code block of the same object, the two threads synchronize, and when one thread enters the synchronized block, the other must wait.
public class SynchronizedExample {
public void func1(a) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + ""); }}}}Copy the code
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
Copy the code
0 1 2 3 4 5 6 7 8 9Copy the code
For the following code, two threads invoke synchronized blocks of different objects, so the two threads do not need to synchronize. As you can see from the output, the two threads execute interspersed.
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
Copy the code
0 0 1 1 2 3 3 4 5 5 6 6 7 7 8 8 9 9Copy the code
2. Synchronize a method
public synchronized void func (a) {
// ...
}
Copy the code
It acts on the same object as a synchronized code block.
3. Synchronize a class
public void func(a) {
synchronized (SynchronizedExample.class) {
// ...}}Copy the code
Apply to the entire class, meaning that two threads calling such synchronization statements on different objects of the same class will also synchronize.
public class SynchronizedExample {
public void func2(a) {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + ""); }}}}Copy the code
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
Copy the code
0 1 2 3 4 5 6 7 8 9Copy the code
4. Synchronize a static method
public synchronized static void fun(a) {
// ...
}
Copy the code
Apply to the entire class.
ReentrantLock
ReentrantLock is a lock in the java.util.concurrent (J.U.C) package.
public class LockExample {
private Lock lock = new ReentrantLock();
public void func(a) {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + ""); }}finally {
lock.unlock(); // Make sure the lock is released to avoid deadlocks.}}}Copy the code
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
Copy the code
0 1 2 3 4 5 6 7 8 9Copy the code
To compare
1. Lock implementation
Synchronized is implemented by the JVM, while ReentrantLock is implemented by the JDK.
2. The performance
The new version of Java has many optimizations for synchronized, which is much the same as ReentrantLock, such as spinlocks.
3. Wait can be interrupted
When the thread holding the lock does not release the lock for a long time, the waiting thread can choose to abandon the wait and do something else instead.
ReentrantLock interrupts, while synchronized does not.
4. A fair lock
A fair lock means that when multiple threads are waiting for the same lock, they must obtain the lock in the sequence in which the lock was applied.
Synchronized locks are unfair, and reentrantLocks are unfair by default, but they can also be fair.
5. Lock binds multiple conditions
A ReentrantLock can bind multiple Condition objects simultaneously.
Used to choose
Unless you need to use the advanced features of ReentrantLock, use synchronized. This is because synchronized is a locking mechanism implemented by the JVM and natively supported by the JVM, while ReentrantLock is not supported by all JDK versions. And with synchronized you don’t have to worry about deadlocks if the lock is not released, because the JVM ensures that the lock is released.
Collaboration between threads
When multiple threads can work together to solve a problem, threads need to be coordinated if some parts must be completed before others.
join()
Calling another thread’s join() method in a thread suspends the current thread rather than waiting busily until the target thread terminates.
For the following code, although thread B starts first, thread B will wait for thread A to finish before executing because thread A’s join() method is called in thread B, so the output of thread A is guaranteed to precede that of thread B.
public class JoinExample {
private class A extends Thread {
@Override
public void run(a) {
System.out.println("A"); }}private class B extends Thread {
private A a;
B(A a) {
this.a = a;
}
@Override
public void run(a) {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B"); }}public void test(a) {
A a = new A();
B b = newB(a); b.start(); a.start(); }}Copy the code
public static void main(String[] args) {
JoinExample example = new JoinExample();
example.test();
}
Copy the code
A
B
Copy the code
wait() notify() notifyAll()
A call to wait() causes a thread to wait for a condition to be met. The thread is suspended while waiting. When another thread runs such that the condition is met, the other thread calls notify() or notifyAll() to wake up the suspended thread.
They are all part of Object, not Thread.
Can only be used in synchronous method or synchronous control block in use, otherwise it will throw IllegalMonitorStateException at runtime.
During a wait() suspension, the thread releases the lock. This is because if the lock is not released, no other thread can enter the synchronization method or synchronization control block of the object, and there is no notify() or notifyAll() to wake up the suspended thread, resulting in a deadlock.
public class WaitNotifyExample {
public synchronized void before(a) {
System.out.println("before");
notifyAll();
}
public synchronized void after(a) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after"); }}Copy the code
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
WaitNotifyExample example = new WaitNotifyExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
Copy the code
before
after
Copy the code
The difference between wait() and sleep()
- Wait () is the Object method, while sleep() is the static method of Thread;
- Wait () releases the lock, sleep() does not.
await() signal() signalAll()
The Condition class is provided in the java.util.Concurrent library for coordination between threads, and the await() method can be called on the Condition to make the thread wait. Other threads call signal() or signalAll() to wake up the waiting thread.
Compared to wait(), await() can specify the condition to wait and is therefore more flexible.
Use Lock to get a Condition object.
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before(a) {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally{ lock.unlock(); }}public void after(a) {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally{ lock.unlock(); }}}Copy the code
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
Copy the code
before
after
Copy the code
Thread status
A thread can only be in one state, and the thread state here refers to the thread state of the Java VIRTUAL machine, and does not reflect the thread state in a specific operating system.
NEW
It is not started after being created.
RUNABLE
Running in the Java VIRTUAL machine. At the operating system level, however, it may be running, or it may wait for resource scheduling (for example, processor resources) to complete and enter the running state. Therefore, the runnable state means that it can be run. Whether it can be run depends on the resource scheduling of the underlying operating system.
They are BLOCKED.
A monitor lock is requested to enter a synchronized function or block of code, but the monitor lock is already occupied by another thread, so it is blocked. To terminate the state entry and thus RUNABLE requires another thread to release the Monitor Lock.
WAITING indefinitely
Wait for another thread to explicitly wake up.
The difference between blocking and waiting is that blocking is passive, waiting for a Monitor Lock to be acquired. Wait is active, entered by calling methods such as object.wait ().
Enter the method | Exit the method |
---|---|
Object.wait() method with no Timeout parameter | Object.notify() / Object.notifyAll() |
Thread.join() method with no Timeout parameter | The called thread completes execution |
LockSupport. Park () method | LockSupport.unpark(Thread) |
TIMED_WAITING
There is no need to wait for another thread to wake up explicitly, it will be woken up automatically after a certain amount of time.
Enter the method | Exit the method |
---|---|
Thread.sleep () method | End of the time |
The object.wait () method with the Timeout parameter set | End of time/object.notify ()/object.notifyall () |
Thread.join() method with Timeout parameter set | Time ends/called thread completes execution |
LockSupport. ParkNanos () method | LockSupport.unpark(Thread) |
LockSupport. ParkUntil () method | LockSupport.unpark(Thread) |
Sleep () is often described as putting a Thread to sleep when the thread.sleep () method is called to put a Thread to a finite wait state. “Suspending a thread” is often described when calling the object.wait () method to make a thread wait for a deadline or indefinitely. Sleep and suspension are used to describe behavior, while blocking and waiting are used to describe states.
Death (TERMINATED)
This can be done by the thread itself after completing the task, or by raising an exception.
Java SE 9 Enum Thread.State
J.U.C – AQS
Java.util.concurrent (J.U.C) greatly improves concurrency performance, and AQS are considered to be at the heart of J.U.C.
CountDownLatch
Used to control one or more threads to wait for multiple threads.
A counter CNT is maintained and every call to the countDown() method decays the counter by 1, at which point threads that are waiting because of calling the await() method are woken up.
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end"); executorService.shutdown(); }}Copy the code
run.. run.. run.. run.. run.. run.. run.. run.. run.. run.. endCopy the code
CyclicBarrier
It is used to control multiple threads to wait for each other, and only when multiple threads have arrived will they continue to execute.
Similar to CountdownLatch, both are implemented by maintaining counters. After the thread executes the await() method, the counter decreases by one and waits until the counter reaches zero and all waiting threads calling the await() method can continue.
One difference between CyclicBarrier and CountdownLatch is that the CyclicBarrier’s counter can be recycled by calling the reset() method, hence its name.
CyclicBarrier has two constructors, parties indicating the initial value of the counter and barrierAction executing once when all threads have reached the barrier.
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
Copy the code
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after.."); }); } executorService.shutdown(); }}Copy the code
before.. before.. before.. before.. before.. before.. before.. before.. before.. before.. after.. after.. after.. after.. after.. after.. after.. after.. after.. after..Copy the code
Semaphore
Semaphore is similar to an operating system Semaphore that controls the number of threads accessing a mutex resource.
The following code simulates concurrent requests to a service that can only be accessed by three clients at a time, with a total of 10 requests.
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + "");
} catch (InterruptedException e) {
e.printStackTrace();
} finally{ semaphore.release(); }}); } executorService.shutdown(); }}Copy the code
2, 1, 2, 2, 2, 2, 2, 1, 2, 2Copy the code
J.U.C – Other components
FutureTask
When we introduced Callable, we learned that it can have a return value, which is encapsulated by the Future<V>. FutureTask implements the RunnableFuture interface, which inherits from the Runnable and Future<V> interfaces, allowing FutureTask to either execute as a task or have a return value.
public class FutureTask<V> implements RunnableFuture<V>
Copy the code
public interface RunnableFuture<V> extends Runnable.Future<V>
Copy the code
FutureTask can be used in scenarios where an execution result is retrieved asynchronously or a task is cancelled. When a computational task takes a long time to execute, it can be encapsulated with FutureTask, and the main thread will fetch the results after it completes its task.
public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call(a) throws Exception {
int result = 0;
for (int i = 0; i < 100; i++) {
Thread.sleep(10);
result += i;
}
returnresult; }}); Thread computeThread =new Thread(futureTask);
computeThread.start();
Thread otherThread = new Thread(() -> {
System.out.println("other task is running...");
try {
Thread.sleep(1000);
} catch(InterruptedException e) { e.printStackTrace(); }}); otherThread.start(); System.out.println(futureTask.get()); }}Copy the code
other task is running...
4950
Copy the code
BlockingQueue
Java. Util. Concurrent. BlockingQueue interface has the following the realization of the blocking queue:
- FIFO queues: LinkedBlockingQueue, ArrayBlockingQueue (fixed length)
- Priority queue: PriorityBlockingQueue
Provide blocking take() and put() methods: if the queue is empty, take() will block until there is something in the queue; If the queue is full, put() blocks until there is a free place in the queue.
Implement producer-consumer issues using BlockingQueue
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
@Override
public void run(a) {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce.."); }}private static class Consumer extends Thread {
@Override
public void run(a) {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume.."); }}}Copy the code
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = newProducer(); producer.start(); }}Copy the code
produce.. produce.. consume.. consume.. produce.. consume.. produce.. consume.. produce.. consume..Copy the code
ForkJoin
It is used for parallel computing. Similar to MapReduce, a large computing task is divided into multiple small tasks for parallel computing.
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}
@Override
protected Integer compute(a) {
int result = 0;
if (last - first <= threshold) {
// The task is small enough to calculate directly
for (inti = first; i <= last; i++) { result += i; }}else {
// Break it down into small tasks
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
returnresult; }}Copy the code
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1.10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
Copy the code
ForkJoin is started using a ForkJoinPool, a special thread pool whose number of threads depends on the number of CPU cores.
public class ForkJoinPool extends AbstractExecutorService
Copy the code
ForkJoinPool implements work stealing algorithms to improve CPU utilization. Each thread maintains a double-ended queue to store tasks that need to be executed. The job stealing algorithm allows an idle thread to steal a task from another thread’s two-ended queue to execute. The stolen task must be the latest to avoid competing with the queue thread. For example, in the figure below, Thread2 takes the latest Task1 task from Thread1’s queue, and Thread1 takes Task2 out to execute it, thus avoiding a race. However, contention can still occur if there is only one task in the queue.
Example of thread insecurity
If multiple threads access the same shared data without performing a synchronization operation, the results of the operation are inconsistent.
The following code shows 1000 threads incrementing the CNT at the same time, and it may be less than 1000 at the end of the operation.
public class ThreadUnsafeExample {
private int cnt = 0;
public void add(a) {
cnt++;
}
public int get(a) {
returncnt; }}Copy the code
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
Copy the code
997
Copy the code
Java memory model
The Java memory model attempts to mask the differences in memory access between hardware and operating systems, so that Java programs can achieve a consistent memory access effect across all platforms.
Main memory vs. working memory
Registers on the processor can read and write several orders of magnitude faster than memory, and to resolve this speed contradiction, a cache is inserted between them.
The addition of caching brings a new problem: cache consistency. If multiple caches share the same main memory area, data from multiple caches may be inconsistent, and some protocol is required to resolve this problem.
All variables are stored in main memory, and each thread also has its own working memory, which is stored in a cache or register that holds a main memory copy of the variables used by the thread.
Threads can only manipulate variables in working memory directly, and variable values between threads need to be passed through main memory.
Intermemory operation
The Java memory model defines eight operations to interact with main and working memory.
- Read: Transfers the value of a variable from main memory to working memory
- Load: Executes after read, putting the value of read into a copy of the variables in working memory
- Use: Passes the value of a variable in working memory to the execution engine
- Assign: A variable that assigns a value received from the execution engine to working memory
- Store: Transfers the value of a variable in working memory to main memory
- Write: Executes after store, putting the value of store into a variable in main memory
- Lock: variable applied to main memory
- unlock
There are three main features of the memory model
1. The atomicity
The Java memory model guarantees atomicity for read, load, use, assign, Store, write, Lock, and unlock. For example, an assignment to a variable of type int is atomic. However, the Java memory model allows the VIRTUAL machine to divide reads and writes to 64-bit data (long, double) that are not volatile into two 32-bit operations, that is, the load, Store, read, and write operations can be atomically free.
One misconception is that atomic types such as ints do not present thread-safety problems in multithreaded environments. In the previous thread-unsafe code example, CNT was an int variable, and after 1000 threads incremented it, the value was 997 instead of 1000.
For the sake of discussion, memory interaction is simplified into three operations: load, assign, and Store.
The following figure shows that two threads operate on CNT at the same time. Load, assign, and Store operations do not have atomicity on the whole. Therefore, when CNT is modified on T1 and the modified value has not been written to main memory, T2 can still read the old value. As you can see, the two threads did two augmentation operations, but the value of CNT in main memory ended up being 1 instead of 2. So atomicity for int reads and writes just means atomicity for load, assign, and store.
AtomicInteger ensures atomicity of multiple thread modifications.
Using AtomicInteger to rewrite previously thread-unsafe code results in the following thread-safe implementation:
public class AtomicExample {
private AtomicInteger cnt = new AtomicInteger();
public void add(a) {
cnt.incrementAndGet();
}
public int get(a) {
returncnt.get(); }}Copy the code
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicExample example = new AtomicExample(); // Modify only this statement
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
Copy the code
1000
Copy the code
In addition to using atomic classes, synchronized mutex can also be used to ensure atomicity of operations. The memory interactions are lock and unlock, and the bytecode instructions are Monitorenter and Monitorexit.
public class AtomicSynchronizedExample {
private int cnt = 0;
public synchronized void add(a) {
cnt++;
}
public synchronized int get(a) {
returncnt; }}Copy the code
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicSynchronizedExample example = new AtomicSynchronizedExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
Copy the code
1000
Copy the code
2. The visibility
Visibility means that when one thread changes the value of a shared variable, other threads are immediately aware of the change. The Java memory model implements visibility by synchronizing the new value back to main memory after a variable is modified and flushing the value from main memory before the variable is read.
There are three main ways to achieve visibility:
- volatile
- Synchronized, the value of a variable must be synchronized back to main memory before it can be unlocked.
- Final, fields decorated with the final keyword are visible to other threads in the constructor once the initialization is complete and no this escape has occurred (other threads access the half-initialized object through this reference).
Using the volatile modifier for the CNT variable in the previous thread-unsafe example does not solve thread-unsafe problems because volatile does not guarantee atomicity of operations.
3. The order
Orderliness means that all operations are in order when observed in the thread. Observing from one thread to another, all operations are out of order because instruction reordering has occurred. In the Java memory model, the compiler and processor are allowed to reorder instructions. The reordering process does not affect the execution of single-threaded programs, but affects the correctness of multi-threaded concurrent execution.
The volatile keyword prevents instruction reordering by adding a memory barrier that does not place subsequent instructions in front of the barrier.
Orderliness can also be guaranteed by synchronized, which guarantees that only one thread executes synchronized code at any one time, effectively ordering the threads to execute synchronized code sequentially.
Principle of antecedent
As mentioned above, you can use volatile and synchronized to ensure order. In addition, the JVM provides for the principle of antecedent, allowing one operation to complete before another without control.
1. Single thread principle
Single Thread rule
In a thread, actions taken earlier in the program precede actions taken later.
2. Pipe lock rules
Monitor Lock Rule
An UNLOCK operation occurs first after a lock operation on the same lock.
3. Volatile variable rules
Volatile Variable Rule
A write to a volatile variable occurs first after a read to that variable.
4. Thread start rule
Thread Start Rule
The start() method of the Thread object calls each action of the Thread first.
5. Thread joining rule
Thread Join Rule
The end of the Thread object occurs first when the join() method returns.
6. Thread interrupt rule
Thread Interruption Rule
A call to the interrupt() method occurs when code in the interrupted thread detects the occurrence of an interrupt, which can be detected through the interrupted() method.
7. Object termination rules
Finalizer Rule
The completion of an object’s initialization (the end of constructor execution) occurs first at the beginning of its Finalize () method.
8. Transitivity
Transitivity
If operation A precedes operation B and operation B precedes operation C, then operation A precedes operation C.
Thread safety
Multiple threads can behave correctly no matter how they access a class and do not need to synchronize in the calling code.
Thread safety can be implemented in the following ways:
immutable
Immutable objects are thread-safe, requiring no thread-safety safeguards. As long as an immutable object is constructed correctly, you will never see it in an inconsistent state across multiple threads. In multithreaded environments, objects should be made immutable as much as possible to meet thread safety.
An immutable type:
- The base data type modified by the final keyword
- String
- Enumerated type
- Number subclasses include numeric wrapper types such as Long and Double, and big data types such as BigInteger and BigDecimal. But the AtomicInteger and AtomicLong classes, both Number, are mutable.
For collection types, you can use the Collections. UnmodifiableXXX () method to get an immutable collection.
public class ImmutableExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("a".1); }}Copy the code
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at ImmutableExample.main(ImmutableExample.java:9)
Copy the code
Collections. UnmodifiableXXX () on the set of the original copy first, need to modify the collection methods are directly throw an exception.
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
Copy the code
The mutex synchronization
Synchronized and already.
Nonblocking synchronization
The main problem with mutex synchronization is the performance problem caused by thread blocking and waking up, so it is also called blocking synchronization.
Mutex synchronization is a pessimistic concurrency strategy that assumes that if you don’t do the right synchronization, you’re bound to have a problem. Whether or not shared data is actually competing, it does locking (this is a conceptual model, but the virtual machine optimizes a large portion of unnecessary locking), user-mode core mind-shifting, maintaining lock counters, and checking to see if there are blocked threads that need to be woken up.
As the hardware instruction set evolves, we can use an optimistic concurrency strategy based on collision detection: do the operation first, and if there are no other threads contending for the shared data, then the operation succeeds, otherwise compensate (keep retrying until it succeeds). Many implementations of this optimistic concurrency strategy do not require threads to block, so this synchronization operation is called non-blocking synchronization.
1. CAS
Optimistic locking requires atomicity of operation and collision detection, which can no longer be guaranteed by mutex synchronization, but only by hardware. The most typical atomic operation supported by hardware is: compare-and-swap (CAS). The CAS instruction requires three operands, which are the memory address V, the old expected value A, and the new value B. When performing the operation, the value of V is updated to B only if the value of V is equal to A.
2. AtomicInteger
The method of the integer atom class AtomicInteger in the J.U.C package calls the CAS operation of the Unsafe class.
The following code uses AtomicInteger to perform the increment operation.
private AtomicInteger cnt = new AtomicInteger();
public void add(a) {
cnt.incrementAndGet();
}
Copy the code
The following code is the source for incrementAndGet(), which calls the Unsafe getAndAddInt().
public final int incrementAndGet(a) {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
Copy the code
The following code is the source of getAndAddInt(), var1 indicates the memory address of the object, var2 indicates the offset of the field relative to the memory address of the object, and var4 indicates the value to be added by the operation, which is 1. Use getIntVolatile(var1, var2) to get the old expected value. Use compareAndSwapInt() to do the CAS comparison. If the value in the field’s memory address is equal to var5, Update the variable with memory address var1+var2 to var5+var4.
You can see that getAndAddInt() goes in a loop, and the conflict is repeated retry.
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Copy the code
3. ABA
If A variable is first read with A value of A, its value is changed to B, and then changed back to A, the CAS operation will assume that it has never been changed.
The J.U.C package addresses this problem by providing a tagged atom reference class, AtomicStampedReference, that guarantees the correctness of the CAS by controlling the version of the variable value. ABA problems do not affect concurrency in most cases, and if ABA problems need to be addressed, switching to traditional mutex synchronization may be more efficient than atomic classes.
Asynchronous scheme
Synchronization is not necessary to be thread-safe. If a method does not inherently involve sharing data, it naturally does not require any synchronization measures to ensure correctness.
1. The stack
There are no thread-safety issues when multiple threads access local variables of the same method because local variables are stored in the virtual machine stack and are thread-private.
public class StackClosedExample {
public void add100(a) {
int cnt = 0;
for (int i = 0; i < 100; i++) { cnt++; } System.out.println(cnt); }}Copy the code
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
Copy the code
100, 100,Copy the code
2. Thread Local Storage
If the data needed in one piece of code must be shared with other code, see if the code that shares the data can be guaranteed to execute in the same thread. If we can, we can limit the visibility of shared data to the same thread, so that synchronization is not required to ensure that there is no data contention between threads.
Applications that fit this pattern are not uncommon, and most architectural patterns that use consumption queues (such as the producer-consumer pattern) try to consume products in a single thread. One of the most important application examples is the thread-per-request processing in the classic Web interaction model. The widespread application of this processing method enables many Web server applications to use thread-local storage to solve the thread-safety problem.
You can use the java.lang.ThreadLocal class to implement thread-local storage.
For the following code, thread1 sets threadLocal to 1 and thread2 sets threadLocal to 2. After a certain amount of time, thread1 reads threadLocal as 1, unaffected by thread2.
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2); threadLocal.remove(); }); thread1.start(); thread2.start(); }}Copy the code
1
Copy the code
To understand ThreadLocal, look at the following code:
public class ThreadLocalExample1 {
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
ThreadLocal threadLocal2 = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal1.set(1);
threadLocal2.set(1);
});
Thread thread2 = new Thread(() -> {
threadLocal1.set(2);
threadLocal2.set(2); }); thread1.start(); thread2.start(); }}Copy the code
Its corresponding underlying structure diagram is as follows:
Each Thread has a ThreadLocal ThreadLocalMap object.
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
Copy the code
When we call a ThreadLocal set(T value) method, we get the ThreadLocalMap object of the current thread and insert the ThreadLocal->value key-value pair into the Map.
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map ! =null)
map.set(this, value);
else
createMap(t, value);
}
Copy the code
The get() method is similar.
public T get(a) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map ! =null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if(e ! =null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
returnresult; }}return setInitialValue();
}
Copy the code
ThreadLocal is not technically designed to solve the problem of multithreaded concurrency, because there is no multithreaded competition.
In some scenes, especially using a thread pool), because the ThreadLocal. ThreadLocalMap the underlying data structure, leading to a ThreadLocal has a memory leak, should as far as possible after each use ThreadLocal manually call remove (), To avoid the classic risk of ThreadLocal leaking memory or even disrupting its own business.
3. Reentrant Code
This Code, also known as Pure Code, can be interrupted at any point in its execution to execute another piece of Code (including recursive calls to itself) without any errors in the original program after control is returned.
Reentrant code has some common characteristics, such as not relying on data stored on the heap and common system resources, using state quantities passed in from parameters, and not calling non-reentrant methods.
12. Lock optimization
Lock optimization here refers primarily to JVM optimization for synchronized.
spinlocks
Mutex synchronization entering a blocking state is expensive and should be avoided as much as possible. In many applications, shared data is locked for only a short period of time. The idea of a spinlock is to have a thread perform a busy loop (spin) for a period of time when it requests a lock that shares data, and if the lock is acquired during that time, it can avoid entering a blocking state.
Spin-locks reduce overhead by avoiding blocking, but they require busy loops that consume CPU time and are only suitable for scenarios where shared data is locked in a very short state.
Adaptive spin locking was introduced in JDK 1.6. Adaptive means that the number of spins is no longer fixed, but is determined by the number of spins on the same lock the previous time and the state of the lock owner.
Lock elimination
Lock elimination refers to the elimination of locks on shared data that are detected to be impossible to compete with.
Lock elimination is mainly supported by escape analysis. If shared data on the heap cannot escape and be accessed by other threads, it can be treated as private data and thus unlocked.
For code that doesn’t appear to be locked, many locks are implicitly added. For example, the following string concatenation code implicitly locks:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
Copy the code
String is an immutable class, and the compiler automatically optimizes String concatenation. Prior to JDK 1.5, this translates to a successive append() operation on a StringBuffer object:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
Copy the code
Each append() method has a synchronization block. The virtual machine looks at the variable sb and quickly discovers that its dynamic scope is limited within the concatString() method. That is, all references to sb never escape out of the concatString() method, and other threads cannot access it, so it can be eliminated.
Lock coarsening
If a series of continuous operations repeatedly lock and unlock the same object, frequent locking operations can lead to performance degradation.
The successive append() method in the sample code in the previous section is such a case. If the virtual machine detects that the same object is locked by such a piecemeal sequence of operations, the scope of locking will be extended (coarsened) outside the entire sequence of operations. For the example in the previous section, the code extends before the first append() operation and after the last append() operation, requiring only one lock.
Lightweight lock
JDK 1.6 introduced biased locking and lightweight locking, which enables locks to be in four states: unlocked, biasble, Lightweight locked, and heavyweight locking.
The following is the memory layout of the HotSpot VIRTUAL machine object header, which is called the Mark Word. The tag bits correspond to five states, which are given in the state table on the right. Except for the marked for GC state, the other four states have been described earlier.
On the left side of the figure below is a threaded virtual machine stack with a section called Lock Record, which is created during the Lightweight Lock run to hold the Mark Word for the Lock object. On the right is a lock object containing the Mark Word and other information.
Lightweight locks use the CAS operation to avoid the mutex overhead of heavyweight locks, as opposed to traditional heavyweight locks. For most locks, there is no competition during the synchronization cycle. Therefore, you do not need to use mutex for synchronization. You can use CAS for synchronization first.
If the lock object is unlocked and marked 0 01, the lock is unlocked. At this point, the virtual machine creates a Lock Record in the vm stack of the current thread, and then updates the Mark Word of the object to the Lock Record pointer using the CAS operation. If the CAS operation succeeds, the thread acquires the lock on the object, and the object’s Mark Word lock flag changes to 00, indicating that the object is in a lightweight lock state.
If the CAS operation fails, the virtual machine first checks whether the Mark Word of the object points to the vm stack of the current thread. If it does, the current thread already owns the lock object and can proceed directly to the synchronization block. Otherwise, the lock object has been preempted by another thread. If there are more than two threads competing for the same lock, the lightweight lock is no longer valid and should be inflated to a heavyweight lock.
Biased locking
The idea of biased locking is to favor the first thread to acquire the lock object so that the thread does not need to perform synchronization operations, or even CAS operations, after acquiring the lock.
When the lock object is first acquired by the thread, it enters the bias state marked 1 01. At the same time, the CAS operation is used to record the thread ID in Mark Word. If the CAS operation is successful, the thread does not need to perform any synchronization operation every time it enters the lock related synchronization block in the future.
Revoke Bias Is declared over when another thread attempts to acquire the lock object, and returns to the unlocked or lightweight lock state with Revoke Bias.
Good practice for multithreaded development
-
Give threads meaningful names to help you find bugs.
-
Narrow the synchronization range to reduce lock contention. For synchronized, for example, try to use synchronized blocks rather than synchronized methods.
-
Use more synchronization tools and less wait() and notify(). First, the synchronization classes CountDownLatch, CyclicBarrier, Semaphore and sanodomain simplify encoding, while wait() and notify() are difficult to implement complex control flow. Second, these synchronous classes are written and maintained by the best companies and will continue to be optimized and refined in subsequent JDKS.
-
Implement producer-consumer issues using BlockingQueue.
-
More concurrent collections and less synchronous collections, for example ConcurrentHashMap should be used instead of Hashtable.
-
Use local variables and immutable classes to ensure thread-safety.
-
Use thread pools instead of creating threads directly because thread creation is expensive and thread pools can effectively start tasks with a limited number of threads.
The resources
- BruceEckel. Ideas for Java programming: 4th edition [M]. China Machine Press, 2007.
- Zhou Zhiming. In-depth Understanding of Java Virtual Machine [M]. China Machine Press, 2011.
- Threads and Locks
- Thread communication
- Top 50 Java Threads interview questions
- BlockingQueue
- thread state java
- CSC 456 Spring 2012/ch7 MN
- Java – Understanding Happens-before relationship
- 6 장 Thread Synchronization
- How is Java’s ThreadLocal implemented under the hood?
- Concurrent
- JAVA FORK JOIN EXAMPLE
- Introduction to the Fork/Join framework
- Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing