JUC Lecture 3: Java Concurrency-Thread Basics

JUC Lecture 3: Java Concurrency-Thread Basics

This article is the third lecture of JUC. It mainly introduces the basics of threads briefly and provides a foundation for the in-depth introduction of Java concurrency knowledge in subsequent chapters.

1. Understand the interview questions from major BAT companies

Please continue with these questions, which will greatly help you better understand the basics of threads.

  • What are the states of a thread? Explain the ways to transition from one state to another.
  • What are the common ways to use threads?
  • What are the basic threading mechanisms?
  • What are the ways to interrupt threads?
  • What are the mutually exclusive synchronization methods of threads ? How to compare and choose?
  • What are the ways of cooperation between threads ?

2. Thread state transition

image

2.1. New

It has not been started since it was created.

2.2. Runnable

It may be running or waiting for a CPU time slice.

Contains Running and Ready in the operating system thread status .

2.3. Blocking

Waiting to acquire an exclusive lock , this state will end if its thread releases the lock.

2.4. Waiting indefinitely (Waiting)

Wait for other threads to wake up explicitly, otherwise they will not be allocated CPU time slices.

Entry method Exit method
Object.wait() method without setting Timeout parameter Object.notify() / Object.notifyAll()
Thread.join() method without setting Timeout parameter The called thread has completed execution
LockSupport.park() method -

2.5. Timed Waiting

There is no need to wait for other threads to wake up explicitly, it will be automatically woken up by the system after a certain period of time.

When calling the Thread.sleep() method to put a thread into a time-limited waiting state, it is often described as "putting a thread to sleep".

When calling the Object.wait() method to cause a thread to wait for a limited time or wait indefinitely , it is often described as "suspending a thread".

Sleep and suspend are used to describe behavior, while blocking and waiting are used to describe state.

The difference between blocking and waiting is that blocking is passive, waiting to acquire an exclusive lock . Waiting is active and is entered by calling methods such as Thread.sleep() and Object.wait() .

Entry method Exit method
Thread.sleep() method time's up
Object.wait() method with Timeout parameter set End of time / Object.notify() / Object.notifyAll()
Thread.join() method with Timeout parameter set Time ends/The called thread completes execution
LockSupport.parkNanos() method -
LockSupport.parkUntil() method -

2.6. Death (Terminated)

It can be that the thread ends itself after completing the task , or it ends due to an exception .

3. How to use threads

The four common ways to create Java threads are:

  • Implement the Runnable interface;
  • Implement threads with return values ​​through ExecutorService and Callable
  • Inherit the Thread class;
  • Based on thread pool.

As shown below:
Insert image description here

A class that implements the Runnable and Callable interfaces can only be regarded as a task that can be run in a thread , not a thread in the true sense, so it needs to be called through Thread in the end. It can be said that tasks are executed through thread driving .

3.1. Implement the Runnable interface

Implement the Runnable interface to create a thread class . The method of implementing this run()class is based on the specifications of the Java programming language. If the subclass has inherited (extends) a class, it can no longer directly inherit the Thread class. At this time, you can create a thread by implementing the Runnable interface. The specific implementation process is: create a ChildrenClassThread thread by implementing the Runnable interface, instantiate a thread instance named childrenThread, create an instance of the Thread class and pass in the childrenThread thread instance, and call the thread's start method to start the thread.

  • Start the thread by calling the start() method of Thread.
public class MyRunnable implements Runnable {
    
    
    public void run() {
    
    
        // ...
    }
}
public static void main(String[] args) {
    
    
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}
  • Advantages : The thread class only implements the Runable interface and can also inherit other classes. In this way, multiple threads can share the same target object, so it is very suitable for multiple identical threads to process the same resource, so that the CPU code and data can be separated to form a clear model, which better reflects the Object-oriented thinking;
  • Disadvantages : Programming is slightly complicated. If you need to access the current thread, you must use the Thread.currentThread() method

3.2. Implement the Callable interface

  • Threads are created through the Callable and Future interfaces.
    • The Callable interface is a method that allows the thread to return results after completion of execution .

Compared with Runnable, Callable can have a return value, and the return value is encapsulated by FutureTask .

public class MyCallable implements Callable<Integer> {
    
    
    public Integer call() {
    
    
        return 123;
    }
}
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());
}

3.3. Inherit the Thread class

Create a thread class by inheriting the Thread class . This subclass overrides runthe methods of the Thread class. The Thread class implements the Runnable interface and defines some methods for operating threads. We can create a thread by inheriting the Thread class.

  • When the start() method is called to start a thread, the virtual machine puts the thread into the ready queue waiting to be scheduled . When a thread is scheduled, the run() method of the thread will be executed.
public class MyThread extends Thread {
    
    
    public void run() {
    
    
        // ...
    }
}
public static void main(String[] args) {
    
    
    MyThread mt = new MyThread();
    mt.start();
}
  • Advantages : Simple to write, if you need to access the current thread, you don’t need to use Thread.currentThread()a method, just use this directly to get the current thread;
  • Disadvantages : Because the thread class has inherited the Thread class, it cannot inherit other parent classes.

3.4. Use thread pool

3.5. Implement interface VS inherit from Thread

Implementing an interface is better because:

  • Java does not support multiple inheritance, so if you inherit the Thread class, you cannot inherit other classes, but you can implement multiple interfaces;
  • The class may only need to be executable, and inheriting the entire Thread class is too expensive.

4. Basic thread mechanism

4.1、Executor

Executors manage the execution of multiple asynchronous tasks without requiring the programmer to explicitly manage thread lifecycles . Asynchronous here means that the execution of multiple tasks does not interfere with each other and does not require 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: Equivalent 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();
}

4.2、Daemon

Daemon threads are threads that provide services in the background when the program is running and are not an integral part of the program.

When all non-daemon threads end, the program terminates and all daemon threads are killed.

main() belongs to the non-daemon thread.

Use the setDaemon() method to set a thread as a daemon thread .

public static void main(String[] args) {
    
    
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}

4.3、sleep()

Thread.sleep(millisec)The method will sleep the currently executing thread, millisec unit is milliseconds.

sleep() may throw InterruptedException because exceptions cannot be propagated across threads back into main() and therefore must be handled locally. Other exceptions thrown in the thread also need to be handled locally.

public void run() {
    
    
    try {
    
    
        Thread.sleep(3000);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}

4.4、yield()

The 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 other threads for execution. This method is only a suggestion to the thread scheduler, and only a suggestion that other threads with the same priority can run.

public void run() {
    
    
    Thread.yield();
}

5. Thread interruption

A thread will automatically end after it is executed. If an exception occurs during operation, it will also end early.

5.1、InterruptedException

Interrupt a thread by calling interrupt() on the thread. If the thread is blocked, waiting for a limited time, or waiting indefinitely, an InterruptedException will be thrown , thus ending the thread early. However, I/O blocking and synchronized lock blocking cannot be interrupted .

For the following code, start a thread in main() and then interrupt it. Since the Thread.sleep() method is called in the thread, an InterruptedException will be thrown, thus ending the thread early and not executing subsequent statements.

public class InterruptExample {
    
    
    private static class MyThread1 extends Thread {
    
    
        @Override
        public void run() {
    
    
            try {
    
    
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
  	// 下面这行代码不会被执行
    System.out.println("Main run");
}
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)

5.2、interrupted()

If a thread's run() method executes an infinite loop and does not perform operations such as sleep() that would throw an InterruptedException, then calling the thread's interrupt() method cannot cause the thread to end early.

However, calling the interrupt() method will set the thread's interrupt flag , and calling the interrupted() method will return true. Therefore, you can use the interrupted() method in the loop body to determine whether the thread is in an interrupted state, thereby ending the thread early.

public class InterruptExample {
    
    
    private static class MyThread2 extends Thread {
    
    
        @Override
        public void run() {
    
    
            while (!interrupted()) {
    
    
                // ..
            }
            System.out.println("Thread end");
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread thread2 = new MyThread2();
    thread2.start();
    thread2.interrupt();
}
Thread end

5.3. Executor interrupt operation

Calling the shutdown() method of the Executor will wait for all threads to finish executing before shutting down . However, if the shutdownNow() method is called, it is equivalent to calling the interrupt() method of each thread.

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");
}
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)

If you only want to interrupt a thread in the Executor, you can submit a thread by using the submit() method, which will return a Future<?> object, and you can interrupt the thread by calling the cancel(true) method of the object.

Future<?> future = executorService.submit(() -> {
    
    
    // ..
});
future.cancel(true);

6. Thread mutual exclusion synchronization

Java provides two lock mechanisms to control mutually exclusive access to shared resources by multiple threads . The first is synchronized implemented by the JVM, and the other is ReentrantLock implemented by the JDK.

6.1、synchronized

1. Synchronize a code block

public void func() {
    
    
    synchronized (this) {
    
    
        // ...
    }
}

It only works on the same object, if synchronization blocks are called on two objects, no synchronization will occur .

For the following code, two threads are executed using ExecutorService. Since the synchronization code block of the same object is called , the two threads will be synchronized. When one thread enters the synchronization statement block, the other thread must wait.

public class SynchronizedExample {
    
    

    public void func1() {
    
    
        synchronized (this) {
    
    
            for (int i = 0; i < 10; i++) {
    
    
                System.out.print(i + " ");
            }
        }
    }
}

public static void main(String[] args) {
    
    
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

For the following code, the two threads call the synchronization code block of different objects, so the two threads do not need to be synchronized . It can be seen from the output that the two threads are executed cross-cuttingly.

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());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

2. Synchronize a method

public synchronized void func () {
    
    
    // ...
}

It acts on the same object as the synchronized code block.

3. Synchronize a class

public void func() {
    
    
    synchronized (SynchronizedExample.class) {
    
    
        // ...
    }
}

Acts on the entire class, that is to say, two threads calling this synchronization statement on different objects of the same class will also be synchronized .

public class SynchronizedExample {
    
    

    public void func2() {
    
    
        synchronized (SynchronizedExample.class) {
    
    
            for (int i = 0; i < 10; i++) {
    
    
                System.out.print(i + " ");
            }
        }
    }
}

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());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

4. Synchronize a static method

public synchronized static void fun() {
    
    
    // ...
}

Applies to the entire class.

6.2、ReentrantLock

ReentrantLock is a lock in the java.util.concurrent(JUC) package.

public class LockExample {
    
    
    private Lock lock = new ReentrantLock();
    public void func() {
    
    
        lock.lock();
        try {
    
    
            for (int i = 0; i < 10; i++) {
    
    
                System.out.print(i + " ");
            }
        } finally {
    
    
          	// 确保释放锁,从而避免发生死锁。
            lock.unlock();
        }
    }
}

public static void main(String[] args) {
    
    
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

6.3. Comparison

1. Implementation of lock

synchronized is implemented by JVM, while ReentrantLock is implemented by JDK.

2. Performance

The new version of Java has made many optimizations to synchronized, such as spin locks , etc. Synchronized is roughly the same as ReentrantLock.

3. Waiting can be interrupted

When the thread holding the lock does not release the lock for a long time, the waiting thread can choose to give up waiting and deal with other things instead .

ReentrantLock can be interrupted, but synchronized cannot.

4. Fair lock

Fair lock means that when multiple threads are waiting for the same lock, they must obtain the lock in sequence according to the time order of applying for the lock.

Locks in synchronized are unfair, and ReentrantLock is also unfair by default, but it can also be fair.

5. The lock is bound to multiple conditions

A ReentrantLock can bind multiple Condition objects at the same time.

6.4. Use options

Unless you need to use the advanced features of ReentrantLock, use synchronized first . This is because synchronized is a lock mechanism implemented by the JVM. The JVM natively supports it, but ReentrantLock is not supported by all JDK versions. And using synchronized, you don't have to worry about deadlock problems caused by not releasing the lock, because the JVM will ensure that the lock is released .

7. Collaboration interviews between threads are essential

When multiple threads can work together to solve a problem, if some parts must be completed before other parts, then the threads need to be coordinated .

7.1、join()

Calling the join() method of another thread in a thread will suspend the current thread instead of busy waiting until the target thread ends.

For the following code, although thread b starts first, because the join() method of thread a is called in thread b, thread b will wait for thread a to finish before continuing to execute, so in the end it is guaranteed that the output of thread a precedes that of thread b . output .

public class JoinExample {
    
    

    private class A extends Thread {
    
    
        @Override
        public void run() {
    
    
            System.out.println("A");
        }
    }

    private class B extends Thread {
    
    

        private A a;

        B(A a) {
    
    
            this.a = a;
        }

        @Override
        public void run() {
    
    
            try {
    
    
                a.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
    
    
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}
public static void main(String[] args) {
    
    
    JoinExample example = new JoinExample();
    example.test();
}
A
B

7.2、Object的wait()/notify()/notifyAll()

Calling wait() causes the thread to wait for a certain condition to be met. The thread will be suspended while waiting. When other threads run and the condition is met, other threads will call notify() or notifyAll() to wake up the suspended thread.

They are all part of Object, not Thread.

It can only be used in synchronized methods or synchronized control blocks, otherwise IllegalMonitorStateExeception will be thrown at runtime.

During suspension using wait(), the thread releases the lock . This is because if the lock is not released, other threads cannot enter the object's synchronization method or synchronization control block, and then cannot execute notify() or notifyAll() to wake up the suspended thread, causing a deadlock.

public class WaitNotifyExample {
    
    
    public synchronized void before() {
    
    
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
    
    
        try {
    
    
            wait();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    
    
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after

The difference between wait() and sleep()

  • wait() is a method of Object, and sleep() is a static method of Thread;
  • wait() will release the lock, sleep() will not.

7.3、Condition的await()/signal()/signalAll()

The Condition class is provided in the java.util.concurrent class library to achieve coordination between threads . You can call the await() method on Condition to make the thread wait, and other threads call the signal() or signalAll() method to wake up the waiting thread. Compared with the waiting method of wait(), await() can specify the waiting conditions , so it is more flexible.

Use Lock to obtain a Condition object.

public class AwaitSignalExample {
    
    
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("before");
            condition.signalAll();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void after() {
    
    
        lock.lock();
        try {
    
    
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
    
    
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after

7.4、LockSupport 的park()/unpark()

Use park/unpark to achieve thread synchronization

class MyThread extends Thread {
    
    
    private Object object;

    public MyThread(Object object) {
    
    
        this.object = object;
    }

    public void run() {
    
    
        System.out.println("before unpark");
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        // 获取blocker
        System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));
        // 释放许可
        LockSupport.unpark((Thread) object);
        // 休眠500ms,保证先执行park中的setBlocker(t, null);
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        // 再次获取blocker
        System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));

        System.out.println("after unpark");
    }
}

public class test {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread(Thread.currentThread());
        myThread.start();
        System.out.println("before park");
        // 获取许可
        LockSupport.park("ParkAndUnparkDemo");
        System.out.println("after park");
    }
}

operation result:

before park
before unpark
Blocker info ParkAndUnparkDemo
after park
Blocker info null
after unpark

For details, please refer to this article: JUC Lecture 8: JUC Lock: Detailed explanation of LockSupport

Action1: There are three threads ABC. Thread C needs to wait for thread AB to complete before it can be executed?

Method 1: LockSupport + AtomicInteger

  • Execute thread C first, and use park() to suspend thread C. When threads A and B complete execution, the flag is decremented by 1 and judged whether it is 0. If it is 0, use unpark(c) to issue permission to thread C.
public static void main(String[] args) {
    
    
        AtomicInteger flag = new AtomicInteger(2);
        Thread c =  new Thread(()->{
    
    
            System.out.println("线程C开启,等待线程A、B执行完成才继续执行");
            LockSupport.park();
            System.out.println("线程C开始执行");
        });
        c.start();

        new Thread(()->{
    
    
            System.out.println("线程A开始执行");
            try {
    
    
                TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("线程A执行完成");
            if (flag.decrementAndGet() == 0){
    
    
            	//唤醒指定线程
                LockSupport.unpark(c);
            }
        }).start();
        
        new Thread(()->{
    
    
            System.out.println("线程B开始执行");
            try {
    
    
                TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("线程B执行完成");
            if (flag.decrementAndGet() == 0){
    
    
                LockSupport.unpark(c);
            }
        }).start();
}

Method 2: CountDownLatch

  • CountDownLatch has a counter. The countDown method decrements the counter, and the await method waits for the counter to reach 0. All await threads will block until the counter reaches 0 or the waiting thread is interrupted or times out.
public static void main(String[] args) {
    
       
		CountDownLatch latch = new CountDownLatch(2);
    new Thread(() -> {
    
    
        System.out.println("线程A开始执行");
        try {
    
    
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            latch.countDown();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        System.out.println("线程A执行完成");
    }).start();

    new Thread(() -> {
    
    
        System.out.println("线程B开始执行");
        try {
    
    
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            latch.countDown();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        System.out.println("线程B执行完成");
    }).start();

    new Thread(() -> {
    
    
        System.out.println("线程C开启,等待线程A、B执行完成才继续执行");
        try {
    
    
            latch.await();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        System.out.println("线程C执行完成");
    }).start();
}

Method three: CyclicBarrier

CyclicBarrier is similar to CountDownLatch, it can block a group of threads all to a certain state and then execute them simultaneously. The key difference between CyclicBarrier and CountDownLatch is that all threads must reach the location before execution can continue. CountDownLatch is used to wait for events, while CyclicBarrier is used to wait for other threads. All threads cannot continue execution until any thread is completed.

public static void main(String[] args) {
    
    
    CyclicBarrier barrier = new CyclicBarrier(3);
    //只有所有线程执行到了 await(),所有线程才会继续往下执行

    new Thread(() -> {
    
    
        System.out.println("线程A开始执行");
        try {
    
    
            //执行业务
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            System.out.println("线程A执行完成,等待其它线程一起冲破栅栏");
            barrier.await();
        } catch (Exception e) {
    
    
          	e.printStackTrace();
        }
        System.out.println("线程A执行完成");
    }).start();

    new Thread(() -> {
    
    
        System.out.println("线程B开始执行");
        try {
    
    
            //执行业务
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            System.out.println("线程B执行完成,等待其它线程一起冲破栅栏");
            barrier.await();
        } catch (Exception e) {
    
    
          	e.printStackTrace();
        }
        System.out.println("线程B执行完成");
    }).start();

    new Thread(() -> {
    
    
        try {
    
    
            System.out.println("线程C开启,等待线程AB执行完成一起冲破栅栏");
            barrier.await();
            //执行业务
        } catch (Exception e) {
    
    
          	e.printStackTrace();
        }
        System.out.println("线程C执行完成");
    }).start();
}

Guess you like

Origin blog.csdn.net/qq_28959087/article/details/132995120