目录
线程基础知识
并发与并行
进程和线程
线程优先级
创建线程的方式主要有三种
休眠
作出让步
join() 方法
线程协作注意什么
理解线程状态
选择合适的协作工具
共享资源的访问控制
避免竞争条件
创建线程几种方式
线程状态,状态之间切换
新建(New)
就绪(Runnable)
运行(Running)
阻塞(Blocked)
死亡(Dead)
状态之间的切换
停止线程方式
run() 方法 和start() 方法区别
wait、sleep和yield方法的区别
wait
sleep
yield
JMM(JavaMemoryModel)/Java内存模型
1. 主要目的
2. 内存区域划分
3. 数据同步机制
4. 对并发编程的影响
JMM 与 volatile 关键字的关系是什么?
可见性保障
禁止指令重排
不保证原子性
线程基础知识
并发与并行
并发指的是在同一时刻,只有一个线程能够获取到CPU执行任务,而多个线程被快速的轮换执行,这就使得在宏观上具有多个线程同时执行的效果,并发不是真正的同时执行。
并行指的是无论何时,多个线程都是在多个CPU核心上同时执行的,是真正的同时执行。
进程是程序的一次执行过程,它拥有独立的内存空间和系统资源。
而线程是进程中的一个执行单元,一个进程可以包含多个线程。
可以把进程比作一个工厂,工厂(进程)有自己的厂房(独立的内存空间)和各种设备(系统资源)。线程则像是工厂里的工人,多个工人(线程)在这个工厂(进程)里协同工作,共享工厂的资源,共同完成任务。
线程不能单独存在,它必须依存于进程。进程结束,线程也会随之结束。并且,同一进程中的线程可以共享进程的资源,如内存、文件句柄等,这有助于提高程序的运行效率和性能。不过,这也需要注意资源共享可能带来的数据同步等问题。
线程优先级
线程调度器倾向于让优先级较高的线程优先执行,优先级较低的线程只是执行频率较低。
--》优先级高的不一定先执行。
尽管 JDK 有 10 个优先级,但是一般只有「max_priority,norm_priority,min_priority」 三种级别。/praɪˈɒrəti/
在 Java 等编程语言中,线程优先级用于表示线程获取 CPU 资源的优先程度。
线程优先级是一个整数,通常范围是 1 - 10(不同语言或系统可能有所不同),
数字越大表示优先级越高。比如 Java 中,线程优先级分为 10 个等级,默认优先级是 5。
高优先级的线程更有可能比低优先级的线程先获取到 CPU 时间片来运行,但这并不是绝对的。因为线程调度最终还是由操作系统来控制,操作系统的调度算法会综合考虑多种因素。即使一个高优先级的线程处于就绪状态,也有可能因为操作系统的策略(如时间片轮转)而让低优先级的线程先运行。
合理设置线程优先级可以在一定程度上优化程序性能。例如,对于一些对实时性要求高的任务,可以适当提高其线程优先级,像在一个数据采集系统中,将负责采集数据的线程优先级调高,以便它能更及时地获取和处理数据。
最低优先级 1:Thread.MIN_PRIORITY,
最高优先级 10:Thread.MAX_PRIORITY
获取线程优先级: Thread.currentThread().getPriority() /praɪˈɒrəti/
Java 使用 setPriority 方法设置线程优先级,方法签名
public final void setPriority(int newPriority)
子线程默认优先级和父线程一样,
https://zhuanlan.zhihu.com/p/86068193
创建线程的方式主要有三种
- 通过继承 Thread 类来创建线程
- 通过实现 Runnable 接口来创建线程
- 通过 Callable 和 Future 来创建线程
需要配合FutureTask.get() 可以获取放回值
线程的主要创建步骤如下
- 定义一个线程类使其继承 Thread 类,并重写其中的 run 方法,
run 方法内部就是线程要完成的任务,因此 run 方法也被称为 执行体
- 启动方法需要注意,并不是直接调用 run 方法来启动线程,而是使用 start 方法来启动线程。当然 run 方法可以调用,这样的话就会变成普通方法调用,而不是新创建一个线程来调用了。
Callable 接口的好处你已经知道了吧,既能够实现多个接口,也能够得到执行结果的返回值。
Callable 和 Runnable 接口区别?
- Callable 执行的任务有返回值,而 Runnable 执行的任务没有返回值
- Callable(重写)的方法是 call 方法,而 Runnable(重写)的方法是 run 方法。
- call 方法可以抛出异常,而 Runnable 方法不能抛出异常
休眠
影响任务行为的一种简单方式就是使线程休眠,选定给定的休眠时间,
调用它的 sleep() 方法,
一般使用的TimeUnit 这个时间类替换 Thread.sleep() 方法
作出让步
Thread.yield() 是建议执行切换CPU,而不是强制执行CPU切换。
join() 方法
其效果是等待一段时间直到第二个线程结束才正常执行。
如果某个线程在另一个线程 t 上调用 t.join() 方法,此线程将被挂起,直到目标线程 t 结束才回复(可以用 t.isAlive() 返回为真假判断)。
也可以在调用 join 时带上一个超时参数,来设置到期时间,时间到期,join方法自动返回。
对 join 的调用也可以被中断,做法是在线程上调用 interrupted 方法,
这时需要用到 try...catch 子句
线程协作注意什么
在 Java 中线程协作主要涉及多个线程之间的协调工作,有以下注意事项:
理解线程状态
- 线程有多种状态,如新建、就绪、运行、阻塞和死亡。在协作时,要清楚线程在什么状态下才能进行协作,例如线程处于阻塞状态时,可能在等待某个条件满足才能继续执行协作相关的任务。
选择合适的协作工具
- synchronized 和 Object 的 wait/notify/notifyAll 方法:
- 使用 synchronized 关键字保证在同一时刻只有一个线程访问共享资源。在同步代码块或同步方法内部,可以使用对象的 wait 方法让线程等待,直到其他线程调用该对象的 notify 或 notifyAll 方法来唤醒它。注意,wait 方法会释放锁,让其他线程有机会获取锁进入临界区。
- 要正确配对使用 notify 和 wait,避免出现死锁或线程一直等待的情况。通常,notify 只会唤醒一个等待线程,而 notifyAll 会唤醒所有等待线程。
- ReentrantLock 和 Condition 接口:
- ReentrantLock 提供了更灵活的锁机制。通过创建 Condition 对象,可以实现更精细的线程等待和唤醒控制。一个 ReentrantLock 可以绑定多个 Condition 对象,每个 Condition 对象可以管理一组等待线程,这在复杂的多线程协作场景中很有用。
- 与 synchronized 不同,使用 ReentrantLock 和 Condition 时,必须手动释放锁,要注意在 finally 块中正确释放锁,防止死锁。
共享资源的访问控制
- 明确共享资源是什么,并且确保对共享资源的访问是线程安全的。可以使用线程安全的集合类(如 ConcurrentHashMap、CopyOnWriteArrayList 等),或者通过同步机制(如上述的 synchronized 或 ReentrantLock)来保护共享资源。
避免竞争条件
- 竞争条件是指多个线程对共享资源的访问顺序不确定,导致结果不确定。要通过合理的同步和协作机制来消除竞争条件。例如,在多个线程对一个计数器进行操作时,可以使用原子类(如 AtomicInteger)来确保操作的原子性,避免出现数据不一致的情况。
共享资源变量安全。线程安全
共享变量和条件是什么,这是协作的核心。
总结:用synchronized保证原子性,Object wait和notify实现等待唤醒。
notify己方唤醒己方,失败最终导致全部线程阻塞,notifyAll唤醒所有又不够精确
作为改进版,可以使用ReentrantLock的Condition替代synchronized的
wait | notify | notifyAll |
await | signal | signalAll |
ReentrantLock的Condition通过拆分线程等待队列,让线程的等待唤醒更加精确了,想唤醒哪一方就唤醒哪一方。
/riːˈɛntrənt/
场景:controller 成员变量 会出现线程安全问题,放到自己方法内,作用于也在方法内。
@Scope(value = "prototype") // 加上@Scope注解,他有2个取值:
单例-singleton 多实例-prototype
一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的
多线程全面详解总结 - 关键我是你杰哥 - 博客园
创建线程几种方式
- 继承Thread类,重写run方法,掉start方法。
- 实现Runnable接口,重写run方法。
- 实现Callable接口,重写call方法, 有返回值。1,2,3底层都基于实现runnable.
需要配合FutureTask.get() 可以获取放回值
- 用线程池创建。
线程状态,状态之间切换
线程状态 :新建 、就绪/运行、阻塞/等待、结束。
- New (新创建):未启动的线程;
- Runnable (可运行):可运行的线程,需要等待操作系统资源;
- Blocked (被阻塞):等待监视器锁而被阻塞的线程;
- Waiting (等待):等待唤醒状态,无限期地等待另一个线程唤醒;
- Timed waiting (计时等待):在指定的等待时间内等待另一个线程执行操作的线程;
- Terminated (被终止):已退出的线程。 [ˈtɜːmɪneɪtɪd]
新建(New)
- 当创建一个线程对象,但还没调用start()方法时,线程处于新建状态。此时线程只是一个对象实例,系统没有为它分配 CPU 等资源。例如Thread thread = new Thread();就创建了一个处于新建状态的线程。
就绪(Runnable)
- 线程对象调用start()方法后,线程进入就绪状态。此时线程已经获取了除 CPU 时间片之外的所有资源,只要 CPU 调度它,就可以开始运行。比如有多个线程处于就绪状态,操作系统的调度器会按照一定的算法(如时间片轮转等)来决定哪个线程开始执行。
运行(Running)
- 当就绪状态的线程获得 CPU 时间片,开始执行run()方法中的代码时,线程处于运行状态。在这个状态下,线程会执行任务代码。不过运行状态的时间不确定,因为可能会因为时间片用完或者被更高优先级的线程抢占而暂停。
阻塞(Blocked)
- 当线程因为某些原因(如等待 I/O 操作完成、等待获取锁等)暂停执行,就会进入阻塞状态。例如,线程在等待从网络读取数据,或者等待获取一个被其他线程占用的同步锁时,就会阻塞。阻塞状态的线程会让出 CPU 资源,直到阻塞原因解除。
死亡(Dead)
- 线程的run()方法执行完毕或者线程因异常退出,线程就进入死亡状态。此时线程对象可能还存在,但线程已经结束运行,不能再被调度。
状态之间的切换
- 新建 -> 就绪:通过调用线程的start()方法,线程就会从新建状态进入就绪状态。
- 就绪 -> 运行:由操作系统的线程调度器决定将 CPU 时间片分配给就绪状态的线程,使它进入运行状态。
- 运行 -> 就绪:当运行的线程用完时间片,或者有更高优先级的线程进入就绪状态时,操作系统会将当前运行的线程切换回就绪状态,让其他线程运行。
- 运行 -> 阻塞:线程执行过程中遇到需要等待的情况,如等待 I/O 操作、等待锁等,就会从运行状态切换到阻塞状态。
- 阻塞 -> 就绪:当阻塞线程等待的事件完成(如 I/O 操作结束、获取到锁等),线程会从阻塞状态回到就绪状态,等待再次被调度运行。
- 运行 -> 死亡:线程的run()方法正常结束或者线程抛出未捕获的异常导致run()方法终止,线程就从运行状态进入死亡状态。
新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前。 新建状态下,线程压根就没有启动,仅仅只是存在一个线程对象而已.Thread t = new Thread();
此时t就属于新建状态当新建状态下的线程对象调用了start方法,此时从新建状态进入可运行状态.线程对象的start方法只能调用一次,否则报错:IllegalThreadStateException.
例如
可运行(runnable)
分成两种子状态,ready和running。分别表示就绪状态和运行状态。
就绪状态
线程对象调用start方法之后,等待JVM的调度(此时该线程并没有运行),这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行,换句话说线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。
运行状态
线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行
堵塞(blocked)
有一个线程获取了锁未释放,其他线程也来获取,
但发现获取不到锁也进入了被阻塞状态。
被阻塞状态只存在于多线程并发访问下,
区别于后面两种因线程自己进入”等待“而导致的阻塞。
进入状态:1.进入synchronized 代码块/方法。2.未获取到锁
退出状态:获取到监视器锁
等待状态waiting
(等待状态只能被其他线程唤醒):
此时使用的无参数的wait方法,当线程处于运行过程时,调用了wait()方法,此时JVM把当前线程存在对象等待池中.
计时等待状态(timed waiting)
使用了带参数的wait方法或者sleep方法
当线程处于运行过程时,调用了wait(long time)方法,此时JVM把当前线程存在对象等待池中.
当前线程执行了sleep(long time)方法.
终止状态(terminated)
通常称为死亡状态,表示线程终止.
正常执行完run方法而退出(正常死亡).
遇到异常而退出(出现异常之后,程序就会中断)(意外死亡).
JVM 异常结束,所有的线程生命周期均被结束。
线程一旦终止,就不能再重启启动,否则报错(IllegalThreadStateException).
停止线程方式
- 使用退出标志,是线程正常退出; volatile flag,while(flag){}
- interrupt[ˌɪntəˈrʌpt]方法中断线程
打断阻塞线程(sleep、wait、join)的线程,线程会抛出InterruptException异常
打断正常的线程,可以根据打断状态来标记是否退出线程。
stop() 方法强行终止线程,不推荐使用这个方法,该方法已被弃用
线程池:shutdown要等所有线程执行完后再关闭,shutdownNow将线程池内正在执行的线程强制停掉。
同步阻塞:synchronized。 [ˈsɪŋkrənaɪzd]
java线程的五大状态,阻塞状态详解 - Life_Goes_On - 博客园
run() 方法 和start() 方法区别
run方法是线程要执行的具体任务代码的载体。它就像是一个普通的方法,如果直接调用run方法,它会在当前线程中同步执行,就像调用普通方法一样,不会开启新的线程。例如,假设MyThread是一个自定义线程类,在MyThread类中有run方法,直接调用myThread.run(),代码会在当前调用它的线程中按顺序执行run方法里的内容。
start方法用于启动一个线程。当调用start方法时,会创建一个新的执行线程,这个线程会在合适的时候自动调用run方法来执行线程任务。还是以MyThread为例,调用myThread.start()后,会开启一个新的线程,新线程会在系统调度下运行run方法里的代码,和调用run方法所在的线程是相互独立的。
wait、sleep和yield方法的区别
wait
所属类及作用对象:wait方法是Object类中的方法,用于线程间的通信,
让当前线程进入等待状态,直到其他线程调用notify或者notifyAll方法来唤醒它。它必须在同步代码块(使用synchronized关键字修饰)中调用。
对锁的影响:当线程执行wait方法时,会释放当前对象的锁,
使得其他线程可以获取该锁进入同步代码块。
例如,多个线程访问一个共享资源(如一个对象的同步方法),
一个线程执行wait后,其他线程就有机会获取锁来访问这个资源。
使用场景:常用于生产者 - 消费者模式等场景,消费者线程发现没有资源可消费时,
通过wait等待生产者生产资源,生产者生产完后通过notify唤醒消费者。
sleep
所属类及作用对象:sleep是Thread类中的静态方法,作用是让当前线程暂停执行一段时间。 它是线程自身的行为,不涉及线程间的通信和锁的操作。
对锁的影响:与wait不同,sleep不会释放锁。如果线程在持有锁的情况下执行sleep方法, 在睡眠期间仍然持有锁,其他线程无法获取该锁进入同步代码块。
使用场景:用于简单地暂停线程的执行,比如模拟延迟或者定时任务,
像每隔一段时间去检查某个资源的状态。
yield
所属类及作用对象:yield是Thread类中的静态方法,
用于提示线程调度器当前线程愿意让出 CPU 时间片。
它也是线程自身的行为。
对锁的影响:不会释放锁,和sleep类似,在执行yield方法后,如果再次获得 CPU 时间片, 它会继续从让出时间片的位置开始执行,而且在执行yield时,它仍然持有锁。
使用场景:在多线程程序中,当一个线程觉得自己已经执行了足够长的时间或者有其他优先级相当的线程可能需要执行时,可以调用yield方法,让调度器重新分配时间片。不过这种让出是一种提示,调度器可能不会按照线程的意愿来分配时间片。
wait [weɪt] yield [jiːld]
方法归属不同 : wait()是Object类中的非静态方法;
sleep()、yield()是Thread类中的静态方法。
final void wait()
public static native void sleep()
作用: wait()用于线程同步或者线程之间进行通信;
sleep()用于休眠当前线程,并在指定的时间点被自动唤醒;
yield()临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行(如果等待的线程优先级较低,则当前线程继续执行)。
锁的特点不同:wait()释放对象锁,允许其他线程获得该对象锁。
sleep()如果在synchronized代码块中执行,并不释放对象锁,抱着锁睡觉;
yield()仅释放线程所占用的CPU。
JMM(JavaMemoryModel)/Java内存模型
JMM(Java Memory Model)即 Java 内存模型,它是一种抽象的概念,
用于定义 Java 程序中各种变量的访问规则。
1. 主要目的
屏蔽不同硬件和操作系统的内存访问差异,
确保 Java 程序在各种平台上能够有一致的并发行为。
例如,不同的处理器架构可能有不同的缓存一致性协议,
JMM 可以让 Java 开发者不用过多考虑这些底层细节。
2. 内存区域划分
主内存(Main Memory):存储 Java 程序中所有的实例变量和类变量,
是多个线程共享的内存区域。
就像一个公共仓库,所有线程都可以访问其中存储的数据。
工作内存(Working Memory):每个线程都有自己的工作内存,它是线程私有的。
线程对变量的操作(如读取、赋值等)都在自己的工作内存中进行,
工作内存中保存了从主内存中拷贝过来的变量副本或者是刚刚在工作内存中创建的变量。
3. 数据同步机制
变量的读取和赋值规则:当线程要使用一个变量时,首先会从自己的工作内存中查找,
如果没有则从主内存中读取并拷贝到工作内存。
当线程修改一个变量后,会在某个时间将工作内存中的变量副本写回主内存,但这个时间是不确定的,由 JMM 的规则来控制。
内存屏障(Memory Barrier):也称为内存栅栏,用于控制特定操作的执行顺序。
它可以保证在屏障之前的操作一定先于屏障之后的操作执行,从而确保内存操作的有序性。
例如,在多线程环境下,一个线程对共享变量的写操作和另一个线程对该变量的读操作之间可能需要插入内存屏障来保证数据的正确性。
4. 对并发编程的影响
可见性问题:如果没有正确的同步机制,一个线程对共享变量的修改可能对其他线程不可见。例如,线程 A 修改了一个共享变量,但是由于没有及时将修改后的值写回主内存,线程 B 可能读取到旧的值。
原子性问题:有些操作看起来是一个整体,但在多线程环境下可能会被拆分。
例如,对于非原子的变量自增操作(如i++),可能会在多个线程中出现问题,因为它实际上包含了读取、修改和写入三个步骤,JMM 规定了如何处理这类非原子操作的并发情况。
有序性问题:在程序代码中,指令的书写顺序和实际执行顺序可能不同。
JMM 通过内存屏障等机制来保证在一定程度上的有序性,防止指令重排等情况导致的并发问题。
jMM的目的是为了解决Java多线程对共享数据的读写一致性问题.
主内存主要对应于Java堆中的是实例变量和类变量部分,
而工作内存则对应于虚拟机栈中的部分区域。每个线程都有个独立的栈内存空间。
线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
JMM 与 volatile 关键字的关系是什么?
可见性保障
JMM(Java 内存模型)规定了线程之间的变量访问规则,其中一个重要问题是可见性。
在多线程环境下,没有合适的同步机制时,一个线程对共享变量的修改可能对其他线程不可见。而volatile关键字就是 JMM 提供的一种保证可见性的机制。
当一个变量被声明为volatile后,JMM 会确保每次该变量被一个线程修改后,
新的值会立即被写回主内存。而且其他线程在读取这个变量时,会直接从主内存中读取最新的值,而不是使用自己工作内存中的副本,这样就保证了变量在多个线程之间的可见性。
-->嗅探机制--发现变量无效。
禁止指令重排
JMM 允许编译器和处理器对指令进行重排序,以提高程序的执行效率。
但是在多线程环境下,指令重排可能会导致程序出现意外的结果。
volatile关键字在一定程度上禁止了指令重排。
它通过在变量的读写操作前后添加内存屏障来保证有序性。
具体来说,在写操作之后添加一个写屏障,在读操作之前添加一个读屏障,
这样就限制了编译器和处理器对volatile变量相关指令的重排序,从而确保了程序按照预期的顺序执行。
重排序的种类分为三种:
编译器重排序,
指令级并行的重排序,
内存系统重排序。
as-if-serial不管怎么重排序,单线程下的执行结果不能被改变。
不保证原子性
虽然volatile关键字可以保证可见性和一定程度的有序性,但它不保证原子性。
例如,对于i++这样的复合操作(包含读取、加 1 和写入三个步骤),
即使i是volatile变量,在多线程环境下仍然可能出现问题。
因为多个线程可能同时读取到i的相同值,然后分别进行加 1 操作,
最后写回主内存时会覆盖对方的值,导致结果不符合预期。如果要保证原子性,
还需要结合其他同步机制,如synchronized关键字或者java.util.concurrent.atomic包中的原子类。
private static synchronized void add() {
count++;
}
Juc.Atomic包下的类 atomicInteger/Long/boolean