普通线程与多线程示意图
通常 系统中运行的程序/软件当做一个进程[迅雷],迅雷里面多个任务看做多个线程。
总结:一个程序一个进程,一个进程可多个线程。线程是CPU调度和执行的的单位。多线程中至少一个为主线程
注意:真正多线程指多个cpu,即 多核。而学习中模拟出的多线程,同一个时间点cpu只执行一个代码,电脑cpu切换的快速,让人有同时执行的错觉。
核心概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
- main() 称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
并行与并发
- 并行:多个CPU同时执行多个任务。 多个人各自建造各自房子 【不同事情
- 并发:一个CPU执行多个任务【采用时间片方式同时进行】 多个人秒杀一件物品 【同一件事
线程创建方式
- 继承Thread类 简单但java是单继承
- 实现Runnable接口 无返回值
- 实现Callable接口 有返回值
继承Thread类
/**
* 多线程的创建,方式一:继承于Thread类
* 1、创建一个继承于Thread类的子类
* 2、重写Thread类的run()方法
* 3、创建Thread类的子类对象
* 4、通过此对象调用start()
* 4.1、启动当前线程
* 4.2、调用当前线程的run()
*/
public class Thread1 {
//线程开启不一定执行,由cpu进行调度,
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {
Thread.sleep(200);
//当前正在执行的线程休眠(暂停执行),但是,该线程不会释放锁
//#注意:此时sleep中资源无锁,synchronized,或lock。结果不影响,因为主线程又不是跟子线程同一份资源
//无sleep,主线程,子线程交互输出。有时,当前主线程暂停,子线程执行完,主线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=0;i<200;i++){
System.out.println("我是主线程"+i);
}
}
}
//主线程 子线程的执行顺序 由cpu进行调度,cpu执行速度很快,一般先执行主线程
class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<50;i++){
System.out.println("我是子线程"+i);
}
}
}
无sleep时,主、子线程交互输出。有了sleep,main方法中当前主线程暂停,子线程执行完,主线程接着执行。
实现Runnable接口(推荐使用) 一份资源多个代理
/**
* 1、创建一个类实现Runnable接口
* 2、实现Runnable中的抽象方法:run-编写方法体/线程执行体
* 3、创建实现类的对象
* 4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5、通过Thread类的对象调用start()
*/
//多线程抢票
public class ticket {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();//同一份资源
//"A"为线程名称
new Thread(myTicket,"A").start();//代理A
new Thread(myTicket,"B").start();//代理B
}
}
class MyTicket implements Runnable {
public int tickets = 100;
@Override
public void run() {
//加上synchronized关键字 结果只会有一个线程执行到1.因为该资源始终占用中
while (tickets>0) {
System.out.println(Thread.currentThread().getName() + "抢到了第" + tickets-- + "张票");
}
}
}
结果正确直到1
实现Callable接口
/**
* 创建线程的方式三: 实现Callable接口 ---JDK1.5新增
* 对比Runnable接口,可有返回值!可抛出异常!支持泛型 implements Callable<Boolean>!借助
* FutureTask类,比如获取返回结果
*/
public class ThreadNew {
public static void main(String[] args) {
NewCall newCall = new NewCall();
FutureTask task = new FutureTask(newCall);
new Thread(task).start();
//不需要返回值可省略
try {
Object sum = task.get();
System.out.println(sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class NewCall implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
- 新建:==当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:==处于新建状态的线程被start()后,将进入线程队列等待cpu时间片,此时它已具备了运行的条件,只是没分配到cpu资源
- 运行:==当就绪的线程被调用并获得cpu资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:==在某种情况下,被人为挂起或执行输入输出操作时,让出cpu并临时中止自己的执行,并进入阻塞状态
- 死亡:==线程完成了它的全部工作,或线程被提前强制性地中止或出现异常导致结束
静态代理
/**
* @Author zz
* @Date 2021/3/12 15:27
*/
//静态代理 多线程也是静态代理原理实现
public class Proxy {
public static void main(String[] args) {
//真实角色
you you = new you();
//代理角色
Marry marry = new WeddingCompany(you);
//代理角色 帮助真实角色实现
marry.marride();
}
}
interface Marry{
//定义一个结婚接口
void marride();
}
//真实角色 结婚 被代理类
class you implements Marry{
@Override
public void marride() {
System.out.println("xxx结婚了");
}
}
//婚庆公司 帮助我实现结婚 婚庆公司可以在结婚前后增加所需要的业务 代理类
class WeddingCompany implements Marry{
//引入真实角色
public Marry target;
public WeddingCompany(Marry target){
this.target=target;
}
@Override
public void marride() {
before();
target.marride();
after();
}
private void before() {
System.out.println("结婚前,进行场地的布置");
}
private void after() {
System.out.println("结婚后,收拾东西回公司");
}
}
结果:
结婚前
xxx结婚了
结婚后
总结:被代理类和代理类都实现同一个接口,或继承同一个类。
静态:程序运行前已存在代理类的字节码文件。/代理类事先定好的
缺点:由于静态代理在代码运行之前就已经存在代理类,因此对于每一个代理对象都需要建一个代理类去代理,当需要代理的对象很多时就需要创建很多的代理类,严重降低程序的可维护性。用动态代理就可以解决这个问题
Thread静态代理底层剖析
Thread底层也是通过静态代理原理实现的,通过我们开启子线程来定义真实角色,Thread为代理角色,Runnable为要实现得到接口,run为接口中的方法
Runnable接口,以及里面的run方法
动态代理
动态代理:代理类在运行过程中产生的,java提供了两种实现动态代理的方式,分别是基于Jdk的动态代理【代理接口】和基于Cglib的动态代理【代理类】。
/**
* @Author zz
* @Date 2021/3/12 18:45
*/
public class ProxyDongTai {
public static void main(String[] args) {
//真实角色
MarryDT realRole = new youDT();
//代理角色 注意这里不是实现接口InvocationHandler
WeddingCompanyDT proxy=new WeddingCompanyDT(realRole);
//生成代理类
//MarryDT proxyRole = (MarryDT) Proxy.newProxyInstance(proxy.class.getClassLoader(), realRole.getClass().getInterfaces(), proxy);
//调用方法 生成代理类
MarryDT proxyRole = (MarryDT)proxy.getProxy();
proxyRole.marride();
}
}
//代理此接口
interface MarryDT {
//定义一个结婚接口
void marride();
}
//真实角色 结婚
class youDT implements MarryDT {
@Override
public void marride() {
System.out.println("xxx结婚了");
}
}
//动态代理类 要实现InvocationHandler接口
class WeddingCompanyDT implements InvocationHandler {
//引入接口 真实对象
public MarryDT target;
public WeddingCompanyDT(MarryDT target) {
this.target = target;
}
//生成得到的代理类
// loader 类加载器,真实对象类加载器即可
// interfaces 代码要用来代理的接口,真是对象实现的接口
// InvocationHandler 代理对象的调用处理程序
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
//proxy : 代理对象
//method : 对应于在代理对象上调用的接口方法的 Method 实例
//args : 代理对象调用接口方法时传递的实际参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
method.invoke(target,args);
after();
return null;
}
private void after() {
System.out.println("结婚后");
}
private void before() {
System.out.println("结婚前");
}
}
结果:
结婚前
xxx结婚了
结婚后
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
- loader:一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载
- interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,代理类就可以调用接口中声明的所有方法。【返回一个
Class<?>[]
数组,数组中包含了该对象实现的所有接口的Class
对象】- h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用[实际执行者]
//1.被代理类
public class SampleClass {
public void test(){
System.out.println("hello world");
}
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
//2.MethodInterceptor 拦截器实现 匿名实现可另写一个类实现
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("before method run...");
Object result = proxy.invokeSuper(obj, args);
System.out.println("after method run...");
return result;
}
});
SampleClass sample = (SampleClass) enhancer.create();
//3.代理对象调用方法
sample.test();
}
}
JDK动态代理和cglib字节码生成的区别?
1、JDK动态代理只能对实现了接口的类生成代理,而不能针对类
2、Cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要生成final,对于final类或方法,是无法继承的
线程安全
1.synchronized 关键字
synchronized是Java中最基本的同步机制之一,它通过在代码块或方法上添加synchronized关键字来实现线程的同步和互斥
线程安全 | synchronized可以确保多个线程在访问共享资源时不会发生冲突 |
互斥访问 | 同一时刻只能有一个线程访问共享资源。 原子性 |
可重入性 | 同一个线程可以多次获得同一把锁,避免死锁 |
内置锁 | 每个java对象有个内置锁,理解为synchronized 用的对象锁【对象的内置锁】如:this,对象本身 |
注意:线程安全问题都是由全局变量及静态变量引起的。只读操作,默认线程安全。有多个线程执行写操作,要考虑线程同步!为啥不是局部变量?因为局部变量不能共享,每次调用方法都是独立的。 |
同步代码块:
synchronized (锁对象\任意对象) { 可能会产生线程安全问题的代码 } 如: //定义锁对象 Object lock = new Object(); @Override public void run() { //模拟卖票 while(true){ //同步代码块 synchronized (lock){ if (ticket > 0) { //模拟电影选坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--); } } } }
同步方法:
public synchronized void method(){ 可能会产生线程安全问题的代码 } 如: Object lock = new Object(); @Override public void run() { //模拟卖票 while(true){ //同步方法 method(); } } //同步方法,锁对象this 注意:锁对象是本类的对象 即 this public synchronized void method(){ if (ticket > 0) { //模拟选坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--); } }
静态同步方法:
public static synchronized void method(){ 可能会产生线程安全问题的代码 } 注意:锁对象是 类名.class ,类对象唯一,保证同一个锁。
解释下synchronized实现原理:
Java对象头中,有两个标志位用于存储synchronized锁的信息:一个是表示当前对象是否被锁定的标志位,另一个是表示持有锁的线程的标识符。
当一个线程尝试获得一个被synchronized锁保护的资源时,JVM会首先检查该对象的锁标志位。如果锁标志位为0,表示该对象没有被锁定,JVM会将锁标志位设置为1,并将持有锁的线程标识符设置为0。如果锁标志位为1,表示该对象已经被其他线程锁定,当前线程会进入阻塞状态,等待其他线程释放锁
2.Lock接口
我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:
//创建Lock锁对象 private final Lock ck = new ReentrantLock(); @Override public void run() { //模拟卖票 while (true) { //synchronized (lock){ 此处不再用synchronized 关键字,改为下列代码 ck.lock(); // 获取/加上锁 if (ticket > 0) { //模拟选坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--); } ck.unlock();// 释放锁 此处锁为ck对象 //} } }
3.两者比较——synchronized与Lock的对比
synchronized | Lock |
隐式锁,出了作用域、遇到异常等自动解锁 | 显示锁,手动开启和关闭锁 |
有代码块锁和方法锁 | 只有代码块锁 |
使用起来简单 | JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类) |
要么读写都加,要么不加 | 可以对读不加锁,对写加锁 |
不能 | 从sleep的线程中抢到锁 |
建议使用优先级:Lock > 同步代码块 > 同步方法(静态) |
4.同步方法与同步代码块效率
取决于具体应用的场景 和 锁的粒度
-
同步方法:
- 当整个方法需要同步时,这是一个方便的选择。
- 使用 synchronized 关键字锁定整个方法意味着任何调用该方法的线程都会获得同一个锁。因此,如果方法内部的所有操作都需要同步,那么使用同步方法会更简单。
-
同步代码块:
- 如果在方法内只有一部分代码需要同步,而其他部分不需要,那么使用同步代码块可能会更有效率。
- 同步代码块允许在一个方法内部锁定较小的代码段,这样可以减小锁的范围,增加并发度,减少线程竞争。
总结:整个方法需要同步,用同步方法,否则同步代码块。
5.volatile
关键字
线程有各自的线程栈(Thread Stack),用于存储局部变量、方法参数和调用堆栈等信息
1. 线程的可见性:volatile
确保了被修饰的变量对所有线程的可见性【当一个线程A修改了一个 volatile
变量的值时\共享变量,另外一个线程B能读到这个修改的值,而不会使用B线程本地的缓存值。】
2.顺序一致性:禁止指令重排序
public class VolatileExample {
private volatile boolean flag = false; // 使用 volatile 修饰变量
public void toggleFlag() {
flag = !flag; // 切换标志的值
}
public void printFlag() {
System.out.println("Flag: " + flag); // 输出标志的值
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
// 线程 A 不断切换标志的值
Thread threadA = new Thread(() -> {
while (true) {
example.toggleFlag();
}
});
// 线程 B 持续打印标志的值
Thread threadB = new Thread(() -> {
while (true) {
example.printFlag();
}
});
threadA.start();
threadB.start();
}
}
// 不加volatile,某些情况下,线程 B 可能会看不到线程 A 对 flag 的修改,输出的是线程B的缓存值
6.Volatile 和 Synchronized比较
- Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
- Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码。
- Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
- volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。
多线程并发CAS技术
概念:CAS全称是 Compare and swap 直译过来就是 比较并交换
作用:用于实现多线程同步的原子操作。也就是说 CAS 是可以保证线程安全的!
在 Java 中,java.util.concurrent.atomic
包提供了一系列基于 CAS 的原子类,例如 AtomicInteger
、AtomicLong
、AtomicReference
等,它们可以在多线程环境下安全地执行自增、自减、设置值等操作,而无需使用显式的锁。 【暂时理解为前面购票不是用的 synchronized或 Lock显示锁吗,现在不用了给变量 int ticket 改为 private static AtomicInteger ticket= new AtomicInteger(100); 】
逻辑:
假设内存中的原数据为 V,旧的预期值是 A,需要修改的新值是 B (A和B是寄存器中的值)
-
比较 A 与 V 是否相等 (compare)
-
如果 A 与 V 相等,就将 B 写入 V (swap),不相等则无事发生
-
返回当前的操作是否成功
public class Test {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
// 第一次执行结果: 64628
// 第二次执行结果: 62853
// 第三次执行结果: 53330
第一种想法: private static volatile int count = 0;
结果 错误!前面说过了 count++; 这种形式包含了读取、递增和写回三个步骤,不保证线程安全
第二种想法: synchronized关键字 将count++操作包起来,锁为类对象
synchronized (Test.class) {
count++;
}
结果 正确!
第三种做法:使用 Java 提供的原子类保证上述代码的原子性
贴部分代码:\\
private static AtomicInteger count = new AtomicInteger(0); // 初始值给 0
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
count.incrementAndGet();
}
});
原子类这里的实现,就是每次修改 value(内存) 之前,都会确认一下要修改的值是否改变了!
CAS 典型 ABA 问题
CAS 在运行中的核心就是检查内存中V 和 预期值A是否一致,如果一致就视为 V中途没有被改变,所以就可以进行下一步的交换操作【将B写入V】。
但是,在并发环境中。它指的是在一个 CAS 操作中,由于目标值在操作过程中被改变了两次,最终值与预期值一致,但实际上可能发生了意外的变化。
举个例子来说明 ABA 问题:
假设有一个初始值为 A 的共享变量,线程 T1 首先读取到了这个值为 A,然后 T1 将其改为了 B,但是又将其改回了 A,此时线程 T2 也来进行 CAS 操作,它发现目标值仍然是 A,于是认为这个值没有被其他线程改变过,于是成功地进行了交换。在这个过程中,虽然最终值仍然是 A,符合预期,但实际上这个共享变量的状态已经发生了变化。
为了解决 ABA 问题,可以使用版本号或者标记位来标识变量的变化情况。例如,Java 中的 AtomicStampedReference
类就是针对 ABA 问题的一种解决方案,它不仅保存了要操作的对象的引用,还保存了一个版本号,当进行 CAS 操作时,不仅比较值是否相等,还要比较版本号是否相等,以此来避免 ABA 问题的发生。
乐观锁/悲观锁
乐观锁:假设并发访问的概率较低,因此它在读取数据时不会对数据加锁,而是在更新数据时检查是否有其他事务已经修改了数据。【乐观锁会在数据表中引入一个版本号或时间戳字段,当更新数据时,它会比较当前读取到的版本号或时间戳与更新时的版本号或时间戳是否一致,如果一致,则执行更新操作,否则认为数据已经被修改,需要进行冲突解决】
悲观锁:假设并发访问的概率较高,因此在读取数据时就会将数据加锁,以防止其他事务同时修改数据。【数据库的锁机制来实现,比如在读取数据时使用 SELECT ... FOR UPDATE 语句,该语句会在读取数据的同时将数据行加锁,直到事务结束才释放锁定。】
死锁
概念:两个或多个进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象。
【死锁发生在多个进程互相竞争有限资源的情况下。】
synchronzied(A锁){ synchronized(B锁){ } } 死锁的产生有四个必要条件,它们被称为死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:资源只能由占有者主动释放,不能被强行剥夺。
- 循环等待条件:多线程间形成一种头尾相接的循环等待资源关系。
等待唤醒机制
解释:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制所涉及到的方法:
- wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
- notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。【线程池中的线程具备执行资格】
- notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
注意:上述三个方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确方法操作的到底是哪个锁上的线程。
为什么这些操作线程的方法定义在Object类中?
因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。
案例:
如上图说示,输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:
- 1.当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
- 2.当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。
package thread;
//模拟资源类
class Resource {
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name, String sex) {
if (flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 设置成员变量
this.name = name;
this.sex = sex;
// 设置之后,Resource中有值,将标记该为 true ,
flag = true;
// 唤醒output
this.notify();
}
public synchronized void out() {
if (!flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出线程将数据输出
System.out.println("姓名: " + name + ",性别: " + sex);
// 改变标记,以便输入线程输入数据
flag = false;
// 唤醒input,进行数据输入
this.notify();
}
}
//输入线程任务类
class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int count = 0;
while (true) {
if (count == 0) {
r.set("小明", "男生");
} else {
r.set("小花", "女生");
}
// 在两个数据之间进行切换
count = (count + 1) % 2;
}
}
}
//输出线程任务类
class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}
//测试类
public class ThreadDemo07 {
public static void main(String[] args) {
// 资源对象
Resource r = new Resource();
// 任务对象
Input in = new Input(r);
Output out = new Output(r);
// 线程对象
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
// 开启线程
t1.start();
t2.start();
}
}
多线程sleep、yield、wait、join方法的使用和区别
sleep:在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。即当前线程进入阻塞状态,在指定时间内不会执行。【注意一点,不释放锁针对同一资源才有用】
yield:使当前线程让出CPU,给其它线程执行的机会,至于下次执行的线程是哪个,取决于线程调度机制,完全有可能还是原来的线程。即Thread.yield()使当前线程从执行状态(运行状态)变为可执行态(就绪状态
wait:在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。【当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。
唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。】
join:线程之间的并行执行变为串行执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行
public class ThreadExample {
public static void main(String[] args) {
final Object lock = new Object(); // 用于 wait 和 notify 的锁对象
// 线程 a 使用 sleep 方法
Thread a = new Thread(() -> {
try {
System.out.println("Thread a is starting...");
Thread.sleep(2000); // 线程 a 暂停 2 秒钟
System.out.println("Thread a is done sleeping.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 线程 b 使用 yield 方法
Thread b = new Thread(() -> {
System.out.println("Thread b is starting...");
Thread.yield(); // 线程 b 让出 CPU 时间片
System.out.println("Thread b is done yielding.");
});
// 线程 c 使用 wait 和 notify 方法
Thread c = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread c is starting...");
try {
lock.wait(); // 线程 c 进入等待状态,直到被唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread c is done waiting.");
}
});
// 线程 d 使用 join 方法
Thread d = new Thread(() -> {
System.out.println("Thread d is starting...");
try {
a.join(); // 线程 d 等待线程 a 执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread d is done joining.");
});
a.start();
b.start();
c.start();
d.start();
}
}
volatile关键字
多线程详解(完结)_csdn 多线程_Chaffee_的博客-CSDN博客