并发问题之Java多线程

创建线程方式

第一种:继承 Thread

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("通过集成 Thread 类实现线程");
    }

}
// 如何使用
new MyThread().start()

第二种:实现 Runnable

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("通过实现 Runnable 方式实现线程");
    }

}

// 使用
// 1、创建MyRunnable实例
MyRunnable runnable = new MyRunnable();
//2.创建Thread对象
//3.将MyRunnable放入Thread实例中
Thread thread = new Thread(runnable);
//4.通过线程对象操作线程(运行、停止)
thread.start();

如代码所示,这种方法其实是定义一个线程执行的任务(run 方法里面的逻辑)并没有创建线程。它首先通过 MyRunnable 类实现 Runnable 接口,然后重写 run () 方法,之后还要把这个实现了 run () 方法的实例传到 Thread 类中才可以实现多线程

第三种:线程池创建线程

ExecutorService service = Executors.newFixedThreadPool(10);

点进去 newFixedThreadPool 源码,在 IDEA 中调试,可以发现它的调用链是这样的:

Executors.newFixedThreadPool(10) --> new ThreadPoolExecutor(一堆参数) --> Executors.defaultThreadFactory() 

可以发现最终还是调用了 Executors.defaultThreadFactory() 方法,而这个方法的源码是这样的:

static class DefaultThreadFactory implements ThreadFactory {
    // 线程池序号
    static final AtomicInteger poolNumber = new AtomicInteger(1);
    // 线程序号
    final AtomicInteger threadNumber = new AtomicInteger(1);
    // 线程组
    final ThreadGroup group;
    // 线程池前缀
    final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                poolNumber.getAndIncrement() +
                "-thread-";
    }

    /**
     * 重点方法
     * @param r
     * @return
     */
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
        // 是否是守护线程
        if (t.isDaemon()) {
            t.setDaemon(false);
        }
        // 设置优先级
        if (t.getPriority() != Thread.NORM_PRIORITY) {
            t.setPriority(Thread.NORM_PRIORITY);
        }
        return t;
    }
}

如上源码所示:**线程池创建线程本质上是默认通过 DefaultThreadFactory 线程工厂来创建的。**它可以设置线程的一些属性,比如:是否守护线程、优先级、线程名、等等。

但无论怎么设置,最终都是需要通过 new Thread () 创建线程的。

第四种:Callable 创建

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }

    // 使用方法
    // 1、创建线程池
    ExecutorService service = Executors.newFixedThreadPool(10);
    // 2、提交任务,并用 Future提交返回结果
    Future< Integer > future = service.submit(new MyCallable());

}

Callable 与 Runnable 名字还有点像,区别在于 Runnable 是无返回值的。它们的本质都是定义线程要做的任务(call 或 run 方法里面的逻辑),而不是说他们本身就是线程。但无论有无返回值,它们都是需要被线程执行。

如代码所示,它们可以提交到线程池执行,通过 sumbit 方法提交。这时就参考方式三,由线程工厂负责创建线程。当然,还有其他方法执行 Callable 任务。但是不管怎么说,它还是离不开实现 Runnable 接口和继承 Thread 类这两种方式

注意到 Thread 类中有一个 run 方法:

private Runnable target;

@Override
public void run() {
  if (target != null) {
    target.run();
  }
}

实现 Runnable 的方式中,启动线程还是需要调用 start 方法(因为是 Native 方法我们看不到具体逻辑),但是线程要执行任务必须还是要调用 run 方法

我们看代码,run 方法非常简单。它判断 target 不为 null 就直接执行 target 的 run 方法。而 target 正是我们实现的 Runnable ,使用 Runnable 接口实现线程时传给 Thread 类的对象

在看继承 Thread 方式,它调用 thread.start (),最终调用的还是 run 方法(run () 里面是任务)。只不过这个 run () 是我们已经重写的 run () 而不是上面 Runnable (target) 的 run ()。

看到这里可算明白了,事实上创建线程本质只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式,不同的只是 run 方法(执行内容)的实现方式

本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种写法:

  • 实现 Runnable 接口从而实现 run () 的方式
  • 继承 Thread 类重写 run () 方法的方式

然后把我们想要执行的代码传入,让线程去执行。在此基础上,如果我们还想有更多实现线程的方式,比如线程池、Callable 以及 Timer 定时器,只需要在此基础上进行封装即可

哪种写法好?

答案是:Runnable 写法

理由:
  • 易于扩展:Java 是单继承。如果使用继承 Thread 的写法。将不利于后续扩展。

  • 解耦:用 Runnable 负责定义 run () 方法(执行内容)。这种情况下,它与 Thread 实现了解耦。Thread 负责线程的启动以及相关属性设置。

  • 性能:在一些情况下可以提高性能。比如:线程执行的内容很简单,就是打印个日志。如果使用 Thread 实现,那它会从线程创建到销毁都要走一遍,需要多次执行时,还需要多次走这重复的流程,内存开销非常大。但是我们使用 Runnable 可以把它扔到线程池里面,用固定的线程执行,可以提高效率。

线程的状态

查看 Thread 类的源码时,内部定义了这样一个枚举类。这个枚举类定义的就是线程的状态:

public enum State {
      
        NEW, //(新创建)

        RUNNABLE, //(可运行)

        BLOCKED, //(被阻塞)

        WAITING, //(等待)

        TIMED_WAITING, //(计时等待)

        TERMINATED; //(被终止)
}

线程在任何时刻只可能处于以上 6 种的其中 1 种状态,我们可以调用 getState () 查看线程的状态

NEW (新创建)

线程被 NEW 出来,但还没调用 start 方法时,就处于这种状态。一旦调用了 start 方法也就进入了 RUNNABLE 状态。

RUNNABLE(可运行)

处于 RUNNABLE 的线程,比较特殊。它还分两种状态:Running 和 Ready。

也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源

因此,我们可以推断出:

一个处于 Runnable 状态的线程,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,则该线程暂时不运行。但是,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务

也就是说:处于 Runnable 状态的线程并不一定在运行。这点在面试中常问,小伙伴们要注意了。

Blocked(被阻塞)

  • Runnable --> Blocked :进入 synchronized 关键字保护的代码,但是没有获取到 monitor 锁

  • Blocked --> Runnable :线程获取到monitor锁

Waiting (等待)

  • Runnable --> Waiting ,有三种可能:

    1. 没有设置 Timeout 参数的 Object.wait () 方法。

    2. 没有设置 Timeout 参数的 Thread.join () 方法。

    3. LockSupport.park () 方法。

    上面我们提到,线程进入 Blocked 状态只可能是:进入 synchronized 保护的代码,但是没获取到 monitor 锁。

    然而Java 中还有很多锁,比如:ReentrantLock。线程在获取这种锁时,没有抢到就会进入 Waiting 状态,因为它本质上是调用了 LockSupport.park () 方法。

    同样的,调用 Object.wait () 或 Thread.join () 也会让线程进入等待状态

  • Waiting --> Runnable :

    1. 执行了 LockSupport.unpark ()
    2. join 的线程运行结束
    3. 线程被中断
  • Waiting --> Blocked : 当其他线程调用 notify () 或 notifyAll () 来唤醒处于 Waiting 的线程,它会直接进入 Blocked 状态。

    其他线程能调用 notify () 或 notifyAll () 试图唤醒 Waiting 状态线程,说明必须持有同一个 monitor 锁,也就是说处于 Waiting 的线程被唤醒后并不能马上抢到 monitor 锁,所以它必须先进入 Blocked 状态。而唤醒它的线程执行完毕释放锁后,它能抢到锁就从 Blocked 进入 Runnable 状态

  • Blocked 与 Waiting 的区别:

    • Blocked 在等待其他线程释放 monitor 锁

    • Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify ()/notifyAll ()

Timed Waiting(计时等待)

与 Waiting 状态的区别在于:有没有时间限制,Timed Waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒

Terminated(被终止)

  • 进入终止状态
    1. 任务执行完毕,线程正常退出。
    2. 出现一个没有捕获的异常(比如直接调用 interrupt () 方法)。

总结

Thread_Status

  • 线程的状态需要按照箭头方向走:比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
  • 线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可以相互转换

Thread源码解析

1、线程名

//1.构造方法中调用了 init 方法,init 方法内部调用了 nextThreadNum 方法。
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

//2. nextThreadNum 是一个线程安全的方法,同一时间只可能有一个线程修改。
//threadInitNumber 是一个静态变量,它可以被类的所有对象访问。所以,每个线程在创建时直接 +1 作为子线程后缀。
private static int threadInitNumber; 
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

//3. 注意到最后有 this.name = name 赋值给 volatile 变量的 name,默认就是用 Thread-x 作为子线程名。
private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    // 名称赋值
    this.name = name;
    // 省略代码
}

//4.最终 getName 方法拿到的就是这个 volatile 变量 name 的值。
private volatile String name;
public final String getName() {
    return name;
}

2、线程优先级

//1. 线程可以拥有的最小优先级
public final static int MIN_PRIORITY = 1;

//2. 线程默认优先级
public final static int NORM_PRIORITY = 5;

//3. 线程可以拥有的最大优先级
public final static int MAX_PRIORITY = 10

线程的优先级越高,抢占 CPU 时间片(也就是执行权)的概率越大,但并不意味着优先级高的线程就一定先执行。

Thread 类中,通过setPriority方法设置优先级:

public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    // 先验证优先级的合理性,不能大于 10,也不能小于 1
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        // 优先级如果超过线程组的最高优先级,则把优先级设置为线程组的最高优先级
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        // native 方法
        setPriority0(priority = newPriority);
    }
}

3、守护线程

守护线程是低优先级的线程,专门为其他线程服务的,其他线程执行完了,它也就挂了。

java 的垃圾回收线程就是典型的守护线程

它有两个特点:

  • 当别的非守护线程执行完了,虚拟机就会退出,守护线程也就会被停止掉。
  • 守护线程作为一个服务线程,没有服务对象就没有必要继续运行了

通常不会使用守护线程来访问资源(比如修改数据、进行 I/O 操作等等);反之,守护线程经常被用来执行一些后台任务,并且希望在程序退出时线程能够自动关闭。

Thread 类中,通过setDaemon方法设置守护线程:

public final void setDaemon(boolean on) {
    // 判断是否有权限
    checkAccess();
    // 判断是否活跃
    if (isAlive()) {
         //必须在线程启动之前就把目标线程设置为守护线程,否则报错。
        throw new IllegalThreadStateException();
    }
    daemon = on;
}

4、start () 和 run () 的区别?

start () 方法属于 Thread 自身的方法,并且使用了 synchronized 来保证线程安全

public synchronized void start() {
        // 1、状态验证,不等于 NEW 的状态会抛出异常
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        // 2、通知线程组,此线程即将启动
        group.add(this);
    
        boolean started = false;
        try {
            start0(); //native方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                // 3、不处理任何异常,如果 start0 抛出异常,则它将被传递到调用堆栈上
            }
        }
}

run () 方法继承自 Runnable 接口,必须由调用类重写。重写的run()方法其实就是线程要执行的业务方法。

public class Thread implements Runnable {
    private Runnable target;
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • run 方法里面定义的是线程执行的任务逻辑,直接调用时程序依然在主线程中运行
  • start 方法启动线程,使线程由 NEW 状态转为 RUNNABLE,然后再由 jvm 去调用该线程的 run () 方法去执行任务
  • start 方法不能被多次调用,否则会抛出 java.lang.IllegalStateException;而 run () 方法可以进行多次调用,因为它是个普通方法

5、sleep()

  • 睡眠指定的毫秒数,且在这过程中不释放锁
  • 如果参数非法,报 IllegalArgumentException
  • 睡眠状态下可以响应中断信号,并抛出 InterruptedException
  • 调用 sleep 方法,即会从 RUNNABLE 状态进入 Timed Waiting(计时等待)状态

6、如何正确停止线程?

线程在不同的状态下遇到中断会产生不同的响应,有的会抛出异常,有的没有变化,有的则会结束线程。

stop 方法会强制终止线程,但没有给线程足够的时间来处理在线程停止前保存数据的逻辑,会导致数据完整性的问题。它已经被 Java 设置为 @Deprecated 过时方法了。

interrupt()

interrupt()只会发出一个信号告诉线程应该要结束了;但马上停止,还是过一段时间后才停止,甚至选择不停止都是由线程根据自己的业务逻辑来决定的

public void interrupt() {
    // 检查是否有权限
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        // 判断是不是阻塞状态的线程调用,比如刚调用 sleep()
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            // 如果是,抛异常同时推出阻塞。将中断标志位改为 false
            b.interrupt(this);
            return;
        }
    }
    // 否则,顺利改变标志位
    interrupt0();
}
  • 1、只能自己中断自己,不然会抛出 SecurityException
  • 2、如果线程调用 wait、sleep、join 等方法,进入了阻塞,会造成调用中断无效,抛 InterruptedException 异常
  • 3、以上情况不发生时,才会改变线程的中断状态
  • 4、中断已经挂了的线程是无效的

7、yield()

  • 当前线程调用 yield () 会让出 CPU 使用权,给别的线程执行,但是不确保真正让出。谁先抢到 CPU 谁执行。
  • 当前线程调用 yield () 方法,会将状态从 RUNNABLE 转换为 WAITING。

8、join()

调用 join 方法,会等待该线程执行完毕后才执行别的线程

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
    // 超时时间不能小于 0
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    // 等于 0 表示无限等待,直到线程执行完为之
    if (millis == 0) {
        // 判断子线程 (其他线程) 为活跃线程,则一直等待
        while (isAlive()) {
            wait(0);
        }
    } else {
        // 循环判断
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
  • 可以看出 join () 方法底层还是通过 wait () 方法来实现的。
  • 当前线程终止,会调用当前实例的 notifyAll 方法唤醒其他线程。
  • 调用 join 方法,会使当前线程从 RUNNABLE 状态转至 WAITING 状态。

总结

Thread 类中主要有 start、run、sleep、yield、join、interrupt 等方法。

其中 start、sleep、yield、join、interrupt(改变 sleep 状态)是会改变线程状态的。

最后我们再来看这张线程状态切换图。

Change_Status

Synchronized

什么是synchronized?

synchronized 是 Java 的关键字,是一个互斥锁,能够将代码块 (方法) 锁起来。同一时间只能有一个线程进入被锁住的代码块。

synchronized 通过监视器(Monitor)实现锁。java 一切皆对象,每个对象都有一个监视器(锁标记),而 synchronized 就是使用对象的监视器来将代码块 (方法) 锁定的

为什么用synchronized?

加锁的原因是为了线程安全,而线程安全最重要就是保证原子性和可见性

  • 同一时间只能有一个线程执行,从而保证原子性。
  • 使用监视器实现对变量的同步操作,保证了其他线程对变量的可见性。
怎么用synchronized?
  1. 修饰普通同步方法:锁是当前实例对象:

    public class Test {
    
        // 修饰普通同步方法,而普通方法属于对象所有
        // 锁的是 Test 的实例对象,因此多个实例对象调用不会阻塞,
        public synchronized void testCommon() {
            int i = 0;
            while (i++ < 10) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
                System.out.println("Common function is locked " + i);
            }
        }
    
        // 修饰静态同步方法,静态方法属于类(粒度比普通方法大)
        // 锁是类的锁(类的字节码文件对象:Test.class)
        public static synchronized void testStatic() {
            int i = 0;
            while (i++ < 10) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
                System.out.println("Static function is locked " + i);
            }
        }
    
        public static void main(String[] args) {
            Test test = new Test();
            new Thread(test::testCommon,"test1").start();
            new Thread(Test::testStatic,"test2").start();
            //synchronized 修饰静态方法获取的是类锁 (类的字节码文件对象),synchronized 修饰普通方法获取的是对象锁。
            // 也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的
        }
    }
    
    

    另外,还有一种方法是用synchronized修饰同步代码块,锁的是括号内的对象。

     synchronized (this) {
                // doSomething
     }
    

    这里的synchronized跟普通方法一样锁的都是当前实例对象。对于同步代码块,Java 还支持它持有任意对象的锁,比如第二种的 object 。那么这两者有何区别?这两者并无本质区别,但第二种无缘无故定义一个对象,为了代码的可读性,更建议用第一种

Synchronized 的原理?

要进一步深入了解 synchronized 就必须了解 monitor 对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。可以参考这篇博客:https://blog.csdn.net/chenssy/article/details/54883355

Lock

Lock 接口是 Java 5 引入的,最常见的实现类是 ReentrantLock、ReadLock、WriteLock,可以起到 “锁” 的作用。

Lock 和 synchronized 是 java 中两种最常见的锁。“锁” 是一种工具。它用于控制对共享资源的访问。

需要注意的是 Lock 设计的初衷并不是为了取代 synchronized ,而是一种升级。当 synchronized 不合适或者不能满足需求时(后面会说两者区别)用Lock 代替。

一般情况下,Lock 同一时间只允许一个线程来访问这个共享资源。但是也有特殊的时候允许并发访问。比如读写锁里面的读锁*(这就是其中一个 synchronized 不能满足的场景)*

lock

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
     /**
     * lock 加锁主要有 4 个方法:lock、lockInterruptibly、tryLock、tryLock 
     * 解锁只有一个 unlock 方法
     * 此外,还有一个线程间通信的条件(Condition)
     */
}
lock()

在4种加锁方法种,lock 是最基础的。Lock 获取锁和释放锁都是显式的,不像 synchronized 是隐式的。所以 synchronized 会在抛异常时自动释放锁,而 Lock 只能是主动释放,加解锁都必须有显式的代码控制

Lock lock = ...;
// 代码显式加锁
lock.lock();
try {
    //获取到了被本锁保护的资源,处理任务
    //捕获异常
} finally {
    //代码显式释放锁
    lock.unlock(); 
}

这种 lock 的写法才是最安全的,先获取 lock,然后在 try 中操作资源,最后 finally 中释放锁,以保证绝对释放(这一步非常重要,它防止代码走不到这里,导致跳过了 unlock () 语句,使得这个锁永远不能被释放)。

此外,lock () 方法有个缺点就是它不能被中断,一旦陷入死锁,lock () 就会陷入永久等待

所以,一般来说我们会用 tryLock 来代替 lock。

tryLock()

tryLock 顾名思义是尝试获取锁的意思,返回值是 boolean,获取成功返回 true,获取失败返回 false

Lock lock = ...;
//使用 if 判断是否获取锁,成功获取则去操作共享资源,失败则去干别的事(比如几秒之后重试或跳过此任务),最后记得释放锁。
if (lock.tryLock()) { 
   
    try {
        //操作资源
    } finally {
        //释放锁
        lock.unlock();
    }
} else {
    //如果不能获取锁,则做其他事情
}

tryLock 解决死锁问题

想象这样一个场景:比如有两个线程同时调用以下这个方法,传入的 lock1 和 lock2 恰好是相反的。如果第一个线程获取了 lock1,第二个线程获取了 lock2,两个线程都需要获取对方的锁才能工作。如果用 lock 这就很容易陷入死锁,原因前面也说了。

这个时候 tryLock 就发挥作用了:其中一个线程尝试获取锁 lock1,获取不到,则去隔段时间重试(这样做的目的在于等另一个获取到锁的线程在这段时间内完成任务,释放锁)。获取到了,则继续获取 lock2 ,获取到就操作共享资源,获取不到则释放 lock1,继续进入重试

public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
    while (true) {
        if (lock1.tryLock()) {
            try {
                if (lock2.tryLock()) {
                    try {
                        System.out.println("获取到了两把锁,完成业务逻辑");
                        return;
                    } finally {
                        lock2.unlock();
                    }
                }
            } finally {
                lock1.unlock();
            }
        } else {
            Thread.sleep(new Random().nextInt(1000));
        }
    }
}
tryLock(long time, TimeUnit unit)

这个方法是 tryLock 的重载,区别在于 tryLock (long time, TimeUnit unit) 方法会有一个超时时间。在拿不到锁时会等待指定的时间,在指定时间内获取不到锁返回 false;获取到锁或者等待期间内获取到锁,返回 true。

此外,超时之后,它将放弃主动获取锁。它还可以响应中断,抛出 InterruptException,避免死锁的产生

lockInterruptibly

lockInterruptibly 去获取锁,获取到了马上返回 true。它非常执拗,如果获取不到锁就会一直尝试获取直到获取到为止,除非当前线程在获取锁期间被中断。可以把它理解为不限时的 tryLock (long time, TimeUnit unit)。

public void lockInterruptibly() throws InterruptException {
    lock.lockInterruptibly();
    try {
        System.out.println("操作资源");
    } finally {
        lock.unlock();
    }
}
unlock

unlock 顾名思义就是释放锁。就 ReentrantLock 而言,调用 unlock 方法时,内部会把锁的 “被持有计数器” 减 1,减到 0 代表当前线程已经完全释放这把锁

newCondition()

Condition有两个主要的方法 await 和 signal 分别用于阻塞线程和唤醒线程。对应于 Object 的 wait 和 notify。

(用法见生产者-消费者模型)

悲观锁&乐观锁

悲观锁与乐观锁是根据操作时是否锁住资源来判别的

悲观锁是什么

悲观锁之所以悲观,是因为它觉得如果不锁住这个资源,别的线程就会来争抢,造成数据结果错误。

所以悲观锁为了确保结果的正确性,会在每次获取并修改数据时,都把数据锁住,让其他线程无法访问该数据,这样就可以确保数据内容万无一失。从这点看悲观锁特别稳。

悲观锁执行过程
  1. 如果 A 拿到锁,正在操作资源,B 就只能进入等待

Sad_lock_1

  1. 直至 A 执行完毕释放锁,CPU 唤醒等待此锁的线程 B

Sad_Lock_2

  1. 线程 B 获取到了锁,就可以对同步资源进行自己的操作

Sad_Lock_3

乐观锁是什么

乐观锁之所以乐观,因为它觉得自己在操作资源时并不会有其他线程干扰。相对于悲观锁,它是不锁住资源的,

因此,为了保障数据的正确性,它在操作之前会先判断在自己操作期间,其他线程是否有操作。如果有,根据业务选择报错或者重试;否则直接操作

乐观锁其实就是依赖的 CAS (compare and swap:比较并交换)算法。

所以,它在操作资源之前并不需要获得锁,直接读取资源到自己的工作内存并直接操作

乐观锁获取数据直接操作

操作完成,准备更新资源时。就会触发 CAS 算法,判断资源是否被其他线程修改过:

Optimistic_check

通过CAS判断:

  1. 没有修改过,直接更新,线程执行完毕

    cas1

  2. 被修改过,根据业务逻辑选择重试或报错

cas2

典型应用

不管是在 Java 还是数据库中都用到了悲观锁、乐观锁的概念,只是实现方式稍有不同。

Java中:

  • 悲观锁:synchronized 关键字和 Lock 接口

    synchronized 必须要获取 mintor 锁才能进去操作资源;Lock 接口也是,必须显示调用 lock 才能操作资源。必须取到锁才能进行操作,这就是悲观锁的思想

  • 乐观锁:原子类

    比如用作线程间的计数器。典型如 AtomicInteger 类在进行运算时,就使用了乐观锁的思想。使用 compareAndSet 方法更新数据,更新失败则重试。

数据库中:

  • 悲观锁:典型的 select for update 语句,在提交之前不允许第三方来修改该数据(高并发环境吃不消)
  • 乐观锁:利用 version 字段实现乐观锁,version 代表这条数据的版本。操作数据不需要获取锁,操作完准备更新时。对比版本号是不是和获取数据时一致?是:更新,否:重新计算,再尝试更新。
使用场景

有人说悲观锁比乐观锁消耗大,因为悲观要锁、乐观不要锁(注意,这里说的是实际没锁住资源,它的锁其实是 CAS 算法)。

是的,如果并发量很小的情况下,悲观锁确实比乐观锁消耗大。但如果并发量很高,导致乐观锁一直在重试,这时它消耗的资源比固定开销的悲观大,也是说不定的。

  • 悲观锁适用于并发写入多,竞争激烈等场景,这些场景下,悲观锁确实会让得不到锁的线程阻塞,但这些开销是固定的。它可以避免后面更新时的无用反复尝试操作,节约开销。
  • 乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。在这些场景下,乐观锁不加锁的特点能让性能大幅提高。

公平锁 & 非公平锁

「公平锁」:多个线程按顺序排队获取锁,每个线程获取机会均等,但永远只有队列首位线程能获取到锁。

  • 优点:每个线程等待一段时间后,都有执行的机会,不至于出现某个线程饿死在队列中。
  • 缺点:是队列里面除了第一个线程,其他的线程都会阻塞,cpu 唤醒阻塞线程的开销会很大。

「非公平锁」:多个线程(不管是不是队列首位)去获取锁时会尝试直接获取锁,能获取到就执行任务,否则乖乖排队。

  • 优点:获取锁更加灵活、吞吐量大、减少 CPU 唤醒线程的开销。
  • 缺点:会出现某些线程永远获取不到锁,饿死在队列中,最终由 JVM 回收。
源码实现

ReentrantLock 默认是非公平锁,想要使用公平锁,创建锁的时候直接给个 true 即可。

ReentrantLock lock = new ReentrantLock(true);

源码可以看到 ReentrantLock 内部有一个 Sync 内部类。他继承了 AbstractQueuedSynchronizer (也就是我们常说的 AQS),在操作锁的时候大多都是通过 Sync 实现的。

public class ReentrantLock implements Lock, java.io.Serializable {

        private static final long serialVersionUID = 7373984872572414699 L;

        /** Synchronizer providing all implementation mechanics */

        private final Sync sync;
    
 		abstract static class Sync extends AbstractQueuedSynchronizer {
    		...
		}   
}

Sync 他又有两个子类,分别是 NonfairSync(非公平锁) & FairSync(公平锁),见名知义。

「非公平锁加锁实现」

从 nonfairTryAcquire 方法内部的 compareAndSetState 方法可以看到,非公平锁获取锁主要是通过 CAS 方式,修改 state 状态为 1,并且通过 setExclusiveOwnerThread (current) 把当前持有锁的线程改为自己。

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691 L;

    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 获取锁的关键代码
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平锁加锁实现」

公平锁跟非公平锁加锁的逻辑差不多,唯一就是公平加锁的 if 判断中多了 hasQueuedPredecessors 是否队首的判断。

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540 L;

    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 多了 hasQueuedPredecessors 是否队首判断
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
一个特例

针对 tryLock () 方法,它不遵守设定的公平原则。

例如,当有线程执行 tryLock () 方法的时候,一旦有线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使设置的是公平锁模式,即使在它之前已经有其他正在等待队列中等待的线程,简单地说就是 tryLock 可以插队。

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
} //这里调用的就是 nonfairTryAcquire (),表明了是不公平的,和锁本身是否是公平锁无关。

读锁 & 写锁

再次看一次 Lock 的继承关系

Read_Write_Lock

ReadWrite_Lock

从上图可以看见,读锁 & 写锁和 ReentrantLock 一样都是实现了 Lock 接口,并且是 ReentrantReadWriteLock 类的内部类。

  • 写锁也叫独占锁,它既能读取数据也能修改数据,同一时间只能有一个线程持有,它是非线程安全的
  • 读锁也叫共享锁,它只能读取数据,允许多个线程同时持有,它是线程安全的
为什么要有读写锁?

想象这样一个场景:在没有读写锁的情况下。我们用 ReentrantLock 仍然是可以保证线程安全的,但同时也浪费了资源。因为读操作是线程安全的,我们允许让多个读操作并行,以便提高程序效率。

但是**「写操作不是线程安全的」**,如果多个线程同时写,或者在写的同时进行读操作,便会造成线程安全问题。

所以,「在读的地方合理使用读锁,在写的地方合理使用写锁,灵活控制,可以提高程序的执行效率」

获取读写锁的规则

看完以上之后,在使用读写锁时遵守下面的 3 个规则:

  1. 有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。

  2. 有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。

  3. 有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。

总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。

读读共享、其他互斥(写写互斥、读写互斥、写读互斥)

如何使用读写锁
/**
 * 读写锁用法
 */
public class ReadWriteLockDemo {

    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);

    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();

    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        // 两个线程获取读锁
        new Thread(ReadWriteLockDemo::read).start();
        new Thread(ReadWriteLockDemo::read).start();
        // 两个线程获取写锁
        new Thread(ReadWriteLockDemo::write).start();
        new Thread(ReadWriteLockDemo::write).start();
    }

}

自旋锁 & 非自旋锁

自旋锁

自旋在 Java 中也就是循环的意思,比如 for 循环,while 循环等等。那自旋锁顾名思义就是**「线程循环地去获取锁」**。

非自旋锁

非自旋锁,也就是普通锁。获取不到锁,线程就进入阻塞状态。等待 CPU 唤醒,再去获取。

执行流程

想象以下场景:某线程去获取锁(可能是自旋锁 or 非自旋锁),然而锁现在被其他线程占用了。它两获取锁的执行流程就如下图所示:

loop_lock

  • 自旋锁:一直占用 CPU 的时间片去循环获取锁,直到获取到为止。
  • 非自旋锁:当前线程进入阻塞,CPU 可以去干别的事情。等待 CPU 唤醒了,线程才去获取非自旋锁。

自旋锁的优点:

  • 阻塞 & 唤醒线程都是需要资源开销的,如果线程要执行的任务并不复杂。这种情况下,切换线程状态带来的开销比线程执行的任务要大。
  • 而很多时候,我们的任务往往比较简单,简单到线程都还没来得及切换状态就执行完毕。这时我们选择自旋锁明显是更加明智的。
  • 所以,自旋锁的好处就是**「用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销」**。
Java 中的自旋锁

在 Java 1.5及以上的 JUC 包里面的原子类基本都是自旋锁的实现。我们看看做常用的 AtomicInteger 类,它里面有个 getAndIncrement 方法,源码如下:

getAndIncrement

getAndIncrement 也是直接调用 nsafe 的 getAndAddInt 方法,从下面源码可以看出这个方法直接就是做了一个 do-while 的循环。「这个循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止」

unsafe.getAndAddInt

自旋锁的缺点:
  • 虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。
  • 虽然刚开始自旋锁的开销大于线程切换。但是随着时间一直递增,总会超过线程切换的开销。
适用场景

首先我们知道自旋锁的好处就是能减少线程切换状态的开销;坏处就是如果一直旋下去,自旋开销会比线程切换状态的开销大得多

所以我们的适用场景就是:

  • 并发不能太高,避免一直自旋不成功
  • 线程执行的同步任务不能太复杂,耗时比较短
手写一个自旋锁?

为了引入自旋特性,我们使用 AtomicReference 类提供一个可以原子读写的对象引用变量。

定义一个加锁方法,如果有其他线程已经获取锁,当前线程将进入自旋,如果还是已经持有锁的线程获取锁,那就是重入。

定义一个解锁方法,解锁的话,只有持有锁的线程才能解锁,解锁的逻辑思维将 count-1,如果 count == 0,则是把当前持有锁线程设置为 null,彻底释放锁。

package com.nasus.thread.lock.Spin;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 实现一个可重入的自旋锁
 */
public class ReentrantSpinLock {

    private AtomicReference<Thread> owner = new AtomicReference<>();

    //重入次数
    private int count = 0;

    public void lock() {

        Thread t = Thread.currentThread();

        if (t == owner.get()) {
            ++count;
            return;
        }

        //自旋获取锁
        while (!owner.compareAndSet(null, t)) {
            System.out.println("自旋了");
        }

    }

    public void unlock() {

        Thread t = Thread.currentThread();

        //只有持有锁的线程才能解锁
        if (t == owner.get()) {
            if (count > 0) {
                --count;
            } else {
                //此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁
                owner.set(null);
            }
        }
    }

    public static void main(String[] args) {

        ReentrantSpinLock spinLock = new ReentrantSpinLock();

        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
            spinLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();

    }
}

从结果我们可以看出,前面一直打印 “自旋了”,说明 CPU 一直在尝试获取锁。

(我顶不住了)

生产者消费者模式

生产者消费者问题是线程模型中的经典问题:

生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。

producer-consumer

这个模式里有三个角色:

  • 生产者线程:生产消息、数据
  • 消费者线程:消费消息、数据
  • 阻塞队列:作数据缓冲、平衡二者能力

我们知道当阻塞队列分空时,我们要提醒消费者;队列未满时,我们要提醒生产者;

而队列中在空或满的状态下则会导致线程阻塞,阻塞之后就要在合适的时候去唤醒被阻塞的线程。

Q1:什么时候会唤醒阻塞线程?

  1. 当生产者判断队列已满,生产者线程进入等待。这期间消费者一旦消费了数据、队列有空位,就会通知所有的生产者,唤醒阻塞的生产者线程。
  2. 当消费者判断队列为空,消费者线程进入等待。这期间生产者一旦往队列中放入数据,就会通知所有的消费者,唤醒阻塞的消费者线程。

Q2:为什么要用这种模式?

  1. **实现了解耦:**生产者不用管消费者的动作,消费者也不用管生产者的动作,他们之间通过阻塞队列通信
  2. **阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力:**生产者只有在队列满或消费者只有在队列空时才会等待,其他时间谁抢到锁谁工作,提高效率。

wait/notify方法

wait()
  • wait 会让当前线程进入等待状态,除非其他线程调用了 notify 或者 notifyAll 方法唤醒它,又或者等待时间到。另外,当前线程必须持有对象监控器(也就是使用 synchronized 加锁)
  • 必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 加锁。
  • 超时时间非法,抛 IllegalArgumentException 异常;不持有对象的 monitor 锁,抛 IllegalMonitorStateException 异常;在等待期间被其他线程中断,抛出 InterruptedException 异常。
notify() & notifyAll()
  • notify () 随机唤醒一个等待该对象锁的线程,即使是多个也随机唤醒其中一个(与线程优先级无关)。
  • notifyAll () 通知所有在等待该竞争资源的线程,谁抢到锁谁拥有执行权(与线程优先级无关)。
  • 当前线程不持有对象的 monitor 锁,抛 IllegalMonitorStateException 异常。
wait()和sleep()的区别
相同点
  1. 都会暂停当前的线程,让其进入计时等待。对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。
  2. 它们都可以响应 interrupt 中断,并抛出 InterruptedException 异常。
不同点
  1. wait 是 Object 类的方法,而 sleep 是 Thread 类的方法。
  2. Thread.sleep()不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep()不会让线程释放锁。(可以简单认为和锁相关的方法都定义在Object类中);调用 wait 方法会释放 monitor 锁。
  3. wait 方法必须在 synchronized 保护的代码中使用;sleep 方法可在任意地方。
  4. sleep()时间一到马上恢复执行(因为没有释放锁);调用wait()后需要等中断,或者对应对象的 notify 或 notifyAll 才会恢复,抢到锁才能够重新获得CPU执行时间。

等待唤醒机制

wait 会让当前线程等待并释放锁; notify 唤醒任意一个等待同一个锁的线程; notifyAll 则是唤醒所有等待该锁的线程,谁先抢到锁谁执行。这就是所谓的等待唤醒机制。

  • wait():当缓冲区已满/空时,生产者/消费者线程停止自己的执行,主动释放锁使自己处于等待状态,让其他线程执行。
  • notify():当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。

先来看看用等待唤醒机制如何实现生产者、消费者模式的

阻塞队列
public class MyBlockingQueue {

    private int maxSize;
    private LinkedList<Integer> queue; 
	//定义了一个队列
    public MyBlockingQueue(int size) {
        this.maxSize = size;
        queue = new LinkedList<>();
    }

    //首先看put方法
    public synchronized void put() throws InterruptedException {
        while (queue.size() == maxSize) { //while 检查队列是否已满,满则进入等待并主动释放锁
            System.out.println("队列已满,生产者: " + Thread.currentThread().getName() +"进入等待");
            wait();
        }
        
        Random random = new Random();
        int i = random.nextInt();
        System.out.println("队列未满,生产者: " +
                Thread.currentThread().getName() +"放入数据" + i);
        
       //不满则生产数据,同时判断放入数据之前队列是否空
        if (queue.size() == 0) { // 队列空则唤醒消费者(因为当队列为空消费者才进入阻塞,而此时已有数据可消费)
            notifyAll();
        }

        queue.add(i); //生产数据
    }

    //然后看take方法
    public synchronized void take() throws InterruptedException {
        while (queue.size() == 0) {  //while 检查队列是否空,空则进入等待并主动释放锁
            System.out.println("队列为空,消费者: " + Thread.currentThread().getName() +"进入等待");
            wait(); 
        }
        
		//不空则生产数据,同时判断取出数据之前队列是否已满    
        if (queue.size() == maxSize) { // 队列满则唤醒生产者(因为队列满时生产者才进入阻塞,此时队列已可生产)
            notifyAll();
        }

        System.out.println("队列有数据,消费者: " +
                Thread.currentThread().getName() +"取出数据: " + queue.remove()); //消费数据
    }

}

注:多线程情况下必须用 while判断 而不是用 if

生产者&消费者
//生产者类
public class Producer implements Runnable {

    private MyBlockingQueue myBlockingQueue;

    public Producer(MyBlockingQueue myBlockingQueue) {
        this.myBlockingQueue = myBlockingQueue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                myBlockingQueue.put();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//消费者类
public class Consumer implements Runnable{

    private MyBlockingQueue myBlockingQueue;

    public Consumer(MyBlockingQueue myBlockingQueue) {
        this.myBlockingQueue = myBlockingQueue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                myBlockingQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//测试类
public class MyBlockingQueueTest {

    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue(10);
        Producer producer = new Producer(myBlockingQueue);
        Consumer consumer = new Consumer(myBlockingQueue);
        new Thread(producer).start();
        new Thread(consumer).start();
    }

}

Condition 实现

Condition 是一个多线程间协调通信的工具类,它的 await、sign/signAll 方法正好对应 Object 的 wait、notify/notifyAll 方法。相比于 Object 的 wait、notify 方法,Condition 的 await、signal 结合的方式实现线程间协作更加安全和高效,所以更推荐这种方式实现线程间协作。

Object 的 wait、notify 方式需要结合 synchronized 关键字实现等待唤醒机制,同样 Condition 也需要结合 Lock 类。

实现思路基本一样,这里就当复习一下

阻塞队列
public class MyBlockingQueueForCondition {
	//同样定义了一个队列
    private Queue<Integer> queue;
    private int max = 10;
    //ReentrantLock 类型的锁
    private ReentrantLock lock = new ReentrantLock();
    //创建 notFull、notEmpty 两个条件,分别代表未满、不为空的条件。最后定义了 take、put 方法。
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public MyBlockingQueueForCondition(int size) {
        this.max = size;
        queue = new LinkedList();
    }

    //同样先看put
    public void put(Integer i) throws InterruptedException {
        // 先获取锁,确保同步
        lock.lock();
        try { 
            while (queue.size() == max) { //while 检查队列是否已满,满则进入等待并主动释放锁
                System.out.println("队列已满,生产者: " + Thread.currentThread().getName() + "进入等待");
                notFull.await();
            }

             //不满则生产数据,同时判断放入数据之前队列是否空
            if (queue.size() == 0) { // 队列空则唤醒消费者(因为当队列为空消费者才进入阻塞,而此时已有数据可消费)
                notEmpty.signalAll();
            }

            // 队列不满,生产数据
            queue.add(i);
        } finally {
            // 最后一定要释放锁
            lock.unlock();
        }
    }

    public Integer take() throws InterruptedException {
        // 加锁
        lock.lock();
        try {
            while (queue.size() == 0) {//while 检查队列是否空,空则进入等待并主动释放锁
                System.out.println("队列为空,消费者: " + Thread.currentThread().getName() + "进入等待");
                notEmpty.await();
            }
			//不空则生产数据,同时判断取出数据之前队列是否已满  
            if (queue.size() == max) { // 队列满则唤醒生产者(因为队列满时生产者才进入阻塞,此时队列已可生产)
                notFull.signalAll();
            }

            // 否则,取出
            return queue.remove();
        } finally {
            // 最后别忘记释放锁
            lock.unlock();
        }
    }
}
生产者&消费者
//生产者
public class ProducerForCondition implements Runnable {

    private MyBlockingQueueForCondition myBlockingQueueForCondition;

    public ProducerForCondition(MyBlockingQueueForCondition myBlockingQueueForCondition) {
        this.myBlockingQueueForCondition = myBlockingQueueForCondition;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                myBlockingQueueForCondition.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//消费者
public class ConsumerForCondition implements Runnable{

    private MyBlockingQueueForCondition myBlockingQueueForCondition;

    public ConsumerForCondition(MyBlockingQueueForCondition myBlockingQueueForCondition) {
        this.myBlockingQueueForCondition = myBlockingQueueForCondition;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                System.out.println("消费者取出数据: " + myBlockingQueueForCondition.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

//测试类
public class MyBlockingQueueForConditionTest {

    public static void main(String[] args) {
        MyBlockingQueueForCondition myBlockingQueueForCondition = new MyBlockingQueueForCondition(10);
        ProducerForCondition producerForCondition = new ProducerForCondition(myBlockingQueueForCondition);
        ConsumerForCondition consumerForCondition = new ConsumerForCondition(myBlockingQueueForCondition);
        new Thread(producerForCondition).start();
        new Thread(consumerForCondition).start();
    }

}

BlockingQueue 实现

你可能已经发现,生产者消费者模型主要代码还是在阻塞队列当中。而 Java当中也已经提供了 BlockingQueue 接口以及对应的实现类,如ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue等。我们这里选用最简单的ArrayBlockingQueue 实现。它的内部也是采取 ReentrantLock 和 Condition 结合的等待唤醒机制

public class ArrayBlockingQueueTest {

    public static void main(String[] args) {
        // 创建一个 ArrayBlockingQueue 并给定最大长度为 10
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); 
        // 创建生产者
        Runnable producer = () -> {
            try {
                // 放入数据
                Random random = new Random();
                while (true) { //生产者在 while (true) 中有空位就生产
                    queue.put(random.nextInt());
                }
            } catch (Exception e) {
                System.out.println("生产数据出错: " + e.getMessage());
            }
        };
        // 开启线程生产数据
        new Thread(producer).start();

        // 创建消费者
        Runnable consumer = () -> {
            try {
                // 取出数据
                while (true) { //消费者在 while (true) 中有数据就取
                    System.out.println(queue.take());
                }
            } catch (Exception e) {
                System.out.println("消费数据出错: " + e.getMessage());
            }
        };
        // 开启线程消费数据
        new Thread(consumer).start();
    }

}

虽然看上去很简单,但其实背后 ArrayBlockingQueue 已经为我们做好了线程间通信的工作了,比如队列满了就去阻塞生产者线程,队列有空就去唤醒生产者线程等
ition = new ConsumerForCondition(myBlockingQueueForCondition);
new Thread(producerForCondition).start();
new Thread(consumerForCondition).start();
}

}




###  BlockingQueue 实现

你可能已经发现,生产者消费者模型主要代码还是在阻塞队列当中。而 Java当中也已经提供了 BlockingQueue 接口以及对应的实现类,如ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue等。我们这里选用最简单的`ArrayBlockingQueue` 实现。**它的内部也是采取 ReentrantLock 和 Condition 结合的等待唤醒机制**。

```java
public class ArrayBlockingQueueTest {

    public static void main(String[] args) {
        // 创建一个 ArrayBlockingQueue 并给定最大长度为 10
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); 
        // 创建生产者
        Runnable producer = () -> {
            try {
                // 放入数据
                Random random = new Random();
                while (true) { //生产者在 while (true) 中有空位就生产
                    queue.put(random.nextInt());
                }
            } catch (Exception e) {
                System.out.println("生产数据出错: " + e.getMessage());
            }
        };
        // 开启线程生产数据
        new Thread(producer).start();

        // 创建消费者
        Runnable consumer = () -> {
            try {
                // 取出数据
                while (true) { //消费者在 while (true) 中有数据就取
                    System.out.println(queue.take());
                }
            } catch (Exception e) {
                System.out.println("消费数据出错: " + e.getMessage());
            }
        };
        // 开启线程消费数据
        new Thread(consumer).start();
    }

}

虽然看上去很简单,但其实背后 ArrayBlockingQueue 已经为我们做好了线程间通信的工作了,比如队列满了就去阻塞生产者线程,队列有空就去唤醒生产者线程等

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值