Multithreading and highly concurrent learning

Posted by timandkitty on Mon, 07 Mar 2022 17:19:20 +0100

Interview questions

1. It is required to print A1B2C3... Z26 in thread order

Solution 1 (LockSupport):
The code is as follows:

  static Thread t1 = null, t2 = null;

    public static void main(String[] args) {

        t1 = new Thread(() -> {

            for (char a ='A'; a <= 'Z'; a++) {
                System.out.print(a);
                LockSupport.unpark(t2);
                LockSupport.park();
            }

        });

        t2 = new Thread(() -> {
            for (int i = 1; i <= 26; i++) {
                LockSupport.park();
                System.out.print(i);
                LockSupport.unpark(t1);
            }
        });

        t1.start();
        t2.start();
    }

There may be a question in the above code, that is, why t2 should locksupport Park() at the front?
This purpose is to ensure that when t1 and t2 compete for CPU time slice during operation, if t2 obtains the CPU execution time slice, t2 park will be directly there, wait for t1 to execute unpark and then output the corresponding number, so as to ensure that the execution sequence of t1 must be before t2.

The second solution:
Using synchronized_ wait_ notify
Solution 1 (use countdowncatch to control which thread executes first):

    private static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) {
        final Object o = new Object();

        new Thread(() -> {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o) {
                for (int i = 1; i <= 26; i++) {
                    System.out.print(i);
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();//Finally, this wake-up operation should be performed to end the thread
            }
        }).start();

        new Thread(() -> {
            latch.countDown();
            synchronized (o) {
                for (char c = 'A'; c <= 'Z'; c++) {
                    System.out.print(c);
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();//Finally, this wake-up operation should be performed to end the thread
            }

        }).start();
    }

Solution 2:

  private static volatile boolean t2Started = false;

   // private static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) {
        final Object o = new Object();

        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();

        new Thread(() -> {
         /*   try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/

            synchronized (o) {
                while(!t2Started) {
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                for (char c : aI) {
                    System.out.print(c);
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                o.notify();
            }
        }, "t1").start();

        new Thread(() -> {

            synchronized (o) {
                for (char c : aC) {
                    System.out.print(c);
                  //  latch.countDown();
                    t2Started = true;
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }
        }, "t2").start();
    }

The volatile modifier controls which thread executes first

The third solution is Lock_Condition:

    private static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();

        new Thread(() -> {

            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            lock.lock();
            try {
                for (int i = 1; i <= 26; i++) {
                    System.out.print(i);
                    condition2.signal();
                    condition1.await();
                }
                condition2.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }


        }, "t1").start();

        new Thread(() -> {
            lock.lock();
            try {
                for (char c = 'A'; c <= 'Z'; c++) {
                    System.out.print(c);
                    latch.countDown();
                    condition1.signal();
                    condition2.await();
                }
                condition1.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }


        }, "t2").start();
    }

The fourth solution is CAS:

//The enumeration guarantee procedure is more rigorous
   enum ReadyToRun {T1, T2}

    static volatile ReadyToRun r = ReadyToRun.T1; //Think about why you have to be volatile

    public static void main(String[] args) {

        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();

        new Thread(() -> {

            for (char c : aI) {
                while (r != ReadyToRun.T1) {
                }
                System.out.print(c);
                r = ReadyToRun.T2;
            }

        }, "t1").start();

        new Thread(() -> {

            for (char c : aC) {
                while (r != ReadyToRun.T2) {
                }
                System.out.print(c);
                r = ReadyToRun.T1;
            }
        }, "t2").start();
    }

volatile ensures visibility between threads.
You can also use AutomicInteger

  static AtomicInteger threadNo = new AtomicInteger(1);


    public static void main(String[] args) {

        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();


        new Thread(() -> {

            for (char c : aI) {
                while (threadNo.get() != 1) {
                }
                System.out.print(c);
                threadNo.set(2);
            }

        }, "t1").start();

        new Thread(() -> {

            for (char c : aC) {
                while (threadNo.get() != 2) {
                }
                System.out.print(c);
                threadNo.set(1);
            }
        }, "t2").start();
    }

The fifth solution BlockingQueue:

   static BlockingQueue<String> q1 = new ArrayBlockingQueue(1);
    static BlockingQueue<String> q2 = new ArrayBlockingQueue(1);

    public static void main(String[] args) throws Exception {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();

        new Thread(() -> {

            for (char c : aI) {
                System.out.print(c);
                try {
                    q1.put("ok");
                    q2.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }, "t1").start();

        new Thread(() -> {

            for (char c : aC) {
                try {
                    q1.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(c);
                try {
                    q2.put("ok");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }, "t2").start();


    }

Sixth solution (PipedStream):
It is equivalent to establishing a virtual pipeline between two threads (but very inefficient) and then communicating with each other through the pipeline.
If the first thread wants to write on the second thread
Then the first thread has an outputstream and the second thread has an inputstream

  public static void main(String[] args) throws Exception {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();

        PipedInputStream input1 = new PipedInputStream();
        PipedInputStream input2 = new PipedInputStream();
        PipedOutputStream output1 = new PipedOutputStream();
        PipedOutputStream output2 = new PipedOutputStream();

        input1.connect(output2);
        input2.connect(output1);

        String msg = "Your Turn";


        new Thread(() -> {

            byte[] buffer = new byte[9];

            try {
                for (char c : aI) {
                    input1.read(buffer);

                    if (new String(buffer).equals(msg)) {
                        System.out.print(c);
                    }

                    output1.write(msg.getBytes());
                }

            } catch (IOException e) {
                e.printStackTrace();
            }

        }, "t1").start();

        new Thread(() -> {

            byte[] buffer = new byte[9];

            try {
                for (char c : aC) {

                    System.out.print(c);
					//Both write and read are blocked here
                    output2.write(msg.getBytes());

                    input2.read(buffer);

                    if (new String(buffer).equals(msg)) {
                        continue;
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
            }

        }, "t2").start();
    }

The seventh solution (TransferQueue):

   public static void main(String[] args) {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();

        TransferQueue<Character> queue = new LinkedTransferQueue<Character>();
        new Thread(() -> {
            try {
                for (char c : aI) {
                    //Block first
                    System.out.print(queue.take());
                    queue.transfer(c);
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();

        new Thread(() -> {
            try {
                for (char c : aC) {
                    //Give this character to thread t1
                    queue.transfer(c);
                    System.out.print(queue.take());
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }

Several common classes of thread pool

Callable

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> c = new Callable() {
            @Override
            public String call() throws Exception {
                return "Hello Callable";
            }
        };

        ExecutorService service = Executors.newCachedThreadPool();
        Future<String> future = service.submit(c); //asynchronous

        System.out.println(future.get());//block

        service.shutdown();
    }

Callable can only represent one task, but it cannot be used as a Future.

FutureTask

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        FutureTask<Integer> task = new FutureTask<>(() -> {
            TimeUnit.MILLISECONDS.sleep(500);
            return 1000;
        }); //new Callable () { Integer call();}

        new Thread(task).start();

        System.out.println(task.get()); 


    }

FutureTask can be used as a task or Future, because it saves not only the task but also the execution results.
The reason is that FutureTask implements RunnableFuture

RunnableFuture inherits Runnable and Future, in which Runnable stores tasks and Future stores results.
Call
Callable - > runnable + return value
Future - > store the future results of execution
FutureTask->Future+Runnable

CompletableFuture

Combined processing of various tasks

  • Suppose you can provide a service to query the prices of the same kind of products on major e-commerce websites and summarize and display them
  public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start, end;

        /*start = System.currentTimeMillis();

        priceOfTM();
        priceOfTB();
        priceOfJD();

        end = System.currentTimeMillis();
        System.out.println("use serial method call! " + (end - start));*/

        start = System.currentTimeMillis();

        CompletableFuture<Double> futureTM = CompletableFuture.supplyAsync(() -> priceOfTM());
        CompletableFuture<Double> futureTB = CompletableFuture.supplyAsync(() -> priceOfTB());
        CompletableFuture<Double> futureJD = CompletableFuture.supplyAsync(() -> priceOfJD());

        CompletableFuture.allOf(futureTM, futureTB, futureJD).join();
        end = System.currentTimeMillis();
        System.out.println("use completable future! " + (end - start));

    private static double priceOfTM() {
        delay();
        return 1.00;
    }

    private static double priceOfTB() {
        delay();
        return 2.00;
    }

    private static double priceOfJD() {
        delay();
        return 3.00;
    }

    /*private static double priceOfAmazon() {
        delay();
        throw new RuntimeException("product not exist!");
    }*/

    private static void delay() {
        int time = new Random().nextInt(500);
        try {
            TimeUnit.MILLISECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("After %s sleep!\n", time);
    }

As above, multiple tasks are executed asynchronously through completable future, and through completable future allOf(futureTM, futureTB, futureJD). The join () method collects the execution results of multiple tasks, and then executes the subsequent business logic.

Topics: Java