JUC Thread 基础回顾

文章目录

  • 并行与并发
  • 进程与线程
    • 线程切换上下文
  • 创建线程的方法
  • 用户线程和守护线程
  • Thread 常用方法
    • 实例方法
    • 类方法
      • 线程打断示例
  • 线程的状态
  • 线程安全
    • 怎样尽可能的避免线程安全问题
  • 线程同步
  • synchronized
  • 经典示例
    • 错误写法
    • 添加 synchronized 关键字
    • 最小化同步块
    • 最小化同步块,并进行重入判断
  • Monitor 监视器(管程)
    • Monitor 的核心组成部分
  • 锁优化
    • 轻量级锁
    • 自旋优化
    • 偏向锁
      • 偏向锁和轻量级锁
    • 锁消除
  • wait 和 notify/notifyAll
    • 示例:点外卖

并行与并发

  • 并发(concurrent):是指多个线程在同一个 CPU 下 “同时” 执行。
  • 并行(Parallel):是指多个线程在多个 CPU 下同时执行。

你一个人同时交了 3 个女朋友,你要同时应付 3 个人,这叫并发。你们 3 个人,分别交了一个女朋友,这叫并行。

进程与线程

  • 进程:某些软件启动的时候就会在操作系统中启动一个或多个进程。
  • 线程:一个进程内可以有一个或多个线程。
  • 进程基本上是相互独立的,而多个线程可以存在于一个进程内
  • 多个线程可以在同一个进程内共享进程拥有的资源(如:内存空间)
  • 线程更轻量,线程都上下文切换成本一般情况下要比进程上下文切换低

线程切换上下文

线程上下文切换,此过程在 CPU 并发执行多个线程的情况下,由一个线程切换到另一个线程执行的过程。此过程涉及到保存当前执行线程的当前状态和载入要切换的线程的状态。这些状态就是我们在 《JVM 运行时数据区》一章中我们提到的 PC 寄存器和Java 虚拟机栈等,更详细的内容请参考我们前面的 JVM 系列的《JVM 运行时数据区》一文

  • PC 寄存器,是每个线程私有的,指向当前线程下一个要执行的指令。
  • 每个线程都对应一个 Java 虚拟机栈,其内部分为一个一个的栈帧(stack Frame),对应着一次一次的 Java 方法调用。

如果线程数过多,并发量大,上下文切换就会很频繁,不断频繁的切换上下文就会影响线程的执行效率。

创建线程的方法

  • 继承 Thread
  • 实现 Runable 接口

用户线程和守护线程

  • 用户线程(User Thread):是系统的工作线程,它会完成线程要执行的所有程序。它是独立的执行线程,其生命周期跟创建它的线程独立。(如果用户线程死循环或死锁,一直不结束,其暂用的服务器资源也不会释放)
  • 守护线程(Daemon Thread):其生命周期随创建它的线程消亡而消亡。

示例

public class DaemonThread {

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        },"userThread");


        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"DeamonThread");

        // 设置 t2 为守护线程(注意:此方法要在 start 之前执行才能设置成功)
        t2.setDaemon(true);
        t1.start();
        t2.start();


    }

}

执行结果:

userThread:0
userThread:1
userThread:2
userThread:3
userThread:4
DeamonThread0
userThread:5
userThread:6
userThread:7
userThread:8
userThread:9

可以看到守护进行的逻辑并没有完全执行完毕。因为守护进行随着 main 主线程的结束而结束了。

Thread 常用方法

实例方法

  • start():启动线程
  • run():实现多线程需要重写该方法。该方法如果直接调用,不会启动多线程,跟其他正常调用一样。
  • isAlive():当前线程是否存活
  • getName():获取现场的名称
  • setPriority() 、getPriority 设置(获取)线程优先级 [0,10],默认为 5.
  • setDaemon(boolean on) 设置是否为守护线程。
  • join(long millis) 等待线程执行 millis 毫秒。一般不建议使用该方法,join 的本质就是等待调用该方法的线程先执行完。
  • join(),等待线程执行结束。比如在 main 线程中调用 t1.join(),表示 main 线程将等待 t1 线程执行结束后才接着执行。join 方法的底层其实是 wait 方法。
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

		// millis == 0 表示无超时等待
        if (millis == 0) {
        	// isAlive 是 native 方法,判断当前线程是否是活动的
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
            	// delay 表示要休眠的时间 millis 减去 已经休眠的时间 now
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);// 这里如果直接设置为 millis ,可能在虚假唤醒时导致真正 wait 的时间比 millis 长
                // 这里计算 now ,表示当前已经 wait 了多长时间了
                now = System.currentTimeMillis() - base;
            }
        }
    }

该源码,可结合下文 wait/notify 一节来理解。

  • interrupt():打断线程。被打断的线程对应的 join 、wait 、sleep 等方法将抛出 InterruptedException,且清除打断标记(标记为 false)。如果打断正常执行的线程,打断标记为(true)。
  • isInterrupted():获取打断标记。如果线程被中断,返回 true,否则为 false。该方法不重置打断标记的状态(不清除打断状态)。其源码如下:
public boolean isInterrupted() {
   return isInterrupted(false);
}
// ClearInterrupted 表示是否重置打断状态
private native boolean isInterrupted(boolean ClearInterrupted);

类方法

  • currentThread():获取当前线程对象。(在哪个线程中执行,即获取哪个线程)
  • yield():线程礼让(在哪个线程中执行,表示当前线程让出CPU使用权)。正常情况不推荐使用,可在调试中使用
  • sleep(long millis):线程休眠(固定的时间)。线程休眠时不会释放锁。
  • interrupted():当前线程的打断标记。并重置打断标记。将清除打断状态,重复调用两次该方法,第二次将返回false。
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

线程打断示例

       Thread t1 = new Thread(()->{
           boolean flag = false;
            while ((flag = Thread.currentThread().isInterrupted()) == false){
                System.out.println("执行..." + flag); // 在此处被打断打断标记不会被重新设定为 false
                try {
                    Thread.sleep(1000); // 在此处被打断,打断标记会被重新设定为 false
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 可在此编写如果被打断,线程停止还是继续执行
                    Thread.currentThread().interrupt();// 再次执行打断(则退出循环)
                }
            }
       },"t1");


       t1.start();
       Thread.sleep(10000);

       t1.interrupt();

线程的状态

  • NEW
    尚未启动的线程处于此状态。new 了 Thread 对象,但没有执行 start 方法
  • RUNNABLE
    Java虚拟机中执行的线程处于此状态。调用了 start 方法后,如果没有其他特殊的操作,都处于此状态
  • BLOCKED
    等待监视器锁定的阻塞线程处于此状态。
  • WAITING
    无限期等待另一个线程执行特定操作的线程处于此状态。调用 jion 、wait 等不传等待时间的方法后的状态
  • TIMED_WAITING
    等待另一个线程执行操作长达指定等待时间的线程处于此状态。调用了 jion、wait、sleep 等传了等待时间的方法后的状态
  • TERMINATED
    已退出的线程处于此状态。

线程的几种状态示例:

        // 线程 1 不执行 start 方法,状态为 NEW
        Thread t1 = new Thread(()->{

        },"t1");

        // t2 写个死循环,让他一直执行下去,状态为 RUNNABLE
        Thread t2 = new Thread(()->{
            while (true){}
        },"t2");

        // t3 执行 t2.join 不设时间,状态为 WAITING
        Thread t3 = new Thread(()->{
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t3");

        // t4 synchronized 加锁,睡眠一段时间,状态为:TIMED_WAITING
        Thread t4 = new Thread(()->{
            synchronized (StateTest.class) {
                try {
                    Thread.sleep(999999);
                    // 或者
                    //t2.join(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t4");

        // t5 直接执行,会在短时间内执行结束,状态为 TERMINATED
        Thread t5 = new Thread(()->{

        },"t5");


        // t6 和 t4 共用一把锁,当 t4 执行时,t4 抱锁睡眠,t6 则处于 BLOCKED 状态
        Thread t6 = new Thread(()->{
            synchronized (StateTest.class) {

            }
        },"t6");

        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();

        Thread.sleep(200);

        System.out.println("t1:" + t1.getState());
        System.out.println("t2:" + t2.getState());
        System.out.println("t3:" + t3.getState());
        System.out.println("t4:" + t4.getState());
        System.out.println("t5:" + t5.getState());
        System.out.println("t6:" + t6.getState());

线程安全

  • 临界区(Critical Section)

一段代码段内,如果出现对共享资源进行读写操作,则这段代码称为临界区。

  • 竟态条件(Race Condition)

多个线程在不同的时刻访问同一个资源,导致数据不一致或错误的结果或者结果无法预测(每次执行结果不一样)。

线程安全的代码是指在多线程环境下,不管多少线程并发访问,都能保证程序的正确性和一致性。线程安全的代码不会出现上述问题。

怎样尽可能的避免线程安全问题

尽量使用局部变量:因为局部变量存储在 Java 虚拟机栈,而虚拟机栈是线程私有的,多个线程直接不会存在局部变量的共享使用。

如果局部变量的引用被暴露给方法外部(常见为通过return将局部变量引用返回),也会产生线程安全问题。

线程同步

多线程在操作同一个资源时,同一时刻只能有一个线程操作,其他线程等待这个线程操作结束后抢占操作这个资源,就是线程同步。

线程同步可以保证多线程在操作同一个资源时,结果的正确性。但同时只能有一个线程可以操作,降低了程序的性能。

synchronized

synchronized关键字是一种内置的同步机制,用于控制多个线程对共享资源的访问。它用于保证在同一时刻,只有一个线程可以访问被synchronized保护的代码块或方法。但其可能引发死锁问题。

  • 修饰代码块:当synchronized用于修饰一个代码块时,它被称为同步块,其内部包含的代码只能由一个线程执行。如果多个线程试图同时进入同一个同步块,其他线程将会被阻塞(这些线程的状态为:BLOCKED),直到当前线程退出该同步块。这种方式常用于对共享资源进行细粒度的锁定(只对同步块内进行锁定),使用 synchronized 关键字实现同步时,该方式为推荐使用方式。
public class Example {  
    private Object lock = new Object();  
      
    public void someMethod() {  
        synchronized (lock) {  
            // 代码块  
        }  
    }  
}
  • 修饰成员方法:当synchronized用于修饰成员方法时,整个方法体只能由一个线程执行。这意味着如果一个线程正在执行该方法,其他任何尝试执行该方法的线程都将被阻塞。这种方法常用于对方法的整体逻辑进行保护,但要注意,使用同步方法可能会对整个对象进行锁定,而不仅仅是对所需的部分进行锁定。
public class Example {  
      
    public synchronized void someMethod() {  
        // 方法体  
    }  
}
  • 修饰静态方法:当synchronized用于修饰静态方法时,它将对整个进行锁定,这意味着如果一个线程正在执行该静态方法,其他任何尝试执行该类中任何静态方法的线程都将被阻塞。这常用于控制对类级别的静态资源的访问。
public class Example {  
    public static synchronized void someStaticMethod() {  
        // 静态方法体  
    }  
}

经典示例

多窗口买票问题

错误写法

package com.yyoo.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 模拟多窗口卖票
 */
public class SellTicketTest {

    /**
     * 当前票量
     */
    private int count;

    /**
     * @param count 总共多少张票
     */
    public SellTicketTest(int count){
        this.count = count;
    }

    public int getCount(){
        return this.count;
    }

    /**
     * 卖票
     * @param buyNum 一次销售的数量
     */
    public void sell(int buyNum){
        if(this.count >= buyNum){
            try {
                Thread.sleep(10); // 模拟 CPU 上下文切换
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.count -= buyNum;
        }else {
            // 抛出此异常,说明是正常判断的
            throw new RuntimeException("余票不足");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        SellTicketTest ticket = new SellTicketTest(1000);
        Random random = new Random();
        List<Thread> list = new ArrayList<>();
        for(int i = 0; i < 1500; i++){
            Thread t = new Thread(()->{
                // 一次
                ticket.sell(1 + random.nextInt(6));
            },"t" + i);
            t.start();
            list.add(t);
        }

        for(Thread t : list){
            t.join();
        }

        // 余票不为 0 ,表示出现了线程安全问题
        System.out.println("最后剩余:" + ticket.getCount());
        System.out.println("总耗时:" + (System.currentTimeMillis() - start)+"ms");

    }

}

结果

最后剩余:-642 # 多次执行此值是不一样的
总耗时:263ms

添加 synchronized 关键字

package com.yyoo.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 模拟多窗口卖票
 */
public class SellTicketTest {

    /**
     * 当前票量
     */
    private int count;

    /**
     * @param count 总共多少张票
     */
    public SellTicketTest(int count){
        this.count = count;
    }

    public int getCount(){
        return this.count;
    }

    /**
     * 卖票
     * @param buyNum 一次销售的数量
     */
    public synchronized void sell(int buyNum){
        if(this.count >= buyNum){
            try {
                Thread.sleep(10); // 模拟 CPU 上下文切换
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.count -= buyNum;
        }else {
            // 抛出此异常,说明是正常判断的
            throw new RuntimeException("余票不足");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        SellTicketTest ticket = new SellTicketTest(1000);
        Random random = new Random();
        List<Thread> list = new ArrayList<>();
        for(int i = 0; i < 1500; i++){
            Thread t = new Thread(()->{
                // 一次
                ticket.sell(1 + random.nextInt(6));
            },"t" + i);
            t.start();
            list.add(t);
        }

        for(Thread t : list){
            t.join();
        }

        // 余票不为 0 ,表示出现了线程安全问题
        System.out.println("最后剩余:" + ticket.getCount());
        System.out.println("总耗时:" + (System.currentTimeMillis() - start)+"ms");

    }

}

结果

最后剩余:0
总耗时:4481ms

此方式,结果是对了,但是耗时太长

最小化同步块

package com.yyoo.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 模拟多窗口卖票
 */
public class SellTicketTest {

    /**
     * 当前票量
     */
    private int count;

    /**
     * @param count 总共多少张票
     */
    public SellTicketTest(int count){
        this.count = count;
    }

    public int getCount(){
        return this.count;
    }

    /**
     * 卖票
     * @param buyNum 一次销售的数量
     */
    public void sell(int buyNum){
        if(this.count >= buyNum){
            try {
                Thread.sleep(10); // 模拟 CPU 上下文切换
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (this) {
                this.count -= buyNum;
            }
        }else {
            // 抛出此异常,说明是正常判断的
            throw new RuntimeException("余票不足");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        SellTicketTest ticket = new SellTicketTest(1000);
        Random random = new Random();
        List<Thread> list = new ArrayList<>();
        for(int i = 0; i < 1500; i++){
            Thread t = new Thread(()->{
                // 一次
                ticket.sell(1 + random.nextInt(6));
            },"t" + i);
            t.start();
            list.add(t);
        }

        for(Thread t : list){
            t.join();
        }

        // 余票不为 0 ,表示出现了线程安全问题
        System.out.println("最后剩余:" + ticket.getCount());
        System.out.println("总耗时:" + (System.currentTimeMillis() - start)+"ms");

    }

}

结果

最后剩余:-579
总耗时:277ms

最小化同步块,时间降下来了,但是结果不对

最小化同步块,并进行重入判断

package com.yyoo.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 模拟多窗口卖票
 */
public class SellTicketTest {

    /**
     * 当前票量
     */
    private int count;

    /**
     * @param count 总共多少张票
     */
    public SellTicketTest(int count){
        this.count = count;
    }

    public int getCount(){
        return this.count;
    }

    /**
     * 卖票
     * @param buyNum 一次销售的数量
     */
    public void sell(int buyNum){
        if(this.count >= buyNum){
            try {
                Thread.sleep(10); // 模拟 CPU 上下文切换
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 加锁,并进行重入判断
            synchronized(this) {
                if(this.count >= buyNum) {
                    this.count -= buyNum;
                }else {
                    throw new RuntimeException("余票不足");
                }
            }
        }else {
            // 抛出此异常,说明是正常判断的
            throw new RuntimeException("余票不足");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        SellTicketTest ticket = new SellTicketTest(1000);
        Random random = new Random();
        List<Thread> list = new ArrayList<>();
        for(int i = 0; i < 1500; i++){
            Thread t = new Thread(()->{
                // 一次
                ticket.sell(1 + random.nextInt(6));
            },"t" + i);
            t.start();
            list.add(t);
        }

        for(Thread t : list){
            t.join();
        }

        // 余票不为 0 ,表示出现了线程安全问题
        System.out.println("最后剩余:" + ticket.getCount());
        System.out.println("总耗时:" + (System.currentTimeMillis() - start)+"ms");

    }

}

结果

最后剩余:0
总耗时:232ms

结果正确,且耗时较短。

总结:使用 synchronized 关键字,应最小化同步块(避免锁膨胀),尽量降低线程阻塞的时间,且需要进行重入判断。

Monitor 监视器(管程)

  • 在Java中,Monitor(监视器)是一种同步机制,用于管理多线程之间的互斥访问和并发控制。
  • Monitor 确保在任何给定时间只有一个线程可以进入被保护的代码块,从而避免并发访问的数据竞争和不一致性。
  • 在Java中,每个对象都有一个关联的Monitor,它通过使用关键字synchronized来实现。synchronized关键字可以应用于方法或代码块,它指示只有一个线程可以同时执行带有相同监视器的同步代码。
  • 虽然Monitor提供了一种强大的同步机制,但过度使用synchronized关键字可能会导致性能问题。过多的锁竞争和线程等待可能会降低程序的执行效率。所以很多情况下需要使用更轻量级的同步机制(如Lock接口的实现)来替代synchronized关键字,以提高并发性能。

Monitor 的核心组成部分

  • Owner: 它标识了当前的持有Monitor对象的线程信息。(当前持有锁的线程
  • EntryList: 可以把它理解成一个阻塞队列,因为同一时间只有一个线程能争抢到锁,那么没有争抢到锁的线程就会被阻塞(状态变成Blocked),也就是加入到Monitor对象的EntryList中,等待Owner中的线程释放锁后,再去重新争抢锁。(在排队的线程列表)
  • WaitSet: 翻译过来叫等待集合,当持有锁的线程调用了锁对象的wait方法时, 因为wait方法会去释放当前锁,所以当前线程就会从Owner退出来,加入到WaitSet中,并且状态变成Waiting状态,等待其他的线程重新唤醒。(在等待唤醒的线程列表

锁优化

Java 锁的优化方向主要是尽量减少线程阻塞和唤醒的开销,以提高并发性能。

轻量级锁

在传统的重量级锁机制中,当一个线程请求一个已经被其他线程持有的锁时,请求的线程会被挂起,并进入操作系统的调度队列,等待锁被释放。然而,上下文切换的成本是相当高昂的,严重影响并发性能。

轻量级锁则旨在避免这种情况。当一个线程请求一个已经被另一个线程持有的轻量级锁时,JVM 会让请求的线程进入自旋状态,而非将其挂起。自旋状态下的线程会在用户态不断检查锁的状态,期望在短时间内锁能够被释放,避免了昂贵的上下文切换。然而,如果锁的竞争持续时间过长,轻量级锁也会膨胀为重量级锁,以避免过长时间的无效自旋。

使用 synchronized 关键字声明的锁在没有竞争的情况下会被 JVM 优化为轻量级锁。

轻量级锁的性能优势主要源于它在无竞争情况下能够通过 CAS 操作(Compare and Swap,比较并交换)成功获取锁,而无需进行线程切换和调度。如果无法通过 CAS 操作获得锁,就会膨胀

  • 锁膨胀:轻量级锁膨胀为重量级锁的过程即为锁膨胀

自旋优化

自旋锁是指线程在获取锁的时候,当前锁被其他线程占用还未释放,那么当前线程会进入循环,一直重复获取锁的状态,而不会进入挂起或者睡眠等状态。当锁被释放后,当前线程获取到该锁的执行权,则会跳出循环执行相关代码。

  • 优点:不会产生上下文切换,避免上下文切换带来的开销
  • 缺点:循环次数过多,导致 CPU 使用率过高,效率降低。
  • 如果获得锁的线程,执行时间过长(超过上下文切换带来的开销),会导致当前线程自旋次数过多
  • 自旋线程和获得锁的线程是并行的,所以多核 CPU 自旋优化才有作用

自适应自旋(JDK1.6+):自适应自旋会根据锁的竞争情况动态调整自旋的次数。

偏向锁

偏向锁是一种优化锁性能的策略,其核心思想是减少不必要的锁竞争开销。当一个锁被一个线程频繁获取时,JVM 将这个锁"偏向"到这个线程,意味着在此后的几次尝试中,该线程可以无需同步操作就能获取这个锁。这大大减少了锁获取和释放的开销,提升了程序的运行效率。

  • 偏向锁会保存其偏向的线程的id,当该线程再次尝试获得该锁的时候,无需同步操作。
  • 偏向锁,主要是为了优化无竞争情况下的锁性能。在没有线程竞争的情况下,偏向锁的性能一般比轻量级锁和重量级锁更优。
  • 偏向锁在高并发场景下并不总是最优的选择,当多线程竞争锁的情况下,偏向锁会通过撤销偏向锁并升级为轻量级锁或重量级锁,这个过程的消耗较大。
  • 适用于几乎没有真正线程竞争的情况,即一个线程连续多次获取同一把锁。
  • 偏向锁是默认开启(-XX:+UseBiasedLocking)的且其是延迟生效(设置 -XX:BiasedLockingStartupDelay=0 表示不延迟)的。
  • 可以使用 使用 -XX:+PrintBiasedLockingStatistics JVM 参数来获取偏向锁的统计信息(此设置在1.8中需要同步设置 -XX:+UnlockDiagnosticVMOptions 才能生效)
  • 撤销:如果锁对象调用了它的 hashCode 方法,偏向锁将升级为轻量级锁。多个线程访问偏向锁,也会导致偏向锁撤销。撤销是个消耗资源的操作。
  • 批量重偏向:当另一个线程获得该锁的次数超过阈值(20次)后,锁将重偏向到该线程。(注:重偏向的前提还是线程相互间没有竞争)
  • 批量撤销:如果线程撤销超过阀值(40次)后,JVM 会将锁对象变为不可偏向的。(注:批量撤销的前提是线程竞争加剧的情况)

偏向锁和轻量级锁

  • 偏向锁偏向锁主要针对那些几乎没有竞争的情况。它假设总是同一线程会再次获取同一把锁,因此减少了获取和释放锁的开销。然而,当存在竞争的情况时,偏向锁需要撤销,这将带来额外的开销。因此,偏向锁在并发程度比较低,但是锁获取频繁的场景下表现良好。
  • 轻量级锁轻量级锁则主要针对竞争不激烈的情况。它依赖于 CAS 操作(Compare and Swap,比较并交换)来试图获取锁,避免了线程切换的开销。但是如果长时间获取不到锁,线程会进入阻塞状态,升级为重量级锁。因此,轻量级锁在并发程度不高,且锁的争用不激烈的场景下表现良好。

锁消除

如果某个锁对象一定不会被其他线程所使用,那么 JVM 将进行优化,取消掉该锁。

    public static void main(String[] args) {
        
        Object lock = new Object();
        
        synchronized (lock){
            System.out.println("aaa");
        }
        
    }

示例中 lock 对象为局部变量,synchronized 加锁后,其他线程是无法获得 lock 的,所以 JVM 运行时,去消除该 synchronized 代码块,就像没加锁一样。

wait 和 notify/notifyAll

wait 和 notify/notifyAll 都是 Object 对象的方法,所以 Java 中的所有类,都有这些方法。而且这些方法最终都是 native 方法。

注意:wait 和 notify/notifyAll 只能在 synchronized 关键字包含的代码块或方法中执行,否则会抛出 IllegalMonitorStateException(方法注释的说法为,当前线程必须获得锁才能执行该方法)。

  • wait():表示当前线程进入 Monitor 的 WaitSet 等待,直到被唤醒,被唤醒后进入 EntryList 阻塞,直到重新获得锁
  • wait(long):表示当前线程进入 Monitor 的 WaitSet 等待,直到等待时间到或被唤醒,之后进入 EntryList 阻塞,直到重新获得锁
  • notify():唤醒在 WaitSet 中等待的某一个线程(随机)。
  • notifyAll():唤醒在 WaitSet 中等待的所有线程

示例:点外卖


/**
 * 点外卖:
 * 线程1:商家
 * 线程2:买家
 * 线程3:骑手
 * 使用 wait/notify 实现
 */
public class WaitNotifyTest1 {

    /**
     * 0:未点餐
     * 1:已点餐,未制作
     * 2:制作完成,骑手未送货
     * 3:骑手送货成功,可以开始干饭了
     */
    private static int state = 0; // 这个就是共享数据

    private static final Object lock = new Object();

    public static void main(String[] args) {

        // 商家
        Thread t1 = new Thread(()->{
            synchronized (lock){
                while (state < 1){// 没人点餐就等待(需要循环等待,使用 if 会导致虚假唤醒问题(notifyAll 同时唤醒了骑手和买家,但是买家抢到了锁))
                    System.out.println(Thread.currentThread().getName()+":没人点餐,等待中");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                // 有人点餐就制作
                System.out.println(Thread.currentThread().getName()+":已接单,制作中");
                try {
                    // 模拟商家制作时间
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+":制作成功");
                state = 2;
                lock.notifyAll();// 通知骑手接单(这里notify不能指定唤醒骑手线程所以此处调用notifyAll唤醒所有等待的线程)

            }
        },"商家");

        // 买家
        Thread t2 = new Thread(()->{
            synchronized (lock){
                if(state == 0){
                    state = 1; // 下单
                    System.out.println(Thread.currentThread().getName()+":下单成功,等待送餐");
                    lock.notifyAll();
                    try {
                        lock.wait();// 下单成功后开始等待送餐
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                while (state < 3){// 没有送到,就等待
                    System.out.println(Thread.currentThread().getName()+":外卖没送到,等待中");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                // 送到了,就开始干饭
                System.out.println(Thread.currentThread().getName()+":外卖送到了,开始干饭");
                // 买家结束(无需再次唤醒商家和卖家)
            }

        },"买家");

        // 骑手
        Thread t3 = new Thread(()->{

            synchronized (lock){

                while (state < 1){// 没人点餐,等待
                    System.out.println(Thread.currentThread().getName()+":没人点餐,等待中");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

                while (state < 2){// 没有制作完成,等待
                    System.out.println(Thread.currentThread().getName()+":没有制作完成,等待中");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                // 制作完成了开始送货
                System.out.println(Thread.currentThread().getName()+":接到外卖,送货中");
                try {
                    // 模拟骑手送货
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+":送货完成");
                state = 3;
                lock.notifyAll();// 通知买家收货

            }

        },"骑手");


        t1.start();
        t2.start();
        t3.start();

    }


}

可能的结果之一:

商家:没人点餐,等待中
买家:下单成功,等待送餐
商家:已接单,制作中
商家:制作成功
买家:外卖没送到,等待中
骑手:接到外卖,送货中
骑手:送货完成
买家:外卖送到了,开始干饭

到此,我们的基础回顾就基本差不多了,在此多说一句,接下来的文章,可能需要大家对 JVM 有所了解,比如我们上面提到的虚拟机栈、以及后面我们要提到的 ThreadLocal 的内存泄漏等都需要 JVM 的基础知识,当然在 Future 架构、线程池定义中我们还会涉及到 lamada 表达式、函数式接口等,如果你还没有了解过,可以查看我其他专栏的文章先了解一下。

你可能感兴趣的:(人在江湖之J.U.C,详解,wait/notify,synchronized,锁优化,锁自旋,偏向锁)