Java learning note 11 - multithreaded tool class practice

Keywords: Programming Java Database Lambda SQL

Runnable interface

Runnable is an interface. There are only run methods in it. Thread also implements runnable interface, so to realize multithreading, you need to realize runnable interface finally, not to mention to directly demonstrate the code.

Runnable test code

public class RunnableDemo {
    public static void main(String[] args) {
        // You can define a class to implement the Runnable interface
        RunnableImpl runnable = new RunnableImpl();
        new Thread(runnable).start();

        // Easy writing
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "Current thread");
            }
        }).start();

        // lambda functional interface, more elegant
        new Thread(() -> System.out.println(Thread.currentThread().getName() + "Current thread")).start();
    }

}

class RunnableImpl implements Runnable {

    @Override
    public void run() {
        // Specific execution logic
        System.out.println(Thread.currentThread().getName() + "Current thread");
    }
}
Callable / Future

Callable is also an interface. There are only call methods in it. The difference between callable and Runnable is that callable can get return value, throw exception and so on by cooperating with future. Future represents the result of asynchronous calculation and provides the method to check whether the calculation is completed, wait for the calculation to be completed and get the result. FutureTask is commonly used. In fact, run is executed in run method. Let's look at it directly below Code.

Callable test code

import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Random random = new Random();
                return "The return value is:" + random.nextInt(200);
            }
        };

        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        String result = futureTask.get(); // Block, wait for execution result to continue
        System.out.println(result);
    }
}

Do you see the effect of Callable / Future? Yes, if we apply it to the actual business? I have an idea! Look at code ~

FutureTask test code

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class MyFutureTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // In the actual business scenario, a method will call multiple interfaces / query multiple database tables / multiple sql
        // The execution time is cumulative if it is executed in sequence, but what if it is executed with FutureTask? The execution time is the slowest one, which leads to inefficiency~
        // Here's a chestnut

        // Here you can use the MyFutureTask we wrote earlier. The effect is the same
        FutureTask<String> futureTask1 = new FutureTask<>(() -> {
            TimeUnit.SECONDS.sleep(3L);
            return "Task 1 interface completed";
        });
        FutureTask<String> futureTask2 = new FutureTask<>(() -> {
            TimeUnit.SECONDS.sleep(5L);
            return "Task 2 interface execution completed";
        });
        FutureTask<String> futureTask3 = new FutureTask<>(() -> {
            TimeUnit.SECONDS.sleep(2L);
            return "Task 3 interface execution completed";
        });

        long time = System.currentTimeMillis();
        new Thread(futureTask1).start();
        new Thread(futureTask2).start();
        new Thread(futureTask3).start();

        Map<String, String> result = new HashMap<>();
        result.put("result1", futureTask1.get());
        result.put("result2", futureTask2.get());
        result.put("result3", futureTask3.get());

        System.out.println(result);
        System.out.println("Execution time:" + (System.currentTimeMillis() - time));
    }
}
CountDownLatch use

CountDownLatch can also implement asynchronous multithreading. You can understand that it is an inverted counter. Wait for the counter value to be 0, and then continue to execute. You can also do concurrent testing. Next, test the code directly~

CountDownLatch test code

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        CountDownLatch count = new CountDownLatch(3);
        Map<String, String> result = new HashMap<>();

        long time = System.currentTimeMillis();
        executorService.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(3L);
                result.put("result1", "Task 1 interface completed");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                count.countDown();
            }
        });
        executorService.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(5L);
                result.put("result2", "Task 2 interface execution completed");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                count.countDown();
            }
        });
        executorService.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(2L);
                result.put("result3", "Task 3 interface execution completed");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                count.countDown();
            }
        });

        count.await(); // Waiting counter to zero

        System.out.println(result);
        System.out.println("Execution time:" + (System.currentTimeMillis() - time));
        executorService.shutdown();
    }
}

The principle of CountDownLatch is similar to the lock we mentioned before, and it can also be implemented with AQS (see previous sections for AQS). Next, we write our own CountDownLatch.

Realize own CountDownLatch
import java.util.Iterator;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class MyCountDownLatch {
    //MyAqs aqs = new MyAqs() {
    //
    //    @Override
    //    public int tryAcquireShared() {
    //        return (this.getState().get() == 0) ? 1 : -1;
    //    }
    //
    //    @Override
    //    public boolean tryReleaseShared() {
    //        return this.getState().decrementAndGet() == 0;
    //    }
    //};
    //
    //public MyCountDownLatch(int count) {
    //    aqs.getState().set(count);
    //}
    //
    //public void await() {
    //    aqs.acquireShared();
    //}
    //
    //public void countDown() {
    //    aqs.releaseShared();
    //}


    volatile AtomicInteger count;
    LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();

    public MyCountDownLatch(int count) {
        this.count = new AtomicInteger(count);
    }

    public void await() {
        boolean addQ = true;
        while (this.count.get() != 0) {
            if (addQ) {
                waiters.offer(Thread.currentThread());
                addQ = false;
            } else {
                LockSupport.park(); // Wake up after receiving unpark notification, continue the cycle
            }
        }
        waiters.remove(Thread.currentThread()); // Remove thread from waiting collection
    }

    public void countDown() {
        if (this.count.decrementAndGet() == 0) {
            // Notify other waiting threads
            Iterator<Thread> iterator = waiters.iterator();
            while (iterator.hasNext()) {
                Thread waiter = iterator.next();
                LockSupport.unpark(waiter); // Wake up thread to continue
            }
        }
    }
}
CyclicBarrier use

CyclicBarrier, also known as "thread fence", specifies the number of fence threads when creating an object; automatically performs the specified task when the number of threads reaches the specified amount; the important difference between CyclicBarrier and CountDownLatch is that CyclicBarrier objects can trigger execution multiple times; typical application scenarios:

  1. Large amount of data, realizing batch insert operation database
  2. Statistics for the whole year, 12 threads for each month, after all statistics are completed, the statistics will be summarized;

CyclicBarrier test code

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.LinkedBlockingQueue;

public class CyclicBarrierDemo {
    public static void main(String[] args) throws InterruptedException {
        LinkedBlockingQueue<String> sqls = new LinkedBlockingQueue<>();
        // Task 1 + 2 + 3... 1000 is divided into 100 tasks (1 +.. 10, 11 + 20) - > 100 threads to process.

        // barrierAction execution is triggered whenever four threads are in wait state
        CyclicBarrier barrier = new CyclicBarrier(4, new Runnable() {
            @Override
            public void run() {
                // This is to trigger batch execution every 4 database operations
                System.out.println("There are 4 threads executing, and batch insertion begins: " + Thread.currentThread().getName());
                for (int i = 0; i < 4; i++) {
                    System.out.println(sqls.poll());
                }
            }
        });

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    sqls.add("data - " + Thread.currentThread().getName()); // Cache up
                    Thread.sleep(1000L); // Simulation database operation time consuming
                    barrier.await(); // Wait for the fence to open, and four threads will execute this code before continuing to execute
                    System.out.println(Thread.currentThread().getName() + "Insert completed");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }

        Thread.sleep(2000);
    }
}

Semaphore use

Semaphore, also known as semaphore, controls multiple threads to scramble for permission; typical scenario: token bucket, code concurrent processing flow restriction.

Semaphore test code

import java.util.Random;
import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        SemaphoreDemo semaphoreTest = new SemaphoreDemo();
        int N = 9; // Number of guests
        Semaphore semaphore = new Semaphore(5); // Number of hands, limit number of requests
        for (int i = 0; i < N; i++) {
            String vipNo = "vip-00" + i;
            new Thread(() -> {
                try {
                    semaphore.acquire(); // Get token

                    semaphoreTest.service(vipNo);

                    semaphore.release(); // release token
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    // Limit current control 5 threads access at the same time
    public void service(String vipNo) throws InterruptedException {
        System.out.println("Come out upstairs to meet a VIP, VIP number" + vipNo + ",...");
        Thread.sleep(new Random().nextInt(3000));
        System.out.println("Welcome VIP out, VIP No" + vipNo);
    }
}
Realize your Semaphore
public class MySemaphore {
    MyAqs aqs = new MyAqs() {

        @Override
        public int tryAcquireShared() { // Semaphore acquisition, quantity - 1
            for (; ; ) {
                int count = getState().get();
                int n = count - 1;
                if (count <= 0 || n < 0) {
                    return -1;
                }
                if (getState().compareAndSet(count, n)) {
                    return 1;
                }
            }
        }

        @Override
        public boolean tryReleaseShared() { // Semaphore release, quantity + 1
            return this.getState().incrementAndGet() >= 0;
        }
    };

    /**
     * Set the number of tokens
     */
    public MySemaphore(int count) {
        aqs.getState().set(count);
    }

    /**
     * Get token
     */
    public void acquire() {
        aqs.acquireShared();
    }

    /**
     * release token
     */
    public void release() {
        aqs.releaseShared();
    }

}
Use of Fork / Join framework

The ForkJoinPool is an implementation of the ExecutorService interface, designed for work that can be recursively broken down into small chunks.

The Fork / Join framework allocates tasks to the working threads in the thread pool, making full use of the advantages of multiprocessors and improving program performance.

The first step in using the Fork / Join framework is to write code that does part of the work. Similar pseudo codes are as follows:

If (the current working part is small enough)

Do the work directly

Other

Split the current work into two parts

Call these two parts and wait for the result

Wrap this code in the ForkJoinTask subclass, usually a recursive task (which can return results) or a recursive action.

Implementation ideas:

  1. Each Worker thread maintains a task queue, that is, the task queue in the ForkJoinWorkerThread
  2. Task queue is a two-way queue, so LIFO and FIFO can be realized at the same time
  3. Subtasks are added to the task queue of the Worker thread where the original task is located
  4. The Worker thread uses LIFO to get tasks, and the tasks in the last queue get out first (the subtasks always join the queue later, but they need to be executed first)
  5. When the task queue is empty, a task execution will be taken from the queue of other workers
  6. If a Worker thread encounters a join operation and is processing other tasks at this time, it will wait until the end of this task. Otherwise, return directly
  7. If a Worker thread fails to steal a task, it will take a break with yield or sleep and try again (if all threads are idle, i.e. there is no task running, then the thread will enter the blocking state and wait for the new task to arrive)

apply

Use as few thread pools as possible - in most cases, the best decision is to use one for each application or system

If no specific tuning is required, use the default public thread pool

Split the ForkJoinTask into subtasks with reasonable thresholds

Avoid any blocking in the ForkJoinTask

Suitable for data processing, result summary, statistics and other scenarios;

java8 instance: java.util.Arrays class for the parallelSort() method

Conclusion: the theory of performance improvement brought by job theft is that API is more complex, and the controllability in actual research and development is not as good as other APIs.

ForkJoinPool test code

import java.util.concurrent.*;

public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Essentially a thread pool, the default number of threads: the number of cores of the CPU
        // By default, the number of parallel threads is equal to the number of processors available
        // The main difference between ForkJoinPool and other types of ExecutorService is that it uses work stealing:
        // All threads in the pool attempt to find and perform tasks submitted to the pool and / or created by other active tasks
        // (if there is no work, it will eventually block waiting for work).
        ForkJoinPool forkJoinPool = new ForkJoinPool();

        // Suitable for data processing, result summary, statistics and other scenarios
        // Take chestnut for example to calculate 1 + 2 + 3 + The result of 100W, split multiple threads for calculation, merge and summarize to return
        // Query the data of multiple interfaces, merge and return
        // For other examples, look up multiple table data in the database, and query in multiple times
        // fork/join
        // forkJoinPool.submit()

        // Calculate 1 + 2 + 3 + The result of 100W is just an example
        long time1 = System.currentTimeMillis();
        // Submit a decomposable recursive task
        ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(new SumTask(1, 1000000));
        System.out.println("The result is:" + forkJoinTask.get() + " execution time" + (System.currentTimeMillis() - time1));
        //forkJoinPool.shutdown();

        // It takes time to actually perform Fork, Join, access from queue and other operations. Please use it in the appropriate scenario
        long time2 = System.currentTimeMillis();
        long sum = 0L;
        for (int i = 1; i <= 1000000; i++) {
            sum += i;
        }
        System.out.println("The result is:" + sum + " execution time" + (System.currentTimeMillis() - time2));

        // Submit a decomposable recursive action task
        forkJoinPool.submit(new PrintAction(1, 1000));
        //Block the current thread until all tasks in the ForkJoinPool are completed
        forkJoinPool.awaitTermination(2, TimeUnit.SECONDS);
        forkJoinPool.shutdown();
    }
}

class SumTask extends RecursiveTask<Long> {

    private static final int THRESHOLD = 1000;
    private int start; // Start subscript
    private int end; // End subscript

    public SumTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        // If the calculation amount of the current task is within the threshold, the calculation will be performed directly
        if (end - start < THRESHOLD) {
            return computeByUnit();
        } else { // If the calculation amount of the current task exceeds the threshold range, split the calculation task
            // Calculate intermediate index
            int middle = (start + end) / 2;
            //Define subtask - iteration idea
            SumTask left = new SumTask(start, middle);
            SumTask right = new SumTask(middle + 1, end);
            //Divide subtask fork
            left.fork();
            right.fork();
            //Consolidated calculation results
            return left.join() + right.join();
        }
    }

    private long computeByUnit() {
        long sum = 0L;
        for (int i = start; i <= end; i++) {
            sum += i;
        }
        return sum;
    }
}

class PrintAction extends RecursiveAction {

    private static final int THRESHOLD = 20;
    private int start; // Start subscript
    private int end; // End subscript

    public PrintAction(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        //Start printing when the end start value is less than MAX
        if ((end - start) < THRESHOLD) {
            computeByUnit();
        } else {
            // Break a big task into two small tasks
            int middle = (start + end) / 2;
            PrintAction left = new PrintAction(start, middle);
            PrintAction right = new PrintAction(middle + 1, end);
            left.fork();
            right.fork();
        }
    }

    private void computeByUnit() {
        for (int i = start; i <= end; i++) {
            System.out.println(Thread.currentThread().getName() + "Value:" + i);
        }
    }
}

Posted by ilovetoast on Mon, 25 Nov 2019 21:23:15 -0800