JUC重点知识介绍急速通关版

一、什么是JUC

指的是java.util下的三个并发编程工具包。

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks
    在Java中,并发编程离不开多线程开发,Java实现多线程的方式主要有以下4种:
  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 开启线程池
    以下是几种方式的简单示例:
public class Demo1 {
    public static void main(String[] args) {
        // 继承Thread类
        Thread1 thread1 = new Thread1();
        thread1.start();
        System.out.println(Thread.currentThread().getName() + " is running ");
        // 实现Runnable接口
        Thread2 thread2 = new Thread2();
        Thread thread = new Thread(thread2);
        thread.start();
        // 实现Callable接口
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        FutureTask<Integer> futureTask = new FutureTask<>(new Thread3());
        executorService.execute(futureTask);
        try {
            Integer integer = futureTask.get();
            System.out.println(Thread.currentThread().getName() + " is running " + " result is " + integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }
    static class Thread1 extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " is running ");
        }
    }
    static class Thread2 implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " is running ");
        }
    }
    static class Thread3 implements Callable<Integer> {
        @Override
        public Integer call() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + " is running " + " result is " + 666);
            return 666;
        }
    }
}

二、线程和进程

在讨论进程和线程之前,有必要先了解一下并发和并行的概念。

并发:同一时刻多个线程访问同一个资源。

例:春运抢票,电商秒杀活动

并行:多项工作互不影响,同时进行。

例:泡面,烧水和准备方便面及料包可以同时进行。

进程:指在操作系统中运行的一个应用程序实例,进程是操作系统资源分配的最小单位。

线程:程序执行的最小单位,可以认为是进程内的一个独立执行流。一个进程可以包含多个线程,至少包含一个线程。

Java语言本身是不能直接开启线程的,通过查看start()方法的源代码,可以看到Java是通过调用一个叫start0()的native方法来实现开启线程的操作的。以下是截取的Thread.java的一部分源码。

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

线程从开启到执行完成,会经历一些状态,这些状态一起组成了线程的生命周期。一个线程有以下几种可能的状态,以下代码也来自Java Thread.java源码:

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,
    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,
    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,
    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called {@code Object.wait()}
     * on an object is waiting for another thread to call
     * {@code Object.notify()} or {@code Object.notifyAll()} on
     * that object. A thread that has called {@code Thread.join()}
     * is waiting for a specified thread to terminate.
     */
    WAITING,
    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,
    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;
}

从注释中可以看出,一个线程进入WAITING状态,除了LockSupport.park的情形外,主要有wait和sleep两种方式,二者的区别是面试经常会考察的内容:

  1. wait()方法属于Object类的成员方法,任何对象的实例都能调用。sleep()是Thread类的静态方法。
  2. wait()方法调用时会释放锁,而sleep()不需要占用锁,因此也无需释放。
  3. wait()方法必须在同步代码块中使用,不需要捕获异常。sleep()方法可以在任何地方使用,必须要捕获异常。

三、Lock锁与Synchronized

先来看一个案例:假设3个售票员,同时卖30张票。分别用Lock和Synchronized实现,怎样才能保证线程安全。

用Synchronized实现:

public class Demo2 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }).start();
    }
    static class Ticket {
        private int remaining = 30;
        public synchronized void sale() {
            if (remaining > 0) {
                System.out.println(Thread.currentThread().getName() + " sale " + remaining--);
            }
        }
    }
}

用Lock实现:

public class Demo3 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }).start();
    }
    static class Ticket {
        private int remaining = 30;
        Lock lock = new ReentrantLock();
        public void sale() {
            lock.lock();
            try {
                if (remaining > 0) {
                    System.out.println(Thread.currentThread().getName() + " sale " + remaining--);
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

通过对比以上两个例子,我们可以得出以下结论:

  • synchronized是Java内置的关键字,Lock是Java的一个接口;
  • synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁;
  • synchronized会自动释放锁,Lock必须手动释放锁;
  • synchronized和Lock都是可重入锁,synchronized是默认的非公平锁,Lock的锁是否公平锁可以设置;

四、线程间通信

最典型的线程间通信场景,莫过于生产者消费者模型。接下来分别以Object的wait/notify,Lock类中的await/signal来实现该案例。

wait/notify(这里有一个问题需要注意,当线程数量比较多的时候,notifyAll可能会导致虚假唤醒,因此这里的判断必须用while,而非if)

public class Demo4 {
    public static void main(String[] args) {
        Resource resource = new Resource();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                resource.produce();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                resource.consume();
            }
        }).start();
    }
    static class Resource {
        private int num = 0;
        public synchronized void produce() {
            try {
                if (num > 0) {//这里需要用while
                    this.wait();
                }
                num++;
                System.out.println(Thread.currentThread().getName() + " current num: " + num);
                this.notifyAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public synchronized void consume() {
            try {
                if (num == 0) {// 这里需要用while
                    this.wait();
                }
                num--;
                System.out.println(Thread.currentThread().getName() + " current num: " + num);
                this.notifyAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

await/signal(这里需要注意,使用await()和signal()的方法中必须有对应的锁,而不能以synchronized修饰)

package com.lantian3.practise.bio.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo5 {
    public static void main(String[] args) {
        Resource resource = new Resource();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                resource.produce();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                resource.consume();
            }
        }).start();
    }
    static class Resource {
        private int num = 0;
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        public void produce() {
            lock.lock();
            try {
                while (num > 0) {
                    condition.await();
                }
                num++;
                System.out.println(Thread.currentThread().getName() + " current num: " + num);
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void consume() {
            lock.lock();
            try {
                while (num == 0) {
                    condition.await();
                }
                num--;
                System.out.println(Thread.currentThread().getName() + " current num: " + num);
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

接下来看一个案例,使线程按约定顺序执行,这个案例中我们开启A,B,C三个线程,让它们依次执行。从代码中可以看出,主要是利用限制条件主动去唤醒对应的线程。

public class Demo6 {
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.printA();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.printB();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.printC();
            }
        }).start();
    }
    static class Test {
        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Condition condition3 = lock.newCondition();
        private int num = 1;
        public void printA() {
            lock.lock();
            try {
                while (num != 1) {
                    condition1.await();
                }
                System.out.println(Thread.currentThread().getName() + " AAAAAAAA ");
                num = 2;
                condition2.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void printB() {
            lock.lock();
            try {
                while (num != 2) {
                    condition2.await();
                }
                System.out.println(Thread.currentThread().getName() + " BBBBBBBB ");
                num = 3;
                condition3.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void printC() {
            lock.lock();
            try {
                while (num != 3) {
                    condition3.await();
                }
                System.out.println(Thread.currentThread().getName() + " CCCCCCCC ");
                num = 1;
                condition1.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

五、八锁现象

其实就是关于Java锁的8个问题,通过对这8个问题的探索,我们大致可以得出Java中的锁,究竟是对什么上锁。

  1. 一个资源类,有两个非静态方法,都被synchronized修饰,开启两个线程,分别调用两个方法,哪个方法先执行?
public class Demo7 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 这个情况下,由于先开启发短信线程,因此大概率先输出发短信。
        new Thread(() -> phone.sendSms()).start();
        new Thread(() -> phone.call()).start();
    }
    static class Phone {
        public synchronized void sendSms() {
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
}
  1. 如果,其中一个方法增加sleep方法,使其延迟执行,哪个方法先执行?
public class Demo7 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 发短信线程中增加延迟4秒,结果依然大概率先输出发短信。
        new Thread(() -> phone.sendSms()).start();
        new Thread(() -> phone.call()).start();
    }
    static class Phone {
        public synchronized void sendSms() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
}
  1. 在以上基础上增加一个非静态的非同步方法,三个方法同时执行,输出顺序是怎样的?
public class Demo7 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 无论普通方法位置在哪里,都会先输出,然后其他线程才会依次输出。
        new Thread(() -> phone.sendSms()).start();
        new Thread(() -> phone.call()).start();
        new Thread(() -> phone.printTest()).start();
    }
    static class Phone {
        public synchronized void sendSms() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
        public void printTest() {
            System.out.println("普通方法");
        }
    }
}
  1. 如果在调用发短信和打电话方法时,分别创建两个Phone对象去调用,输出顺序如何?
public class Demo7 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        // 此种情况下,先输出打电话,由此我们可以初步猜测,非静态方法的锁和对象有很大关系。
        new Thread(() -> phone1.sendSms()).start();
        new Thread(() -> phone2.call()).start();
    }
    static class Phone {
        public synchronized void sendSms() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
}
  1. 如果把发短信和打电话的方法改为静态方法,输出顺序如何?
public class Demo7 {
    public static void main(String[] args) {
        new Thread(() -> Phone.sendSms()).start();
        new Thread(() -> Phone.call()).start();
    }
    static class Phone {
        public static synchronized void sendSms() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public static synchronized void call() {
            System.out.println("打电话");
        }
    }
}
  1. 如果分别通过两个对象去调用,输出顺序如何?
public class Demo7 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        
        new Thread(() -> phone1.sendSms()).start();
        new Thread(() -> phone2.call()).start();
    }
    static class Phone {
        public static synchronized void sendSms() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public static synchronized void call() {
            System.out.println("打电话");
        }
    }
}
  1. 发短信和打电话分别来自静态同步方法和非静态同步方法,通过同一对象进行调用,输出顺序如何?
public class Demo7 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> phone.sendSms()).start();
        new Thread(() -> phone.call()).start();
    }
    static class Phone {
        public static synchronized void sendSms() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
}
  1. 发短信和打电话分别来自静态同步方法和非静态同步方法,通过两个对象进行调用,输出顺序如何?
public class Demo7 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> phone.sendSms()).start();
        new Thread(() -> phone2.call()).start();
    }
    static class Phone {
        public static synchronized void sendSms() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
}

通过以上案例的输出情况我们可以得出以下结论:

  1. 普通同步方法,锁的是调用方法的对象,谁先拿到锁谁先执行,同一个对象拿到的是同一把锁。
  2. 静态同步方法,锁的是类对象(Phone.class),谁先拿到锁谁先执行,由于类只加载一次,因此只有一把锁。
  3. 非静态非同步方法,由于不受锁控制,因此不受锁的影响。
  4. 补充一点,如果synchronized锁的某一代码块,锁的是括号里配置的对象。

六、集合类线程不安全

List:在此案例中,我们可以从结果中发现,list的size绝大多数情况下是小于1000的,说明ArrayList在多线程场景下是线程不安全的。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    for (int i = 0; i < 1000; i++) {
        int finalI = i;
        new Thread(() -> {
            list.add(finalI);
            countDownLatch.countDown();
        }).start();
    }
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(list.size());
}

如果想在多线程场景下使用list,并且想要保证线程安全,可以使用以下List:

  • Vector–add,set,remove等方法,被synchronized修饰
  • Collections.synchronizedList–返回的包装器类会对每个修改列表状态的方法添加synchronized关键字
  • CopyOnWriteArrayList–add,set,remove等方法被锁(通常是ReentrantLock)保护;写时复制
public class Demo8 {
    public static void main(String[] args) {
//        List<Integer> list = new Vector<>();
//        List<Integer> list = new CopyOnWriteArrayList<>();
        List<Integer> list = Collections.synchronizedList(new ArrayList<>());
//        List<Integer> list = new ArrayList<>();
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            new Thread(() -> {
                list.add(finalI);
                countDownLatch.countDown();
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

以上案例中,分别采用上述3种List,可以保证每次list的size都是1000,即线程安全。
Set:

HashSet本质上其实是HashMap,set的值本质上是HashMap的key,value为固定值new Object();,这个可以从源码中看出来。

与List类似,Set也有同样的线程安全问题,对应的解决方案如下:

  • CopyOnWriteArraySet
  • Collections.synchronizedSet
public class Demo9 {
    public static void main(String[] args) {
//        HashSet<Integer> set = new HashSet<>();
//        Set<Integer> set = new CopyOnWriteArraySet();
        Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            new Thread(() -> {
                set.add(finalI);
                countDownLatch.countDown();
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(set.size());
    }
}

Map:
同样的问题在Map中也存在。

解决方法也是两种:

  • ConcurrentHashMap–JDK7之前采用分段锁,同一个segment更新时才会发生锁竞争,JDK8后CAS+volatile变量,不需要分段锁,进一步提高了并发效率。
  • Collections.synchronizedMap
public class Demo10 {
    public static void main(String[] args) {
        HashMap<Integer, Integer> hashMap = new HashMap<>();
//        Map<Integer, Integer> hashMap = new ConcurrentHashMap<>();
//        Map<Integer, Integer> hashMap = Collections.synchronizedMap(new HashMap<>());
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            new Thread(() -> {
                hashMap.put(finalI, finalI);
                countDownLatch.countDown();
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(hashMap.size());
    }
}

七、常用辅助类

  1. CountDownLatch,上面的例子也用到过,计数器归零时程序继续向下执行。
public class Demo11 {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                System.out.println(finalI);
                countDownLatch.countDown();
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("close door");
    }
}
  1. CyclicBarrier,值得注意的是,这个类其实是可以循环使用的,这一点从名称中也能看出来,也就是说,每次有7个线程执行完,都会触发一次CyclicBarrier的回调方法。
public class Demo12 {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(7, () -> {
            System.out.println("执行完毕");
        });
        for (int i = 0; i < 70; i++) {
            int finalI = i;
            new Thread(() -> {
                System.out.println(finalI);
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. Semaphore,可以与抢车位进行类比,10辆车抢3个车位,每次有车离开,后边的车才能停。
public class Demo13 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 拿到信号了 ");
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName() + " 时间到,可以释放信号了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}

八、读写锁

写锁是独占锁,读锁是共享锁,读写锁指的是一个资源可以被多个读线程访问,也可以被一个写线程访问,读写是互斥的,读读是共享的。以下示例通过读写锁安全地读写一个map数据。

public class Demo14 {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    readWriteLock.writeLock().lock();
                    map.put(finalI, finalI);
                    System.out.println(Thread.currentThread().getName() + " 写入成功 ");
                } finally {
                    readWriteLock.writeLock().unlock();
                }
            }).start();
        }
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                readWriteLock.readLock().lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " 读取成功 " + map.get(finalI / 2));
                } finally {
                    readWriteLock.readLock().unlock();
                }
            }).start();
        }
    }
}

九、阻塞队列

比较典型的有ArrayBlockingQueue和SynchronousQueue

操作抛异常不抛异常阻塞等待超时等待
添加add()offer()put()offer(E e,long timeout,TimeUnit unit)
移除remove()poll()take()poll(long timeout, TimeUnit unit)
获取首元素element()peek()

队列都有以上四组API。以下以ArrayBlockingQueue做一些案例演示。

public class Demo15 {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.add("a"));
        System.out.println(queue.add("a"));
        System.out.println(queue.add("a"));
//        System.out.println(queue.add("a")); 抛异常,因为超出了队列的容量。
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());
//        System.out.println(queue.remove()); 抛异常,因为已经没有可删除的元素
    }
}
public class Demo16 {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a"));
        System.out.println(queue.offer("a"));
        System.out.println(queue.offer("a"));
        System.out.println(queue.offer("a"));  // 不抛异常,返回false
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll()); // 不抛异常,返回null
    }
}
public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        queue.put("a");
        queue.put("b");
        queue.put("c");
//        queue.put("d"); // 如果队列已满,这里继续添加会阻塞
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
//        System.out.println(queue.take()); // 如果队列已空,这里继续取数据会阻塞
    }
}
public class Demo18 {
    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");
        queue.offer("d", 2, TimeUnit.SECONDS); // 等待2秒后,仍无法添加元素,就退出
        System.out.println("********************************");
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll(2, TimeUnit.SECONDS)); // 等待2秒后,仍无法获取元素,就退出
    }
}

SynchronousQueue:该队列只存储一个元素,必须队列为空时才能添加元素。

该案例中,每次取出一个元素后,才会继续添加元素到队列。

public class Demo19 {
    public static void main(String[] args) {
        SynchronousQueue<String> queue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "  put  ---- 1 ");
                queue.put("1");
                System.out.println(Thread.currentThread().getName() + "  put  ---- 2 ");
                queue.put("2");
                System.out.println(Thread.currentThread().getName() + "  put  ---- 3 ");
                queue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "  take  ---- 1 ");
                queue.take();
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "  take  ---- 2 ");
                queue.take();
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "  take  ---- 3 ");
                queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

十、线程池

重点可以归纳为:三大方式、七大参数、四种拒绝策略

线程的创建和销毁十分浪费资源,如果能有一个池子,保留一定数量的线程,每次提交任务都可以复用线程池中的线程,可以有效提高效率。另外,线程创建过多的时候,容易引起内存溢出或者CPU资源占满的情况。

在工作实践中,都不推荐使用Executors的newFixedThreadExecutor和newSingleThreadPool、newCachedThreadPool去创建线程池。因为前两者允许的请求队列长度为Integer.MAX_VALUE(大约21亿)极易堆积大量请求,导致内存溢出。后者允许创建的线程数量是Integer.MAX_VALUE,同样容易导致内存溢出。

下面分别在案例中看下这三个方法:

public class Demo20 {
    public static void main(String[] args) {
//        ExecutorService pool = Executors.newSingleThreadExecutor(); // 创建单个线程的线程池
//        ExecutorService pool = Executors.newFixedThreadPool(5); // 创建固定线程的线程池
        ExecutorService pool = Executors.newCachedThreadPool(); // 创建可伸缩线程池
        try {
            for (int i = 0; i < 10; i++) {
                pool.execute(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " OK ");
                });
            }
        } finally {
            pool.shutdown();
        }
    }
}

从Java的源码中,我们可以看到,以上三个方法,实际上是通过ThreadPoolExecutor来实现的,让我们一起从源码中看一下它的七个参数。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
                              int maximumPoolSize, //最大核心线程池大小
                              long keepAliveTime, //超时了没有人调用就会释放
                              TimeUnit unit, //超时单位
                              BlockingQueue<Runnable> workQueue, //阻塞队列
                              ThreadFactory threadFactory, //线程工厂:创建线程,一般不用动
                              RejectedExecutionHandler handler)

7大参数的意义依次是:

  1. 核心线程数,是线程池的基本大小。线程池至少会维持这个数量的线程处于运行状态,即使这些线程空闲。
  2. 最大线程数,线程池允许同时存在的最大线程数。如果当前活动线程数小于核心线程数,并且有新的任务提交时,线程池会创建新的线程直至达到核心线程数。如果此时队列已满并且仍有任务提交,则继续创建线程直到达到最大线程数为止。
  3. 非核心线程闲置超时时长。在线程池中的线程数超过核心线程数时,当这些线程空闲时间超过了keepAliveTime,则会被终止,以减少资源消耗。单位由unit参数指定。
  4. unit:keepAliveTime参数的时间单位,如TimeUnit.SECONDS等
  5. workQueue:工作队列,用于保存等待执行的任务。常见类型有LinkedBlockingQueue、ArrayBlockingQueue和SynchronousQueue等。
  6. 线程工厂,用于创建新线程。默认情况下使用Executors.defaultThreadFactory(),也可以自定义。
  7. 拒绝策略,当线程池无法接受新任务(线程池已满,且队列已满),拒绝策略将决定如何处理新提交的任务。Java提供了一些预置的拒绝策略如AbortPolicy(跑出异常)、CallerRunsPolicy(调用者自己执行新提交的任务)、DiscardPolicy(直接丢弃新任务)、DiscardOldestPolicy(抛弃队列中最老的任务,尝试提交新任务)等。
    以下案例执行过程大致如下:线程池可接受的任务是最大线程数+队列长度,即5+3=8。循环中创建了9个线程,并且拒绝策略采用丢弃队列中最老的任务。i=0和1时,可由核心线程执行,i=2/3/4时进入队列中排队,i=5/6/7时,线程池继续创建线程用于执行任务,i=8时触发拒绝策略,丢弃队列中最老的任务,即i=2。输出结果也可以证明这一点,输出结果中没有i=2的情况。
public class Demo21 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );
        try {
            for (int i = 0; i < 9; i++) {
                int finalI = i;
                executor.execute(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "    " + finalI + "  OK  ");
                });
            }
        } finally {
            executor.shutdown();
        }
    }
}

关于如何确定开启线程数量:

  1. CPU核心数:
    1. 对于CPU密集型任务,理想情况下线程数应该接近于可用的处理器核心数。这是因为单个核心一次只能执行一个线程,过高的线程数会造成上下文切换开销,反而降低效率。可以通过 Runtime.getRuntime().availableProcessors() 获取当前系统的处理器核心数。
  2. IO密集型任务:
    1. 对于IO密集型任务,线程数可以根据IO等待时间来调整。由于IO操作期间CPU可能处于空闲状态,因此可以适当增加线程数,使得在等待IO时,其他线程可以继续执行,充分利用CPU资源。一般推荐的线程数可以是 CPU 核心数的1.5倍至2倍。

十一、函数式接口

可以理解为只有一个抽象方法的接口,最典型的就是Runnable,以下代码摘自Java源码:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

Java内置四大核心函数式接口

  • Consumer
  • Supplier
  • Function<T, R>
  • Predicate
    Function接口简单示例:
public class Demo22 {
    public static void main(String[] args) {
        Function<String, String> function = (str) -> str + "---";
        System.out.println(function.apply("999"));
    }
}

Predicate接口简单示例:

public class Demo23 {
    public static void main(String[] args) {
        Predicate<String> predicate = str -> "".equals(str);
        System.out.println(predicate.test(""));
        System.out.println(predicate.test("999"));
    }
}

Consumer接口简单示例:

public class Demo24 {
    public static void main(String[] args) {
        Consumer<String> consumer = str -> System.out.println("000" + str);
        consumer.accept("888");
    }
}

Supplier接口简单示例:

public class Demo25 {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "999999";
        System.out.println(supplier.get());
    }
}

十二、stream流式计算

其实就是把计算交给Stream去做,对集合做筛选、过滤、遍历等操作:

public class Demo26 {
    public static void main(String[] args) {
        Dog dog1 = new Dog(1, 1, 10.5F);
        Dog dog2 = new Dog(2, 4, 15.5F);
        Dog dog3 = new Dog(3, 3, 5.5F);
        Dog dog4 = new Dog(4, 3, 6.5F);
        Dog dog5 = new Dog(5, 2, 8.5F);
        Dog dog6 = new Dog(6, 2, 9.5F);
        Dog dog7 = new Dog(7, 1, 3.5F);
        List<Dog> dogs = Arrays.asList(dog1, dog2, dog3, dog4, dog5, dog6, dog7);
        dogs.stream()
                .filter(dog -> dog.getId() % 2 == 0)
                .filter(dog -> dog.getAge() > 2)
                .map(dog -> Math.round(dog.getWeight()))
                .sorted()
                .limit(2)
                .forEach(System.out::println);
    }
}
class Dog {
    private int id;
    private int age;
    private float weight;
    public Dog(int id, int age, float weight) {
        this.id = id;
        this.age = age;
        this.weight = weight;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public float getWeight() {
        return weight;
    }
    public void setWeight(float weight) {
        this.weight = weight;
    }
}

十三、ForkJoin

简而言之就是把大量任务递归拆分成小任务分别执行,最后再收集汇总,计算最终结果。

接下来以一个加和计算来示例ForkJoin的操作:

首先创建一个ForkJoinDemo类,进行fork join计算

public class ForkJoinDemo extends RecursiveTask<Long> {
    private Long start;
    private Long end;
    private Long temp = 10000L;
    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected Long compute() {
        if ((end - start) <= temp) {
            long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            Long middle = (start + end) / 2;
            ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(start, middle);
            forkJoinDemo1.fork();
            ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle + 1, end);
            forkJoinDemo2.fork();
            return forkJoinDemo1.join() + forkJoinDemo2.join();
        }
    }
}

然后创建测试类进行计算,并分别对比传统方式和stream parallel方式与fork join方式的效率。实际开发中,一定要具体问题具体分析,采用最优的方式。

 public class Demo27 {
    private static final long START = 0L;
    private static final long END = 10_0000_0000L;
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1();
        test2();
        test3();
    }
    /**
     * 传统方法
     */
    public static void test1() {
        long sum = 0L;
        long start = System.currentTimeMillis();
        for (long i = START; i <= END; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("traditional result: " + sum + " cost time: " + (end - start));
    }
    /**
     * ForkJoin方式
     */
    public static void test2() throws ExecutionException, InterruptedException {
        long sum;
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinDemo forkJoinDemo = new ForkJoinDemo(START, END);
        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
        sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("fork join result: " + sum + " cost time: " + (end - start));
    }
    /**
     * 并行流Stream计算方式
     */
    public static void test3() {
        long sum;
        long start = System.currentTimeMillis();
        sum = LongStream.rangeClosed(START, END).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("stream result: " + sum + " cost time: " + (end - start));
    }
}

十四、异步回调

没有返回值的异步回调:

public class Demo28 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
            System.out.println(Thread.currentThread().getName() + " runAsync ");
        });
        System.out.println("-----------------------------");
        System.out.println(completableFuture.get());
    }
}

有返回值的异步回调:

public class Demo29 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " supplyAsync ");
            int i = 10 / 0;
            return 1024;
        });
        System.out.println(completableFuture.whenComplete((t, u) -> {
            System.out.println("t=> " + t); // 正常返回信息
            System.out.println("u=> " + u); // 错误信息
        }).exceptionally((e -> {
            System.out.println(e.getMessage());
            return 234;
        })).get());
    }
}

十五、JMM Java内存模型

Java内存模型(Java Memory Model, JMM)中定义的8种操作是为了保证多线程环境下内存操作的原子性和可见性,进而确保并发程序的正确性。这些操作主要围绕主内存(Main Memory)和工作内存(Working Memory)之间的数据同步展开。以下是这8种操作的概述:

1. lock(锁定)

  • 主要作用于主内存的变量,将一个变量标志为线程独占状态,通常发生在对共享变量进行写操作前。

  • 在硬件层面,对应的是对内存区域加锁,使得在锁释放前,其他线程无法对该变量进行读写。

2. unlock(解锁)

  • 解除对主内存中某个变量的锁定状态,使得其他线程可以再次对其进行锁定。

  • 在释放锁之后,先前对变量的修改对于其他线程变得可见。

3. read(读取)

  • 从主内存中读取一个变量的值到当前线程的工作内存中。

  • 该操作确保了变量的值从共享内存传输到线程私有的本地内存。

4. load(加载)

  • 将读取到的主内存中的变量值放入工作内存的变量副本中。

  • 这是一个工作内存的操作,确保了线程能够基于最新的值进行计算。

5. use(使用)

  • 在工作内存中使用读取到的变量副本进行计算。

  • 虽然不是所有参考资料都明确列出“use”,但在规范中它代表了从工作内存中读取变量并使用的操作。

6. assign(赋值)

  • 修改工作内存中变量副本的值。

  • 这是对工作内存中变量的修改操作。

7. store(存储)

  • 将工作内存中的变量值回写到主内存中。

  • 当线程完成对变量的修改后,需要将修改的结果同步回主内存。

8. write(写入)

  • 完成实际的主内存写操作,使得其他线程能够观察到该变量的最新值。

  • write操作是store操作的具体实现,确保了变量在主内存中的更新。

这些操作共同构成了Java内存模型中的内存间交互协议,它们之间遵循一定的规则和顺序,确保了多线程环境下的内存一致性。同时,Java编译器和JVM会在适当的时候插入必要的内存屏障指令,以保证这些操作的正确排序和内存可见性。值得注意的是,这些概念相对底层,开发者通常不需要直接处理这些细节,而是通过使用Java标准库提供的并发工具(如synchronized、volatile关键字、Lock接口等)来间接实现内存模型的要求。

Java内存模型中volatile是一个非常重要的关键字。

  • 保证内存可见性;
public class Demo30 {
    private static volatile int num = 0; // 当不使用volatile修饰时,线程无法知晓num值的变化,线程进入死循环
                                         // 当使用volatile修饰时,线程对num的变化可见,跳出循环,输出“end”
    public static void main(String[] args) {
        new Thread(() -> {
            while(num == 0) {
            }
            System.out.println("end");
        }).start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
        System.out.println(num);
    }
}
  • 防止指令重排序;
    这种情况实际上是非常难以复现的,最典型的就是在DCL(Double Check Lock)单例模式中,如果不使用volatile修饰变量,可能导致获取到的单例是中间状态,而导致程序出错。后边的内容中会讲到。

  • 无法保证操作的原子性。

public class Demo31 {
    private static volatile int num = 0;
    // 执行结果绝大多数num小于2万,表明volatile无法保证操作的原子性。
    // 可以在该方法增加synchronized解决该问题
    // 或者把num变量改为AtomicInteger
    public static void add() {
        num++;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield(); // 如果除main和gc线程,还有其他线程在执行,就让其他线程优先执行。
        }
        System.out.println(Thread.currentThread().getName() + "  --- " + num);
    }
}

十六、单例模式

  1. 饿汉式–最简单有效,唯一的缺陷在于当对象占用空间较大时,可能浪费内存空间。
public class Demo33 {
    private static final Demo33 DEMO33 = new Demo33(); // 在类加载时就创建好对象。
    // 私有化构造器,防止从外部创建新对象。
    private Demo33() {
    }
    // 通过静态方法获取对象
    public static Demo33 getInstance() {
        return DEMO33;
    }
}
  1. 懒汉式
public class Demo34 {
    private volatile static Demo34 demo34;
    private static boolean flag = false;
    private Demo34() {
        synchronized (Demo34.class) {
            if (!flag) {
                flag = true;
            } else {
                throw new RuntimeException("Don't use reflection to get instance.");
            }
        }
    }
    public static Demo34 getInstance() {
        if (null == demo34) {
            synchronized (Demo34.class) {
                if (null == demo34) {
                    demo34 = new Demo34();
                }
            }
        }
        return demo34;
    }
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        for (int i = 0; i < 10; i++) {
//            new Thread(() -> {
//                System.out.println(Demo34.getInstance());
//            }).start();
//        }
        System.out.println("***********************************");
        Demo34 instance = Demo34.getInstance();
        System.out.println(instance);
        Field flag = Demo34.class.getDeclaredField("flag");
        flag.setAccessible(true);
        Constructor<Demo34> declaredConstructor = Demo34.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        flag.set(demo34, false);
        Demo34 demo34 = declaredConstructor.newInstance();
        System.out.println(demo34);
        flag.set(demo34, false);
        Demo34 newInstance = declaredConstructor.newInstance();
        System.out.println(newInstance);
        System.out.println(demo34 == newInstance);
    }
}
  1. 静态内部类
public class Demo35 {
    private Demo35() {
        
    }
    
    public static class InnerClass {
        private static final Demo35 DEMO_35 = new Demo35();
    }
    
    public static Demo35 getInstance() {
        return InnerClass.DEMO_35;
    }
}
  1. 枚举类–最大的优势就是可以防止通过反射创建新对象。
public enum Demo36 {
    INSTANCE;
    
    public Demo36 getInstance() {
        return INSTANCE;
    }
}

十七、深入理解CAS

Java中的CAS(Compare and Swap)本质上是一种原子操作指令,它可以用来实现无锁的数据同步机制。在硬件层面,许多现代处理器提供了CAS指令,它可以在不锁定总线或者其他全局资源的情况下,比较并更新内存中的数据。如果内存位置的值与期望值相符,则更新该位置的值;若不符,则不做任何修改。

在Java中,CAS操作通过JNI(Java Native Interface)调用底层操作系统或者硬件提供的原子指令实现。具体在Java API层面,java.util.concurrent.atomic包下的原子类(如AtomicInteger、AtomicLong等)广泛使用了CAS来保证多线程环境下的原子性操作。

例如,AtomicInteger类中的compareAndSet方法就实现了CAS操作,它的逻辑大致如下:

public final boolean compareAndSet(int expect, int update) {
    // native修饰的方法表明它是通过JNI调用C/C++编写的本地方法实现的
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

这里unsafesun.misc.Unsafe类的对象,它提供了直接操作内存的底层能力,包括直接调用CAS指令。compareAndSwapInt方法就是利用CPU级别的CAS指令完成的原子操作。
总结来说,Java中的CAS是一种基于硬件支持的并发原语,它允许在不使用互斥锁的前提下,以原子方式读取、比较并有条件地更新内存中的某个数值,从而极大地提升了并发环境下程序的性能,减少了锁竞争带来的开销。然而,尽管CAS在很多场景下非常高效,但也存在ABA问题以及长时间循环重试可能导致性能下降等问题,在实际应用中需要结合具体业务场景合理选择并发控制策略。

public class Demo37 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2024);
        System.out.println(atomicInteger.compareAndSet(2024, 2025));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2024, 2025));
        System.out.println(atomicInteger.get());
    }
}
// 输出如下
// true
// 2025
// false
// 2025

ABA问题大致如下例所示:

public class Demo38 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2024);
        System.out.println(atomicInteger.compareAndSet(2024, 2025));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2025, 2024));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2024, 8888));
        System.out.println(atomicInteger.get());
    }
}

十八、原子引用

通过增加stamp记录修改历史,即便出现ABA问题,也可以通过stamp的值判断。

public class Demo39 {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);
        new Thread(() -> {
            int stamp = reference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 1 ->  " + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + reference.compareAndSet(
                    1,
                    2,
                    reference.getStamp(),
                    reference.getStamp() + 1));
            System.out.println(Thread.currentThread().getName() + " 2 ->  " + reference.getStamp());
            System.out.println(Thread.currentThread().getName() + reference.compareAndSet(
                    2,
                    1,
                    reference.getStamp(),
                    reference.getStamp() + 1));
            System.out.println(Thread.currentThread().getName() + " 3 ->  " + reference.getStamp());
        }).start();
        new Thread(() -> {
            int stamp = reference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 1 ->  " + stamp);
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + reference.compareAndSet(
                    1,
                    6,
                    reference.getStamp(),
                    reference.getStamp() + 1));
            System.out.println(Thread.currentThread().getName() + " 2 ->  " + reference.getStamp());
        }).start();
        try {
            TimeUnit.SECONDS.sleep(8);
            System.out.println("---------------------------");
            System.out.println(reference.getReference());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

十九、Java中的锁

  1. 公平锁和非公平锁
    Java中最典型的两种锁,就是synchronized和ReentrantLock。

synchronized关键字实现的锁是隐式的,且默认是非公平锁。也就是说,当多个线程等待同一个锁时,被唤醒的线程不一定是等待时间最长的线程,操作系统可以自由选择哪个线程获取锁。

ReentrantLock类提供了公平锁和非公平锁两种选择。如果不指定构造函数参数,默认创建的是非公平锁。但是可以通过构造函数创建公平锁,例如new ReentrantLock(true),这里的参数true表示创建公平锁。

以上二者默认情况下表现为悲观锁,即获取数据前会先加锁。java.util.concurrent.atomic包下的原子类,使用CAS实现乐观锁机制。

以下摘自ReentrantLock源码中的两个构造函数:

public ReentrantLock() {
  sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync : new NonfairSync();
}
  1. 自旋锁
    代码分析:
  • SpinLock 类中定义了一个 AtomicReference<Thread> 类型的原子引用变量 atomicReference,用于存储当前持有锁的线程。
  • myLock() 方法中,首先获取当前线程,然后进入一个 while 循环。循环体为空,仅仅判断 atomicReference 是否可以被设置为当前线程。如果 atomicReference 当前为 null,则通过 compareAndSet(null, thread) 方法将其设置为当前线程。compareAndSet() 是一个原子操作,它会比较当前值是否为 null,如果是则将值设置为 thread,否则不做任何改变并返回 false
  • 当第一个线程(例如 “T1”)调用 myLock() 时,它会进入 while 循环并尝试设置 atomicReference 为自身。由于此时 atomicReference 初始值为 null,所以第一次尝试就能成功,跳出循环,线程获得锁。
  • 若第二个线程(例如 “T2”)在第一个线程还未释放锁时调用 myLock(),它也会进入 while 循环。但由于此时 atomicReference 已经被设置为 “T1”,compareAndSet(null, thread) 将一直返回 false,于是 “T2” 线程会持续自旋,不断地重复检查 atomicReference 是否仍被 “T1” 线程持有。
  • 当 “T1” 线程执行完毕调用 myUnLock() 方法时,它会将 atomicReference 设置回 null。这时,如果 “T2” 线程还在自旋,下一次循环将会发现 atomicReference 变为 null,然后成功设置为 “T2”,从而 “T2” 线程得以获取锁。
    输出结果为:

T1 ==> myLock

T2 ==> myLock

T1 ==> myUnLock

T2 ==> myUnLock

public class Demo40 {
    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        new Thread(() -> {
            spinLock.myLock();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.myUnLock();
            }
        }, "T1").start();
        new Thread(() -> {
            spinLock.myLock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.myUnLock();
            }
        }, "T2").start();
    }
    static class SpinLock {
        AtomicReference<Thread> atomicReference = new AtomicReference<>();
        public void myLock() {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + " ==> myLock ");
            while (!atomicReference.compareAndSet(null, thread)) {
            }
        }
        public void myUnLock() {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + " ==> myUnLock ");
            atomicReference.compareAndSet(thread, null);
        }
    }
}
  1. 死锁
    在Java中,死锁(Deadlock)通常出现在多个线程互相等待对方释放资源,从而导致所有线程都无法继续执行的情况。死锁的发生需要四个必要条件(亦称死锁四要素):

    1. 互斥条件(Mutual Exclusion):至少有一个资源在任何时刻只能被一个线程占有。例如,一个锁只能被一个线程持有。
    2. 持有并等待条件(Hold and Wait):已经持有至少一个资源的线程正在等待获取其他资源,而它又不愿意释放已经持有的资源。
    3. 不可剥夺条件(No Preemption):资源一旦被线程占有,除非该线程主动释放,否则其他线程无法强行剥夺。
    4. 循环等待条件(Circular Wait):存在一个线程等待队列,形成一个线程—资源的循环等待链。例如,线程A持有资源1并等待资源2,线程B持有资源2并等待资源1。
      一个典型的Java死锁示例是两个线程分别持有了一个资源,并尝试获取对方的资源,但都不愿意释放已经持有的资源,这就形成了死锁:

实际开发中可以用以下方法避免或预防死锁出现:

  1. 预防死锁:
    1. 资源有序分配法:为系统中的所有资源赋予一个全局唯一的编号,并要求所有线程按编号顺序申请资源。这样可以打破循环等待条件,例如在上述死锁示例中,可以规定线程先申请编号较小的资源。
    2. 一次性申请所有资源:让线程一次性申请它所需要的全部资源,避免持有并等待条件的产生。
  2. 避免死锁:
    1. 设置超时:在申请资源时设置一个合理的超时时间,当超过这个时间还没有获取到资源时,线程主动放弃已获得的部分资源,然后重新尝试获取资源,打破死锁。
    2. 资源预先分配:尽可能地预先为线程分配所需的全部资源,或者提前规划好资源分配方案,确保不会形成死锁。

文章的最后,如果大家想看视频版本的讲解,可以关注以下B站账号。

一、JUC开篇介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值