table of Contents
ForkJoinPool branch/merge framework
background
Continue to relax... Organize the notes of studying the JUC thread last month. JUC is the abbreviation of java.util.concurrent package, the sample codes are all running in jdk8 environment
CASE
Introduction
CAS is the abbreviation of Compare And Swap, used to ensure the atomicity of data, which is the hardware support for concurrent operation and sharing of data
Contains three values: memory value V, estimated value A, updated value B
If and only if V == A, V = B, otherwise do nothing
application
The implementation class of CAS in Java is an atomic data class, which can modify the value atomically
The original modification such as i = i++ is divided into three steps:
int temp = i;
i = i + 1;
i = temp;
This is obviously unsafe in a multi-threaded environment
public class Main {
public static void main(String[] args) {
CASDemo casDemo = new CASDemo();
for (int i = 0; i < 10; i++) {
new Thread(casDemo).start();
}
}
}
class CASDemo implements Runnable {
private int mNum = 0;
@Override
public void run() {
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + ":" + getNum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getNum() {
return mNum++;
}
}
The output result may have duplicate values, but if you change to an atomic class, this will not happen
public class Main {
public static void main(String[] args) {
CASDemo casDemo = new CASDemo();
for (int i = 0; i < 10; i++) {
new Thread(casDemo).start();
}
}
}
class CASDemo implements Runnable {
private AtomicInteger mAtomicNum = new AtomicInteger(0);
@Override
public void run() {
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + ":" + getAtomicNum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getAtomicNum() {
return mAtomicNum.getAndIncrement();
}
}
In addition to using the CAS algorithm to ensure the atomicity of operations, atomic variables also use the volatile keyword to ensure memory visibility
CountDownLatch
Introduction
CountDownLatch (Latch): When some operations are completed, only the operations of all other threads are completed, the current operation will continue to execute
application
Usage Participate in the following code, and its comments
public class Main {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5); // 初始化闭锁对象,给出初值
LatchDemo demo = new LatchDemo(latch);
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
new Thread(demo).start();
}
latch.await(); // 等待闭锁值为0
System.out.println("耗费时间:" + (System.currentTimeMillis() - start));
}
}
class LatchDemo implements Runnable {
private final CountDownLatch mLatch;
public LatchDemo(CountDownLatch latch) {
mLatch = latch;
}
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
synchronized (mLatch) { // 由于闭锁对象被多个线程引用,所以此处加个同步锁
mLatch.countDown(); // 子线程执行完,闭锁值-1
}
}
}
Callable interface
Introduction
In addition to implementing the Runnable interface and inheriting the Thread class, implementing the Callable interface is the third way to implement threads. Compared to implementing the Runnable interface, the Callable interface needs to give types, methods have return values and exceptions, and need FutureTask support
use
First implement the Callable interface, specify the generic as String, and generate a random letter sequence of length 10
class CallableDemo implements Callable<String> {
private static final String sCHARACTERS = "abcdefghijklmnopqrstuvmxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private final Random mRandom = new Random();
@Override
public String call() throws Exception {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10; i++) {
int index = Math.abs(mRandom.nextInt()) % sCHARACTERS.length();
builder.append(sCHARACTERS.charAt(index));
}
return builder.toString();
}
}
Then use FutureTask to execute the task and get the result
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
CallableDemo demo = new CallableDemo();
FutureTask<String> task = new FutureTask<>(demo); // 实例化FutureTask,泛型和Callable的泛型一致
new Thread(task).start();
System.out.println(task.get()); // get()方法会阻塞,直到结果返回。因此FutureTask也可以用于闭锁
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
It is also relatively easy to use FutureTask to achieve blocking to count thread running time
try {
CallableDemo demo = new CallableDemo();
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
FutureTask<String> task = new FutureTask<>(demo);
new Thread(task).start();
System.out.println(task.get());
}
System.out.println("耗费时间:" + (System.currentTimeMillis() - start));
} catch (ExecutionException e) {
e.printStackTrace();
}
Lock sync lock
Introduction
In addition to the synchronization code block and the synchronization method, Lock is the third way to solve the multi-thread safety problem. It is more flexible than the synchronization method and the synchronization code block. It needs to be locked through the lock() method, and then released through the unlock() method.
use
Go directly to the code, the unlock() method should be placed in the finally block in the try-catch
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
Ticket ticket = new Ticket();
for (int i = 0; i < 3; i++) {
new Thread(ticket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Ticket implements Runnable {
private int mTick = 100;
private Lock mLock = new ReentrantLock();
@Override
public void run() {
while (mTick > 0) {
mLock.lock();
mTick--;
System.out.println(Thread.currentThread().getName() + ":" + mTick);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mLock.unlock();
}
}
}
}
volatile keyword
1 Introduction
Volatile is used to solve the memory visibility problem. The memory visibility problem refers to the problem that when multiple threads operate on shared data, because they access the TLAB of their own thread, the operation of each other is invisible. About TLAB, see the article heap JVM study notes of the relevant sections
To solve the memory visibility problem, you can use synchronized, lock, and volatile methods
2. Use
For the following code, the value of demo.isFlag() of the main thread is false. Because of the existence of TLAB, the flag value in the main thread and the child thread cannot be modified synchronously.
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
while (true) {
if (demo.isFlag()) {
System.out.println("........");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadDemo implements Runnable {
private boolean mFlag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (Exception e) {
}
mFlag = true;
}
public boolean isFlag() {
return mFlag;
}
public void setFlag(boolean flag) {
mFlag = flag;
}
}
But adding the volatile keyword is different. Volitale changes the memory visibility of the flag. When the child thread changes the flag, it is directly flushed to the memory; while the main thread reads the flag directly from the memory, and the value of the flag is changed to True
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
while (true) {
if (demo.isFlag()) {
System.out.println("........");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadDemo implements Runnable {
private volatile boolean mFlag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (Exception e) {
}
mFlag = true;
}
public boolean isFlag() {
return mFlag;
}
public void setFlag(boolean flag) {
mFlag = flag;
}
}
But the ensuing problem is that volatile will bring performance degradation, but it is still higher than synchronized, but volatile is not mutually exclusive, and can not guarantee the atomicity of variables, just change the variable operation from TLAB to main memory go with
Concurrent container class
Take ConcurrentHashMap as an example to learn about concurrent container classes
Introduction
HashMap thread is not safe, HashTable thread safe, but low efficiency.
ConcurrentHashMap uses a lock segmentation mechanism, and uses concurrentLevel to measure lock segmentation. The default value is 16. Each segment maintains a hash map, which is an independent lock, so when multiple threads access different lock segments, it is not only thread safe, but also efficient.
But since jdk1.8, the internal ConcurrentHashMap has also been replaced with the CAS algorithm
Other container classes
Synchronous TreeMap can be replaced with ConcurrentSkipListMap. When the number of readings and traversal is much larger than the number of updates, you can use CopyOnWriteArrayList to replace the synchronized ArrayList
The usage of the synchronous container class is the same as that of the ordinary container class. How to use it and how to use it, skip it here, but you can refer to the article Multithreading to realize the accumulation of sequence , which uses a concurrent chain queue
False wakeup
False wakeup means that when the wait() method is wrapped in an if statement, an error will occur when it is awakened, such as the following code
class Clerk {
private int mNum = 0;
public synchronized void add() throws InterruptedException {
if (mNum >= 1) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
wait();
}
mNum += 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
public synchronized void sub() throws InterruptedException {
if (mNum <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
wait();
}
mNum -= 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
}
class Producer implements Runnable {
private Clerk mClerk;
public Producer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.add();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Clerk mClerk;
public Consumer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.sub();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
When running two producers and two consumers, you will find that mCount is negative. This is because after the producer has notified the clerk, multiple consumers have performed the mCount-1 operation
In order to solve the false wakeup problem, the wait() method should be called in the loop
class Clerk {
private int mNum = 0;
public synchronized void add() throws InterruptedException {
while (mNum >= 1) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
wait();
}
mNum += 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
public synchronized void sub() throws InterruptedException {
while (mNum <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
wait();
}
mNum -= 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
}
class Producer implements Runnable {
private Clerk mClerk;
public Producer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.add();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Clerk mClerk;
public Consumer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.sub();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Condition's wake and wait
Synchronous locks can also implement producer consumers, and the Condition object in the lock object also has similar waiting and wake-up methods
class Clerk {
private int mNum = 0;
private Lock mLock = new ReentrantLock();
private Condition mCondition = mLock.newCondition();
public void add() throws InterruptedException {
mLock.lock();
try {
while (mNum >= 1) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
mCondition.await(50, TimeUnit.MILLISECONDS);
}
mNum += 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
mCondition.signalAll();
} finally {
mLock.unlock();
}
}
public void sub() throws InterruptedException {
mLock.lock();
try {
while (mNum <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
mCondition.await(50, TimeUnit.MILLISECONDS);
}
mNum -= 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
mCondition.signalAll();
} finally {
mLock.unlock();
}
}
}
class Producer implements Runnable {
private Clerk mClerk;
public Producer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.add();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Clerk mClerk;
public Consumer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.sub();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Start an equal number of producers and consumers to verify it
Use Condition to achieve orderly alternating threads, such as alternate printing of ABC
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
AlterDemo demo = new AlterDemo();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
demo.printA();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
demo.printB();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
demo.printC();
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class AlterDemo {
private int mNum = 1;
private Lock mLock = new ReentrantLock();
private Condition mCondition1 = mLock.newCondition();
private Condition mCondition2 = mLock.newCondition();
private Condition mCondition3 = mLock.newCondition();
public void printA() {
mLock.lock();
try {
while (mNum != 1) {
mCondition1.await();
}
System.out.println("A");
mNum = 2;
mCondition2.signal();
} catch (Exception e) {
} finally {
mLock.unlock();
}
}
public void printB() {
mLock.lock();
try {
while (mNum != 2) {
mCondition2.await();
}
System.out.println("B");
mNum = 3;
mCondition3.signal();
} catch (Exception e) {
} finally {
mLock.unlock();
}
}
public void printC() {
mLock.lock();
try {
while (mNum != 3) {
mCondition3.await();
}
System.out.println("C");
mNum = 1;
mCondition1.signal();
} catch (Exception e) {
} finally {
mLock.unlock();
}
}
}
Read-write lock
Introduction
The read-write lock is to ensure the mutual exclusion of write/write
usage
Use the readLock() and writeLock() methods to obtain the read lock and write lock of the read-write lock object
class WriteReadDemo {
private int mNum = 0;
private ReadWriteLock mLock = new ReentrantReadWriteLock();
public void set(int num) {
mLock.writeLock().lock();
try {
mNum = num;
} finally {
mLock.writeLock().unlock();
}
}
public void get() {
mLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + ": " + mNum);
} finally {
mLock.readLock().unlock();
}
}
}
Then start thread verification
WriteReadDemo demo = new WriteReadDemo();
for (int i = 0; i < 50; i++) {
new Thread(() -> demo.set((int) (Math.random() * 100)), "Write").start();
}
for (int i = 0; i < 100; i++) {
new Thread(demo::get, "Read" + i).start();
}
Thread Pool
Introduction
In order to reduce the overhead of creating, maintaining, and destroying threads, we can use thread pools to create scheduling threads instead of directly new
usage
The thread pool can be created through the relevant new method of Executors
newFixedThreadPool() creates a fixed-size thread pool
newCachedThreadPool() creates a cache thread pool, the capacity can be adjusted according to needs
newSingleThreadExecutor() creates a single thread pool
newScheduledThreadPool() creates a fixed-size thread pool and executes tasks regularly
ExecutorService pool = Executors.newFixedThreadPool(5);
After creation, you can execute the task through the submit() method of the thread pool
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
int sum = 0;
for (int j = 0; j < 51; j++) {
sum += j;
}
System.out.println(Thread.currentThread().getName() + ": " + sum);
});
}
Finally, close the thread pool
pool.shutdown();
shutdown() will wait for all running child threads to end and then shut down, and shutdownNow() will immediately shut down and terminate the child threads that have not yet ended.
For ScheduledExecutorService scheduling tasks, you need to call its schedule() method, pass in the runnable or callable object, delay and delay unit
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
pool.schedule(() -> {
int sum = 0;
for (int j = 0; j < 51; j++) {
sum += j;
}
System.out.println(Thread.currentThread().getName() + ": " + sum);
}, (long) (Math.random() * 1000), TimeUnit.MILLISECONDS);
}
pool.shutdown();
If you are interested in reading the thread pool source code, you can refer to the article Brief Analysis of Thread Pool Source Code for Android Development and Learning
ForkJoinPool branch/merge framework
Introduction
Fork/Join framework: If necessary, split a large task (Fork) into several small tasks until it can no longer be divided; then merge the results of the small tasks (Join) into the results of the large task
This is similar to the MapReduce framework, except that the splitting process of MapReduce is inevitable
Work stealing mode:
When a subtask thread completes the tasks in its task queue, and the big task has not ended, then it will randomly steal a task from the end of the queue of other subtask threads to execute
use
First define the task class, inherit the RecursiveTask parent class, specify the generic type, and implement the method compute()
class FJSumCalculate extends RecursiveTask<Long> {
private long mStart, mEnd;
private static final long sTHRESHOLD = 100L; // 拆分临界值
public FJSumCalculate(long start, long end) {
mStart = start;
mEnd = end;
}
@Override
protected Long compute() {
long length = mEnd - mStart;
if (length <= sTHRESHOLD) {
long sum = 0L;
for (long i = mStart; i <= mEnd; i++) {
sum += i;
}
return sum;
}
// 拆分
long middle = (mEnd + mStart) / 2;
FJSumCalculate left = new FJSumCalculate(mStart, middle - 1);
left.fork();
FJSumCalculate right = new FJSumCalculate(middle, mEnd);
right.fork();
// 合并
return left.join() + right.join();
}
Pay attention to the choice of critical value, not too small
Then test in the main method, use ForkJoinPool to perform the task
ForkJoinPool pool = new ForkJoinPool();
System.out.println(pool.invoke(new FJSumCalculate(1L, 99999L)));
pool.shutdown();
instead
LongStream can also be used in java8 to perform similar parallel accumulation logic
LongStream.rangeClosed(1L, 99999L).parallel().reduce(Long::sum).getAsLong();
Conclusion
This is the end of the JUC thread study notes, welcome everyone