并发编程笔记

并发编程笔记

本博客根据黑马java并发编程教程学习而做的笔记,链接如下

一、基本概念

1、进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
  • Java 中,线程作为小调度单位,进程作为资源分配的小单位。 在 windows 中进程是不活动的,只是作 为线程的容器

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 进程拥有共享的资源,如内存空间等,供其内部的线程共享
    • 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

进程和线程的切换

上下文切换

内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。包括以下内容:

  • 通用目的寄存器
  • 浮点寄存器
  • 程序计数器
  • 用户栈
  • 状态寄存器
  • 内核栈
  • 各种内核数据结构:比如描绘地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表

进程切换和线程切换的主要区别

最主要的一个区别在于进程切换涉及虚拟地址空间的切换而线程不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换

页表查找是一个很慢的过程,因此通常使用cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是快表TLB(translation Lookaside Buffer,用来加速页表查找)。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换快

而且还可能出现缺页中断,这就需要操作系统将需要的内容调入内存中,若内存已满则还需要将不用的内容调出内存,这也需要花费时间

为什么TLB能加快访问速度

快表可以避免每次都对页号进行地址的有效性判断。快表中保存了对应的物理块号,可以直接计算出物理地址,无需再进行有效性检查

2、并发与并行

并发是一个CPU在不同的时间去不同线程中执行指令。

并行是多个CPU同时处理不同的线程。

引用 Rob Pike 的一段描述:

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

3、应用

应用之异步调用(案例1)

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步
  1. 设计
    多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…

  2. 结论

  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞
  • tomcat 的工作线程 ui 程序中,开线程进行其他操作,避免阻塞 ui 线程

结论

  1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
    • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分(参考后文的【阿姆达尔定律】)
    • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

3、悲观锁 乐观锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

乐观锁 悲观锁是一种思想。可以用在很多方面。比如数据库方面。

  • 悲观锁就是for update(锁定查询的行)
  • 乐观锁就是 version字段(比较跟上一次的版本号,如果一样则更新,如果失败则要重复读-比较-写的操作。)

JDK方面:

  • 悲观锁就是sync
  • 乐观锁就是原子类(内部使用CAS实现)

本质来说,就是悲观锁认为总会有人抢我的。乐观锁就认为,基本没人抢。

CAS 乐观锁

乐观锁是一种思想,即认为读多写少,遇到并发写的可能性比较低,所以采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
CAS顶多算是乐观锁写那一步操作的一种实现方式罢了,不用CAS自己加锁也是可以的。

ABA 问题

ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A,当前线程的CAS操作无法分辨当前V值是否发生过变化。

参考:

Java CAS 和ABA问题

乐观锁的业务场景及实现方式

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

二、线程的创建

1、创建一个线程(非主线程)

方法一:通过继承Thread创建线程

public class CreateThread {
	public static void main(String[] args) {
		Thread myThread = new MyThread();
        // 启动线程
		myThread.start();
	}
}

class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println("my thread running...");
	}
}

使用继承方式的好处是,在 run() 方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码

方法二:使用Runnable配合Thread(推荐)

public class Test2 {
	public static void main(String[] args) {
		//创建线程任务
		Runnable r = new Runnable() {
			@Override
			public void run() {
				System.out.println("Runnable running");
			}
		};
		//将Runnable对象传给Thread
		Thread t = new Thread(r);
		//启动线程
		t.start();
	}
}

或者

public class CreateThread2 {
   private static class MyRunnable implements Runnable {

      @Override
      public void run() {
         System.out.println("my runnable running...");
      }
   }

   public static void main(String[] args) {
      MyRunnable myRunnable = new MyRunnable();
      Thread thread = new Thread(myRunnable);
      thread.start();
   }
}

通过实现Runnable接口,并且实现run()方法。在创建线程时作为参数传入该类的实例即可

方法二的简化:使用lambda表达式简化操作

当一个接口带有@FunctionalInterface注解时,是可以使用lambda来简化操作的

所以方法二中的代码可以被简化为

public class Test2 {
	public static void main(String[] args) {
		//创建线程任务
		Runnable r = () -> {
            //直接写方法体即可
			System.out.println("Runnable running");
			System.out.println("Hello Thread");
		};
		//将Runnable对象传给Thread
		Thread t = new Thread(r);
		//启动线程
		t.start();
	}
}

可以再Runnable上使用Alt+Enter

img

原理之 Thread 与 Runnable 的关系

分析 Thread 的源码,理清它与 Runnable 的关系
小结

  • 方法1 是把线程和任务合并在了一起
  • 方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

方法三:使用FutureTask与Thread结合

使用FutureTask可以用泛型指定线程的返回值类型(Runnable的run方法没有返回值)

public class Test3 {
	public static void main(String[] args) throws ExecutionException, InterruptedException {
        //需要传入一个Callable对象
		FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				System.out.println("线程执行!");
				Thread.sleep(1000);
				return 100;
			}
		});

		Thread r1 = new Thread(task, "t2");
		r1.start();
		//获取线程中方法执行后的返回结果
		System.out.println(task.get());
	}
}

public class UseFutureTask {
   public static void main(String[] args) throws ExecutionException, InterruptedException {
      FutureTask<String> futureTask = new FutureTask<>(new MyCall());
      Thread thread = new Thread(futureTask);
      thread.start();
      // 获得线程运行后的返回值
      System.out.println(futureTask.get());
   }
}

class MyCall implements Callable<String> {
   @Override
   public String call() throws Exception {
      return "hello world";
   }
}

总结

使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以


2、原理之线程运行

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈) 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?

  • 其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

3、常用方法

对象实例方法

方法 描述
public void start() 使线程进入就绪状态;Java 虚拟机调用该线程的 run 方法。start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
public void run() 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
public final void setName(String name) 改变线程名称,使之与参数 name 相同。
public final void setPriority(int priority) 更改线程的优先级。java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
public void interrupt() 中断线程。如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
public final boolean isAlive() 测试线程是否处于活动状态。

静态方法

方法 描述
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

(1)start() vs run()

被创建的Thread对象直接调用重写的run方法时, run方法是在主线程中被执行的,而不是在我们所创建的线程中执行。所以如果想要在所创建的线程中执行run方法,需要使用Thread对象的start方法。

(2)sleep()与yield()

sleep (使线程阻塞) 不会释放锁

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),可通过state()方法查看

  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

  3. 睡眠结束后的线程未必会立刻得到执行

  4. 在死循环中加入sleep可以防止CPU空转, 大大降低CPU占用率

  5. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。如:

    //休眠一秒
    TimeUnit.SECONDS.sleep(1);
    //休眠一分钟
    TimeUnit.MINUTES.sleep(1);
    

yield

让出当前线程,让cpu重新分配,但不一定礼让成功。结果可能仍然再次执行当前线程

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

sleep 和 wait

1.sleep 和 wait 有什么区别?

sleep 和 wait 几乎是所有面试中必问的题,但想完全回答正确似乎没那么简单。

对于 sleep 和 wait 的区别,通常的回答是这样的:

  • wait 必须搭配 synchronize 一起使用,而 sleep 不需要;
  • 进入 wait 状态的线程能够被 notify 和 notifyAll 线程唤醒,而 sleep 状态的线程不能被 notify 方法唤醒;
  • wait 通常有条件地执行,线程会一直处于 wait 状态,直到某个条件变为真,但是 sleep 仅仅让你的线程进入睡眠状态;
  • wait 方法会释放对象锁,但 sleep 方法不会。

但上面的回答显然遗漏了一个重要的区别,在调用 wait 方法之后,线程会变为 WATING 状态,而调用 sleep 方法之后,线程会变为 TIMED_WAITING 状态。

2.wait 能不能在 static 方法中使用?为什么?

不能,因为 wait 方法是实例方法(非 static 方法),因此不能在 static 中使用,源码如下:

public final void wait() throws InterruptedException { wait(0);}3.wait/notify 可以不搭配 synchronized 使用吗?为什么?

不行,因为不搭配 synchronized 使用的话程序会报错,如下图所示:

img

更深层次的原因是因为不加 synchronized 的话会造成 Lost Wake-Up Problem,唤醒丢失的问题,详情可见:https://juejin.im/post/5e6a4d8a6fb9a07cd80f36d1

总结

我们通过 synchronized 锁定同一对象,来测试 wait 和 sleep 方法,再通过执行结果的先后顺序证明:wait 方法会释放锁,而 sleep 方法并不会


线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

  • 默认优先级是5

  • 设置方法:

    thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高
    

(3)join()方法

底层原理就是wait,

用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。

如在主线程中调用ti.join(),则是主线程等待t1线程结束

Thread thread = new Thread();
//等待thread线程执行结束
thread.join();   //waiting
//最多等待1000ms,如果1000ms内线程执行完毕,则会直接执行下面的语句,不会等够1000ms
thread.join(1000);

(4)interrupt()方法

用于打断阻塞(sleep wait join…)的线程。 处于阻塞状态的线程,CPU不会给其分配时间片。

  • 如果是打断 运行中的线程打断标记会被置为true
  • 如果是打断 因sleep wait join方法而被阻塞的线程,其实是先获取打断标记为truesleep wait join会消耗掉打断标记,将打断标记置为false, 即清除打断标记,取消阻塞。如果有异常捕获,则进入异常代码块。
  • 捕捉到InterruptedException会清除打断标记,避免影响下一次打断
//打断线程,仅仅是将打断标记置为true而已,线程并不会停止
t1.interrupt();
//用于查看打断标记,返回值被boolean类型,   不会清除打断标记
t1.isInterrupted();
//用于查看打断标记,返回值被boolean类型,   会清除打断标记,也就是将打断标记置为false
t1.interrupted();

正常运行的线程在被打断后不会停止,会继续执行。如果要让线程在被打断后停下来,需要使用打断标记来判断

while(true) {
    if(Thread.currentThread().isInterrupted()) {
        break;
    }
}
interrupt方法的应用——两阶段终止模式

当我们在执行线程一时,想要终止线程二,这是就需要使用interrupt方法来优雅的停止线程二。

img

代码

public class Test7 {
	public static void main(String[] args) throws InterruptedException {
		Monitor monitor = new Monitor();
		monitor.start();
		Thread.sleep(3500);
		monitor.stop();
	}
}

class Monitor {

	Thread monitor;

	/**
	 * 启动监控器线程
	 */
	public void start() {
		//设置线控器线程,用于监控线程状态
		monitor = new Thread() {
			@Override
			public void run() {
				//开始不停的监控
				while (true) {
                    //判断当前线程是否被打断了
					if(Thread.currentThread().isInterrupted()) {
						System.out.println("处理后续任务");
                        //终止线程执行
						break;
					}
					System.out.println("监控器运行中...");
					try {
						//线程休眠
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
						//如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记为true
						Thread.currentThread().interrupt();
					}
				}
			}
		};
		monitor.start();
	}

	/**
	 * 	用于停止监控器线程
	 */
	public void stop() {
		//打断线程
		monitor.interrupt();
	}
}

(5)LockSupport.park();

  • 作用和sleep()一样, 只能在打断标记为false的时候生效
  • 打断 park 线程, 但是不会清空打断状态,因为不需要捕捉异常
  • 如果 打断标记已经为true ,则LockSupport.park();会失效,可以使用 Thread.interrupted() 清除打断状态为false
private static void test3() throws InterruptedException {
 	Thread t1 = new Thread(() -> {
 		log.debug("park...");
 		LockSupport.park();
 		log.debug("unpark...");
 		log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 	}, "t1");
 	t1.start();
	sleep(0.5);
	t1.interrupt();
}

输出:
21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark... 
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true

(6)不推荐使用的打断方法(都已经废弃的方法)

  • stop方法停止线程运行(会真正的杀死线程,可能造成被锁住的共享资源无法被释放,再也没有机会释放锁,其他线程将永远无法获取锁,无法使用这些共享资源)
  • suspend(暂停线程)/resume(恢复线程)方法

(7)守护线程

  • 当JAVA进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,JAVA进程才会结束。
  • 但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。
//将线程设置为守护线程, 默认为false
monitor.setDaemon(true);

守护线程的应用

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求

4、线程的状态(生命周期)

(1)五种状态

这是从 操作系统 层面来描述的

img

【初始状态】

  • 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

【可运行状态】

  • (就绪状态)当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

【运行状态】

  • 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

【阻塞状态】

  • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】,但是显示为Running
  • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
  • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

【终止状态】

  • 表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

(2)六种状态

这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态

img
  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。
  • TERMINATED 当线程代码运行结束

5、查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

linux

  • ps -fe 查看所有进程
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID> 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程
  • jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置

  • 需要以如下方式运行你的 java 类

    java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
    Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=(是否安全连接) -
    Dcom.sun.management.jmxremote.authenticate=(是否认证) (java类)  
    
  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名

如果要认证访问,还需要做如下步骤

  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

三、共享模型之管程

1、共享带来的问题

java中的共享问题体现

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
     Thread t1 = new Thread(() -> {
     for (int i = 0; i < 5000; i++) {
     	counter++;
     	}
     }, "t1");
     Thread t2 = new Thread(() -> {
     for (int i = 0; i < 5000; i++) {
     	counter--;
     	}
     }, "t2");
     t1.start();
     t2.start();
     t1.join();
     t2.join();
     log.debug("{}",counter);
}

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

(1)临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
    例如,下面代码中的临界区
static int counter = 0;
 
static void increment() 
// 临界区 
{   
    counter++; 
}
 
static void decrement() 
// 临界区 
{ 
    counter--; 
}

(2)竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

2、synchronized 解决方案

保证锁住的部分的原子性

(1)解决手段

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住(blocked)。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

即使拥有锁的线程时间片用完,由于锁住的代码没有运行完,不会释放锁,而是等待下次分配到时间片,执行完后才会释放锁。

(2)synchronized语法

synchronized(对象) {
	//临界区
}

例:

static int counter = 0; 
//创建一个公共对象,作为对象锁的对象
static final Object lock = new Object();
 
public static void main(String[] args) throws InterruptedException {    
	Thread t1 = new Thread(() -> {        
    for (int i = 0; i < 5000; i++) {            
        synchronized (lock) {     
        counter++;            
       	 }       
 	   }    
    }, "t1");
 
    Thread t2 = new Thread(() -> {       
        for (int i = 0; i < 5000; i++) {         
            synchronized (lock) {            
            counter--;          
            }    
        } 
    }, "t2");
 
    t1.start();    
    t2.start(); 
    t1.join();   
    t2.join();    
    log.debug("{}",counter); 
}

(3)synchronized加在方法上

  • 加在成员方法上,锁住的是实例对象this

    public class Demo {
    	//在方法上加上synchronized关键字
    	public synchronized void test() {
    	
    	}
    	//等价于
    	public void test() {
    		synchronized(this) {
    		
    		}
    	}
    }
    
  • 加在静态方法上, 锁住的是类对象xxx.class

    public class Demo {
    	//在静态方法上加上synchronized关键字
    	public synchronized static void test() {
    	
    	}
    	//等价于
    	public void test() {
    		synchronized(Demo.class) {
    		
    		}
    	}
    }
    

(4)面向对象改进

把需要保护的共享变量放入一个类

package test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.SynchronizedTest")
public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();  //等待t1结束
        t2.join();  //等待t2结束
        log.debug("count: {}", room.get());
    }
}

/**
 * 对value和自增自减操作进行保护
 */
class Room {
    int value = 0;

    public void increment() {
        synchronized (this) {
            value++;
        }
    }

    public void decrement() {
        synchronized (this) {
            value--;
        }
    }

    public int get() {
        synchronized (this) {
            return value;
        }
    }
}
elAdmin- 2022-03-13 18:16:07:737 [main] DEBUG c.SynchronizedTest - count: 0

(5)所谓的 线程八锁 面试题

其实就是考察 synchronized 锁住的是哪个对象

分析清楚哪个线程抱着哪吧锁就行,对象锁(可以有很多把锁)和类锁(只有一把锁)

3、变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量的引用则未必 (要看该对象是否被共享且被执行了读写操作)

    • 如果该对象没有逃离方法的作用范围,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全,比如return
  • 局部变量是线程安全的——每个方法都在对应线程的栈中创建栈帧,不会被其他线程共享

  •   public static void test1() {
           int i = 10;
           i++; 
      }
    
    public static void test1();
         descriptor: ()V
         flags: ACC_PUBLIC, ACC_STATIC
         Code:
         stack=1, locals=1, args_size=0
         0: bipush 10
         2: istore_0
         3: iinc 0, 1           #局部变量自增是原子操作
         6: return
         LineNumberTable:
         line 10: 0
         line 11: 3
         line 12: 6
         LocalVariableTable:
         Start Length Slot Name Signature
         3 		4 		0 	i 		I
    
img
  • 如果调用的对象被共享,且执行了读写操作,则线程不安全

  •   class ThreadUnsafe {
          //对象实例的局部变量
       	ArrayList<String> list = new ArrayList<>();
       	public void method1(int loopNumber) {
       	for (int i = 0; i < loopNumber; i++) {
       		// { 临界区, 会产生竞态条件
       		method2();
      	 	method3();
      	 	 // } 临界区
      	 }
      	 }
       private void method2() {
       	list.add("1");
      	}
       private void method3() {
       	list.remove(0);
      	}
      }
    

    执行

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
     	ThreadUnsafe test = new ThreadUnsafe();
     	for (int i = 0; i < THREAD_NUMBER; i++) {
     		new Thread(() -> {
     			test.method1(LOOP_NUMBER);
     		}, "Thread" + i).start();
     }
    }
    

    其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

    Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
         at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
         at java.util.ArrayList.remove(ArrayList.java:496) 
         at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
         at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
         at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
         at java.lang.Thread.run(Thread.java:748)
    

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同
img
  • 如果是局部变量,则会在堆中创建对应的对象,不会存在线程安全问题。

  •   static final int THREAD_NUMBER = 2;
      static final int LOOP_NUMBER = 200;
      public static void main(String[] args) {
       	ThreadUnsafe test = new ThreadUnsafe();
       	for (int i = 0; i < THREAD_NUMBER; i++) {   //启用200个线程并分别调用method1
       		new Thread(() -> {
       			test.method1(LOOP_NUMBER);
       		}, "Thread" + i).start();
       }
      }
      
      class ThreadSafe {
       	public final void method1(int loopNumber) {
              //method1的局部变量
       		ArrayList<String> list = new ArrayList<>();
       		for (int i = 0; i < loopNumber; i++) {
       			method2(list);
       			method3(list);
       		}
       	}
       private void method2(ArrayList<String> list) {
       	list.add("1");
       }
       private void method3(ArrayList<String> list) {
       	list.remove(0);
       }
      }
    
img

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
 	ThreadUnsafe test = new ThreadUnsafe();
 	for (int i = 0; i < THREAD_NUMBER; i++) {   //启用200个线程并分别调用method1
 		new Thread(() -> {
 			test.method1(LOOP_NUMBER);
 		}, "Thread" + i).start();
 }
}

class ThreadSafe {
 	public final void method1(int loopNumber) {   //final防止子类重写
 	ArrayList<String> list = new ArrayList<>();
 		for (int i = 0; i < loopNumber; i++) {
 			method2(list);
 			method3(list);  //调用method3时会启用新线程,新线程也能获取本线程的list,即list被暴露给了其他线程
 		}
 	}
 	public void method2(ArrayList<String> list) {
 		list.add("1");
  	}
 	public void method3(ArrayList<String> list) {
 		list.remove(0);
 	}
}

class ThreadSafeSubClass extends ThreadSafe{
 	@Override
 	public void method3(ArrayList<String> list) {
 		new Thread(() -> {
 			list.remove(0);
 		}).start();
 	}
}

从这个例子可以看出 privatefinal 提供【安全】的意义所在,请体会开闭原则中的【闭】


常见线程安全类

  • String
  • Integer
  • StringBuffer StringBuilder线程不安全,所以效率高
  • Random
  • Vector (List的线程安全实现类)
  • Hashtable (Hash的线程安全实现类)
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

Hashtable table = new Hashtable();

new Thread(()->{
 	table.put("key", "value1");
}).start();

new Thread(()->{
 	table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的(都被加上了synchronized
  • 但注意它们多个方法的组合不是原子的,所以可能会出现线程安全问题
img

不可变类线程安全性

  • String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

  • 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

  • 这是因为这些方法的返回值都创建了一个新的对象,而不是直接改变String、Integer对象本身。


实例分析

例1:

public class MyServlet extends HttpServlet {     //默认是单例的,局部变量能被共享
 	// 是否安全?    不是,HashTable才是线程安全的
 	Map<String,Object> map = new HashMap<>();
 	// 是否安全?     是
 	String S1 = "...";
 	// 是否安全?	 是
 	final String S2 = "...";
 	// 是否安全?     不是,其他线程可以修改
 	Date D1 = new Date();
 	// 是否安全?     不是,虽然D2的引用值不能变,但是Date内其他的属性可以变,年月日
 	final Date D2 = new Date();
 
 	public void doGet(HttpServletRequest request, HttpServletResponse response) {
  		// 使用上述变量
 	}
}

例2:

public class MyServlet extends HttpServlet {   //单例
 	// 是否安全?    不是userService中的count可以被共享
 	private UserService userService = new UserServiceImpl();
 
 	public void doGet(HttpServletRequest request, HttpServletResponse response) {
 		userService.update(...);
 	}
}

public class UserServiceImpl implements UserService {
 	// 记录调用次数
 	private int count = 0;
 
 	public void update() {
 	// ...
 	count++;
 	}
}

例3:

@Aspect
@Component
public class MyAspect {   //spring没有加额外说明都是单例
 	// 是否安全?    不是,start可以被共享.  解决方式:像end那样做成方法的局部变量
 	private long start = 0L;
 
    //前置通知
 	@Before("execution(* *(..))")
 	public void before() {
 		start = System.nanoTime();
 }
 
    //后置通知
 	@After("execution(* *(..))")
 	public void after() {
 		long end = System.nanoTime();
		System.out.println("cost time:" + (end-start));
 	}
}

例4:

public class MyServlet extends HttpServlet {   //单例
 	// 是否安全    安全,虽然有成员变量被共享,但是由于userDao私有,没有其他地方能修改它,属于不可变
 	private UserService userService = new UserServiceImpl();
 
 	public void doGet(HttpServletRequest request, HttpServletResponse response) {
 		userService.update(...);
 	}
}

public class UserServiceImpl implements UserService {
 	// 是否安全     安全,虽然userDao可以被共享,但是userDao没有成员变量,没有可更改的属性
 	private UserDao userDao = new UserDaoImpl();
 
 	public void update() {
 		userDao.update();
 	}
}

public class UserDaoImpl implements UserDao { 
 	public void update() {
 		String sql = "update user set password = ? where username = ?";
 		// 是否安全     安全,conn是局部变量,不同的线程conn不是同一个
 		try (Connection conn = DriverManager.getConnection("","","")){
 			// ...
 		} catch (Exception e) {
 			// ...
 		}
 	}
}

例5:

public class MyServlet extends HttpServlet {
 	// 是否安全    不是,conn会被共享
 	private UserService userService = new UserServiceImpl();
 
 	public void doGet(HttpServletRequest request, HttpServletResponse response) {
 		userService.update(...);
 	}
}
public class UserServiceImpl implements UserService {
 	// 是否安全     不是,conn会被共享
 	private UserDao userDao = new UserDaoImpl();
 
 	public void update() {
 		userDao.update();
 	}
}
public class UserDaoImpl implements UserDao {
 	// 是否安全    不是,conn会被共享
 	private Connection conn = null;
 	public void update() throws SQLException {
 		String sql = "update user set password = ? where username = ?";
 		conn = DriverManager.getConnection("","","");
 		// ...
 		conn.close();
 	}
}

例6:

public class MyServlet extends HttpServlet {
 	// 是否安全    安全
 	private UserService userService = new UserServiceImpl();
 
 	public void doGet(HttpServletRequest request, HttpServletResponse response) {
 		userService.update(...);
 	}
}

public class UserServiceImpl implements UserService { 
 	public void update() {
        //每次会创建新的userDao
 	UserDao userDao = new UserDaoImpl();
 	userDao.update();
 	}
}

public class UserDaoImpl implements UserDao {
 	// 是否安全    安全,因为每次调用update都是创建新的userDao
 	private Connection = null;
 	public void update() throws SQLException {
 	String sql = "update user set password = ? where username = ?";
 	conn = DriverManager.getConnection("","","");
 	// ...
 	conn.close();
 	}
}

例7:

public abstract class Test {
 
 	public void bar() {
 		// 是否安全   不安全,sdf被foo()暴露出去了
 		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //
 		foo(sdf);
  	}
 
    //抽象方法
 	public abstract foo(SimpleDateFormat sdf);
 
 
 	public static void main(String[] args) {
 		new Test().bar();
 	}
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法,比如:

public void foo(SimpleDateFormat sdf) {
 	String dateStr = "1999-10-11 00:00:00";
 	for (int i = 0; i < 20; i++) {     //这里多线程可以共享sdf
 		new Thread(() -> {
 		try {
 			sdf.parse(dateStr);
 		} catch (ParseException e) {
 			e.printStackTrace();
 			}
 		}).start();
 	}
}

请比较 JDK 中 String 类的实现


买票案例

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) {
        //售票窗口单例,设置总票数2000
        TicketWindow ticketWindow = new TicketWindow(2000);
        //所有线程集合,ArrayList只会在主线程中使用,不会有线程安全问题
        List<Thread> list = new ArrayList<>();
        // 用来存储买出去多少张票,Vector线程安全
        List<Integer> sellCount = new Vector<>();
        //模拟2000人买票
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件   临界区,对共享变量ticketWindow有读写操作
                int count = ticketWindow.sell(randomAmount());
                //模拟买票耗时,造成指令的交错
                try {
                    Thread.sleep(randomAmount());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }

        //等到所有线程结束再统计
        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 买出去的票求和
        log.debug("selled count:{}", sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    //对成员方法加锁,相当于锁是this
    public synchronized int sell(int amount) {
        //临界区,对count有读写操作,需要保护
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

转账案例

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        //两个账户初始金额都是1000
        Account a = new Account(1000);
        Account b = new Account(1000);

        //a向b多次随机转账
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");

        //b向a多次随机转账
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");

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

        //等待两个线程转账结束
        t1.join();
        t2.join();

        // 查看转账2000次后的两个账户的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public void transfer(Account target, int amount) {
        //临界区,涉及到了共享变量的读写,this的money和target的money都是共享变量,都需要保护
        //在transfer加synchronized不行,相当于加在了this,而两个线程的Account对象不是同一个
        //可以两个对象都加锁,但是容易造成死锁	
        //可以把锁加在Account.class上,但是效率不高,目前只有两个线程
        synchronized (Account.class) {
            if (this.money > amount) {
                //this的money共享
                this.setMoney(this.getMoney() - amount);
                //target的money共享
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

4、Monitor概念

Java 对象头

32位虚拟机,Mark Word就是32bits; 64位虚拟机,Mark Word就是64bits;

HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
也就是说 JAVA对象 = 对象头 + 实例数据 + 对象填充。

其中,对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。

对象头 = Mark Word + 类型指针
(未开启指针压缩的情况下)

  • 32位系统中,对象头 = 8 bytes = 64 bits,Mark Word = 4 bytes = 32 bits;
  • 64位系统中,对象头 = 16 bytes = 128bits,Mark Word = 8 bytes = 64 bits ;

32 位虚拟机为例

  • Integer 8 + 4 字节
  • int 4字节

普通对象 Object Header (64 bits) 32 位虚拟机

|--------------------------------------------------------------| 
| 				Object Header (64 bits)						   |
|------------------------------------|-------------------------| 
| Mark Word (32 bits) 				 | Klass Word (32 bits)    |
|------------------------------------|-------------------------|

数组对象 Object Header (96 bits) 32 位虚拟机

|---------------------------------------------------------------------------------|
| 								Object Header (96 bits) 						  |
|--------------------------------|-----------------------|------------------------|
| 		Mark Word(32bits)		 | 	 Klass Word(32bits)  |   array length(32bits) |
|--------------------------------|-----------------------|------------------------|

其中 Mark Word 结构为 32 位虚拟机

|-------------------------------------------------------|--------------------|
| 					Mark Word (32 bits) 				| 		State		 |
|-------------------------------------------------------|--------------------|
| 	hashcode:25 		| age:4 | biased_lock:0 | 	01  | 		Normal 		 |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2   | age:4 | biased_lock:1 |   01  | 		Biased 		 |
|-------------------------------------------------------|--------------------|
| 				ptr_to_lock_record:30 			|   00  | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| 				ptr_to_heavyweight_monitor:30 	|   10  | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| 												|   11  | 	Marked for GC 	 |
|-------------------------------------------------------|--------------------|

64 位虚拟机 Mark Word

|--------------------------------------------------------------------|--------------------|
| 							Mark Word (64 bits) 			 		 | 		 State 		  |
|--------------------------------------------------------------------|--------------------|
| 	unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 |  01 | 		 Normal 	  |
|--------------------------------------------------------------------|--------------------|
|   thread:54 |   epoch:2   | unused:1 | age:4 | biased_lock:1 |  01 | 		 Biased 	  |
|--------------------------------------------------------------------|--------------------|
| 						ptr_to_lock_record:62 				   |  00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
|					 ptr_to_heavyweight_monitor:62 		  	   |  10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| 														   	   |  11 |    Marked for GC   |
|--------------------------------------------------------------------|--------------------|

(1)原理之Monitor

img
  • 当线程执行到临界区代码时,如果使用了synchronized,会先查询synchronized中所指定的对象(obj)是否绑定了Monitor

    • 如果没有绑定,则会先去去与Monitor绑定,并且将Owner设为当前线程

    • 如果已经绑定,则会去查询该Monitor是否已经有了Owner

      • 如果没有,则Owner与将当前线程绑定
    • 如果有,则放入EntryList,进入阻塞状态(blocked)

  • 当Monitor的Owner将临界区中代码执行完毕后,Owner便会被清空,此时EntryList中处于阻塞状态的线程会被叫醒并竞争,此时的竞争是非公平的

  • 注意

    • 对象在使用了synchronized后与Monitor绑定时,会将对象头中的Mark Word置为Monitor指针。
    • 每个对象都会绑定一个唯一的Monitor,如果synchronized中所指定的对象(obj)不同,则会绑定不同的Monitor

5、Synchronized原理进阶

小故事

故事角色

  • 老王 - JVM
  • 小南 - 线程
  • 小女 - 线程
  • 房间 - 对象
  • 房间门上 - 防盗锁 - Monitor
  • 房间门上 - 小南书包 - 轻量级锁 访问时间错开,无竞争的时候
  • 房间门上 - 刻上小南大名 - 偏向锁 无竞争的时候
  • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值,说明有竞争
  • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

​ 小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。

​ 但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

​ 小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。

​ 后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

​ 于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。

​ 同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字

​ 后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包


对象头格式

img

(1)轻量级锁(用于优化Monitor这类的重量级锁)

轻量级锁使用场景:当一个对象被多个线程所访问,但访问的时间是错开的(不存在竞争),此时就可以使用轻量级锁来优化。

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的mark word(不再一开始就使用Monitor)

    img
  • 让锁记录中的Object reference指向锁对象(Object),并尝试用cas去替换Object中的mark word,将此mark word放入lock record中保存

    • CAS (compare and swap):原子操作,解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。"
img
  • 如果cas替换成功,则将Object的对象头替换为锁记录的地址状态 00(轻量级锁状态),并由该线程给对象加锁
img
  • 如果cas替换失败,有两种情况
    • 如果是本线程执行了synchronized锁重入,那么再添加一条LockRecord作为重入的计数,这时候的cas失败MarkWord保存null
    • 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程。

(2)锁膨胀

  • 如果一个线程在给一个对象加轻量级锁时,cas替换操作失败(因为此时其他线程已经给对象加了轻量级锁),此时该线程就会进入锁膨胀过程
img
  • 此时便会给对象加上重量级锁(使用Monitor)

    • 为Object申请Monitor锁,让Object指向重量级锁地址

    • 将对象头的Mark Word改为Monitor的地址,并且状态改为10(重量级锁)

    • 并且该线程放入入EntryList中,并进入阻塞状态(blocked)

      img
  • 当退出synchronized代码块时(解锁时),如果有取值为Null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1;

  • 当退出synchronized代码块时(解锁时),锁记录不为null。这时使用cas将Mark Word的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀(已经升级为重量级锁),进入重量级锁解锁流程。
      • 根据Monitor地址找到Owner,并置空,再从EntryList换醒其他线程,让其他线程开始竞争锁。

(3)自旋优化

重量级锁竞争时,还可以使用自选来优化,如果当前线程在自旋成功(使用锁的线程退出了同步块,释放了锁),这时就可以避免线程进入阻塞状态。但是会占用cup,适合多核cpu

  • 第一种情况
img
  • 第二种情况,自旋重试失败,进入EntryList阻塞
img
  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋几次甚至不自旋,总之,比较智能。
  • 自旋会占用cpu时间,单核cpu自旋就是浪费,多核CPU自旋才能发挥优势
  • Java7之后不能控制是否开启自旋功能

(4)偏向锁Biased(用于优化轻量级锁 重入)

轻量级锁在没有竞争时,每次重入(该线程执行的方法中再次锁住该对象)操作仍需要cas替换操作,这样是会使性能降低的。

所以引入了偏向锁对性能进行优化:在第一次cas时会将线程的ID写入对象的Mark Word中。此后发现这个线程ID就是自己的,就表示没有竞争,就不需要再次cas,以后只要不发生竞争,这个对象就归该线程所有。

img

偏向状态

  • Normal:一般状态,没有加任何锁,前面62位保存的是对象的信息最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)
  • Biased:偏向状态,使用偏向锁,前面54位保存的当前线程的ID最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)
  • Lightweight:使用轻量级锁,前62位保存的是锁记录的指针,最后两位为状态(00)
  • Heavyweight:使用重量级锁,前62位保存的是Monitor的地址指针,后两位为状态(10)

img

  • 如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101
  • 但是偏向锁默认是有延迟的,不会在程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态
  • 如果没有开启偏向锁,对象的Mark Word后三位应该是001
  • VM参数: -XX: -UseBiasedLocking 禁用偏向锁

synchronized的加锁顺序:优先偏向锁101,然后轻量级锁00,最后重量级锁10


撤销偏向

以下几种情况会使对象的偏向锁失效

  • 调用对象的hashCode方法,会禁用掉对象的偏向锁,因为Mark Word中正常状态Narmal的hashcode有31位,而偏向锁状态Biased存不下hashcode,会转为正常状态。
  • 多个线程使用该对象
  • 调用了wait/notify方法(调用wait方法会导致锁膨胀而使用重量级锁,因为需要把线程加入WaitSet)

(5)批量重偏向

  • 如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向T1的对象仍有机会重新偏向T2
    • 重偏向会重置Thread ID
  • 当撤销达到第20次时(超过阈值),JVM会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程。

(6)批量撤销

当撤销偏向锁到达第40次时,JVM会觉得确实偏向错了,就会将整个类的对象都改为不可偏向的001


(7)锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是在某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

一种极端的情况如下:

public void doSomethingMethod(){
    synchronized(lock){
        //do some thing1
    }
    {
        //do other thing2,做其它不需要同步的工作,但能很快执行完毕
    }
    synchronized(lock){
        //do other thing3
    }
}

上面的代码是有两块需要同步操作的,但在这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后的代码如下:

public void doSomethingMethod(){
    //进行锁粗化:整合成一次锁请求、同步、释放
    synchronized(lock){
        //do some thing1
        //do other thing2,做其它不需要同步的工作,但能很快执行完毕
        //do other thing3
    }
}

注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。

另一种需要锁粗化的极端的情况是:

for(int i=0;i<size;i++){
    synchronized(lock){
    }
}

上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在jdk内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。

锁粗化后的代码如下:

synchronized(lock){
    for(int i=0;i<size;i++){
    }

(8)锁消除


先了解几个概念

  • 动态编译(dynamic compilation)指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫静态编译(static compilation)。
  • JIT编译(just-in-time compilation)狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。
  • 自适应动态编译(adaptive dynamic compilation)也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序“以某种式”先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。

锁消除是发生在编译器级别的一种锁优化方式。有时候我们写的代码完全不需要加锁,却执行了加锁操作。

Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

关闭锁消除

-XX:-EliminateLocks

6、Wait/Notify

为什么要wait?

(1)原理

img
  • 锁对象调用wait方法(obj.wait),就会使当前线程进入WaitSet中,变为WAITING状态。

  • 处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别:

    • BLOCKED状态的线程是在竞争对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态
  • WAITING状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态,会释放锁

  • BLOCKED状态的线程会在Owner线程释放锁的时候被唤醒

  • WAITING状态的线程会在Owner线程调用notify方法(obj.notify/obj.notifyAll),才会被唤醒。

注:只有当对象被锁以后,才能调用wait和notify方法

public class Test1 {
	final static Object LOCK = new Object();
	public static void main(String[] args) throws InterruptedException {
        //只有在对象被锁住后才能调用wait方法
		synchronized (LOCK) {
			LOCK.wait();
		}
	}
}

(2)Wait与Sleep的区别

不同点

  • SleepThread类的静态方法WaitObject的方法,Object又是所有类的父类,所以所有类都有Wait方法。

  • Sleep在阻塞的时候不会释放锁,而Wait在阻塞的时候会释放锁

  • sleep()方法则可以放在任何地方使用。而Wait需要与synchronized一起使用(获得锁以后才能使用

  • sleep()、wait()、notify()、notifyAll()都需要捕获异常

  • 进入 wait 状态的线程能够被 notify 和 notifyAll 线程唤醒,而 sleep 状态的线程不能被 notify 方法唤醒;

  • Thread.Sleep(1000) 不占用CPU,表示用户线程放弃当前的cpu时间片,1秒后参与cpu竞争。sleep(0)是有特殊含义的,表示此时此刻我放弃cpu时间片,别人可以执行,然后马上参与cpu竞争。 如果有锁,则抱着锁睡觉。由于sleep不会释放锁标志,容易导致死锁问题的发生,因此一般情况下,推荐使用wait方法。

  • obj.wait(1000):表示将锁释放1000毫秒,不占用CPU。到时间后如果锁没有被其他线程占用,则再次得到锁,不用竞争。如果锁被其他线程占用,则等待其他线程释放锁。wait(0)和wait()等效,这跟sleep不一样

  • 在调用 wait ()方法之后,线程会变为 WATING 状态,而调用 sleep ()方法之后,线程会变为 TIMED_WAITING 状态。

  • 但是wait(n)sleep(n)的状态都是TIMED_WAITING

API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

(3)优雅地使用wait/notify

STEP1

@Slf4j(topic = "c.TestCorrectPostureStep1")
public class TestCorrectPostureStep1 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        //抱着锁,睡2秒
                        sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        try {
            //一秒后送烟
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            // 这里能不能加 synchronized (room)?
            //不能,加了的话送不进去烟,因为小南还抱着锁
            hasCigarette = true;
            log.debug("烟到了噢!");
        }, "送烟的").start();
    }
}

输出

16:37:10:484 [小南] DEBUG c.TestCorrectPostureStep - 有烟没?[false]
16:37:10:487 [小南] DEBUG c.TestCorrectPostureStep - 没烟,先歇会!
16:37:11:492 [送烟的] DEBUG c.TestCorrectPostureStep - 烟到了噢!         //这里11秒已经送到烟了
16:37:12:488 [小南] DEBUG c.TestCorrectPostureStep - 有烟没?[true]	   //12秒才开始干活
16:37:12:488 [小南] DEBUG c.TestCorrectPostureStep - 可以开始干活了
16:37:12:488 [其它人] DEBUG c.TestCorrectPostureStep - 可以开始干活了
16:37:12:489 [其它人] DEBUG c.TestCorrectPostureStep - 可以开始干活了
16:37:12:489 [其它人] DEBUG c.TestCorrectPostureStep - 可以开始干活了
16:37:12:489 [其它人] DEBUG c.TestCorrectPostureStep - 可以开始干活了
16:37:12:490 [其它人] DEBUG c.TestCorrectPostureStep - 可以开始干活了
  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加
  • synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

STEP2

@Slf4j(topic = "c.TestCorrectPostureStep2")
public class TestCorrectPostureStep2 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        //去休息间等待2秒,让出锁
                        room.wait(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        try {
            //一秒后送烟
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            //notify也需要在同步代码块中运行
            synchronized (room) {
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }
}

输出

16:50:05:155 [小南] DEBUG c.TestCorrectPostureStep2 - 有烟没?[false]
16:50:05:160 [小南] DEBUG c.TestCorrectPostureStep2 - 没烟,先歇会!
16:50:05:160 [其它人] DEBUG c.TestCorrectPostureStep2 - 可以开始干活了
16:50:05:160 [其它人] DEBUG c.TestCorrectPostureStep2 - 可以开始干活了
16:50:05:160 [其它人] DEBUG c.TestCorrectPostureStep2 - 可以开始干活了
16:50:05:160 [其它人] DEBUG c.TestCorrectPostureStep2 - 可以开始干活了
16:50:05:160 [其它人] DEBUG c.TestCorrectPostureStep2 - 可以开始干活了
16:50:06:166 [送烟的] DEBUG c.TestCorrectPostureStep2 - 烟到了噢!
16:50:06:167 [小南] DEBUG c.TestCorrectPostureStep2 - 有烟没?[true]
16:50:06:167 [小南] DEBUG c.TestCorrectPostureStep2 - 可以开始干活了
  • 解决了其它干活的线程阻塞的问题
  • 但如果有其它线程也在等待条件呢?送烟的会不会错误的叫醒其他线程,而不是叫醒小南? 【会】

STEP3

@Slf4j(topic = "c.TestCorrectPostureStep3")
public class TestCorrectPostureStep3 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        //去休息间等待,让出锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }else{
                    log.debug("没干成活");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        //去休息间等待,让出锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        try {
            //一秒后外卖
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                //随便叫醒一个
                room.notify();
            }
        }, "送外卖的").start();
    }
}

输出

16:55:36:081 [小南] DEBUG c.TestCorrectPostureStep3 - 有烟没?[false]
16:55:36:084 [小南] DEBUG c.TestCorrectPostureStep3 - 没烟,先歇会!
16:55:36:085 [小女] DEBUG c.TestCorrectPostureStep3 - 外卖送到没?[false]
16:55:36:085 [小女] DEBUG c.TestCorrectPostureStep3 - 没外卖,先歇会!
16:55:37:085 [送外卖的] DEBUG c.TestCorrectPostureStep3 - 外卖到了噢!
16:55:37:085 [小南] DEBUG c.TestCorrectPostureStep3 - 有烟没?[false]   //外卖到了,但是叫醒的是小南,没有烟
16:55:37:085 [小南] DEBUG c.TestCorrectPostureStep3 - 没干成活
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 没被唤醒的线程会一直等待,线程不会结束
  • 解决方法,改为 notifyAll

STEP4

 new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                //全部叫醒
                room.notifyAll();
            }
        }, "送外卖的").start();

输出

16:59:29:714 [小南] DEBUG c.TestCorrectPostureStep3 - 有烟没?[false]
16:59:29:718 [小南] DEBUG c.TestCorrectPostureStep3 - 没烟,先歇会!
16:59:29:718 [小女] DEBUG c.TestCorrectPostureStep3 - 外卖送到没?[false]
16:59:29:718 [小女] DEBUG c.TestCorrectPostureStep3 - 没外卖,先歇会!
16:59:30:722 [送外卖的] DEBUG c.TestCorrectPostureStep3 - 外卖到了噢!
16:59:30:722 [小南] DEBUG c.TestCorrectPostureStep3 - 有烟没?[false]
16:59:30:722 [小南] DEBUG c.TestCorrectPostureStep3 - 没干成活
16:59:30:723 [小女] DEBUG c.TestCorrectPostureStep3 - 外卖送到没?[true]
16:59:30:723 [小女] DEBUG c.TestCorrectPostureStep3 - 可以开始干活了
  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
  • 解决方法,用 while + wait,当条件不成立,再次 wait

STEP5

@Slf4j(topic = "c.TestCorrectPostureStep3")
public class TestCorrectPostureStep3 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        //去休息间,让出锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }else{
                    log.debug("没干成活");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        //去休息间,让出锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        try {
            //一秒后外卖
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                //全部叫醒
                room.notifyAll();
            }
        }, "送外卖的").start();
    }
}

输出

17:03:51:741 [小南] DEBUG c.TestCorrectPostureStep3 - 有烟没?[false]
17:03:51:745 [小南] DEBUG c.TestCorrectPostureStep3 - 没烟,先歇会!
17:03:51:745 [小女] DEBUG c.TestCorrectPostureStep3 - 外卖送到没?[false]
17:03:51:745 [小女] DEBUG c.TestCorrectPostureStep3 - 没外卖,先歇会!
17:03:52:743 [送外卖的] DEBUG c.TestCorrectPostureStep3 - 外卖到了噢!
17:03:52:743 [小南] DEBUG c.TestCorrectPostureStep3 - 没烟,先歇会!
17:03:52:743 [小女] DEBUG c.TestCorrectPostureStep3 - 外卖送到没?[true]
17:03:52:744 [小女] DEBUG c.TestCorrectPostureStep3 - 可以开始干活了

什么时候适合使用wait

  • 当线程不满足某些条件,需要暂停运行时,可以使用wait。这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程sleep结束后,运行完毕,才能得到执行。

使用wait/notify需要注意什么

  • 当有多个线程在运行时,对象调用了wait方法,此时这些线程都会进入WaitSet中等待。如果这时使用了notify方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用notifyAll方法
synchronized (LOCK) {
	while(//不满足条件,一直等待,避免虚假唤醒) {
		LOCK.wait();
	}
	//满足条件后再运行
}

synchronized (LOCK) {
	//唤醒所有等待线程
	LOCK.notifyAll();

posted @   猫的心情  阅读(90)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示