你一个人同时交了 3 个女朋友,你要同时应付 3 个人,这叫并发。你们 3 个人,分别交了一个女朋友,这叫并行。
- 进程基本上是相互独立的,而多个线程可以存在于一个进程内
- 多个线程可以在同一个进程内共享进程拥有的资源(如:内存空间)
- 线程更轻量,线程都上下文切换成本一般情况下要比进程上下文切换低
线程上下文切换,此过程在 CPU 并发执行多个线程的情况下,由一个线程切换到另一个线程执行的过程。此过程涉及到保存当前执行线程的当前状态和载入要切换的线程的状态。这些状态就是我们在 《JVM 运行时数据区》一章中我们提到的 PC 寄存器和Java 虚拟机栈等,更详细的内容请参考我们前面的 JVM 系列的《JVM 运行时数据区》一文
- PC 寄存器,是每个线程私有的,指向当前线程下一个要执行的指令。
- 每个线程都对应一个 Java 虚拟机栈,其内部分为一个一个的栈帧(stack Frame),对应着一次一次的 Java 方法调用。
如果线程数过多,并发量大,上下文切换就会很频繁,不断频繁的切换上下文就会影响线程的执行效率。
示例
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
DeamonThread:0
userThread:5
userThread:6
userThread:7
userThread:8
userThread:9
可以看到守护进行的逻辑并没有完全执行完毕。因为守护进行随着 main 主线程的结束而结束了。
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 一节来理解。
public boolean isInterrupted() {
return isInterrupted(false);
}
// ClearInterrupted 表示是否重置打断状态
private native boolean isInterrupted(boolean ClearInterrupted);
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();
线程的几种状态示例:
// 线程 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());
一段代码段内,如果出现对共享资源进行读写操作,则这段代码称为临界区。
多个线程在不同的时刻访问同一个资源,导致数据不一致或错误的结果或者结果无法预测(每次执行结果不一样)。
线程安全的代码是指在多线程环境下,不管多少线程并发访问,都能保证程序的正确性和一致性。线程安全的代码不会出现上述问题。
尽量使用局部变量:因为局部变量存储在 Java 虚拟机栈,而虚拟机栈是线程私有的,多个线程直接不会存在局部变量的共享使用。
如果局部变量的引用被暴露给方法外部(常见为通过return将局部变量引用返回),也会产生线程安全问题。
多线程在操作同一个资源时,同一时刻只能有一个线程操作,其他线程等待这个线程操作结束后抢占操作这个资源,就是线程同步。
线程同步可以保证多线程在操作同一个资源时,结果的正确性。但同时只能有一个线程可以操作,降低了程序的性能。
synchronized关键字是一种内置的同步机制,用于控制多个线程对共享资源的访问。它用于保证在同一时刻,只有一个线程可以访问被synchronized保护的代码块或方法。但其可能引发死锁问题。
public class Example {
private Object lock = new Object();
public void someMethod() {
synchronized (lock) {
// 代码块
}
}
}
public class Example {
public synchronized void someMethod() {
// 方法体
}
}
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
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 关键字,应最小化同步块(避免锁膨胀),尽量降低线程阻塞的时间,且需要进行重入判断。
Java 锁的优化方向主要是尽量减少线程阻塞和唤醒的开销,以提高并发性能。
在传统的重量级锁机制中,当一个线程请求一个已经被其他线程持有的锁时,请求的线程会被挂起,并进入操作系统的调度队列,等待锁被释放。然而,上下文切换的成本是相当高昂的,严重影响并发性能。
轻量级锁则旨在避免这种情况。当一个线程请求一个已经被另一个线程持有的轻量级锁时,JVM 会让请求的线程进入自旋状态,而非将其挂起。自旋状态下的线程会在用户态不断检查锁的状态,期望在短时间内锁能够被释放,避免了昂贵的上下文切换。然而,如果锁的竞争持续时间过长,轻量级锁也会膨胀为重量级锁,以避免过长时间的无效自旋。
使用 synchronized 关键字声明的锁在没有竞争的情况下会被 JVM 优化为轻量级锁。
轻量级锁的性能优势主要源于它在无竞争情况下能够通过 CAS 操作(Compare and Swap,比较并交换)成功获取锁,而无需进行线程切换和调度。如果无法通过 CAS 操作获得锁,就会膨胀
自旋锁是指线程在获取锁的时候,当前锁被其他线程占用还未释放,那么当前线程会进入循环,一直重复获取锁的状态,而不会进入挂起或者睡眠等状态。当锁被释放后,当前线程获取到该锁的执行权,则会跳出循环执行相关代码。
自适应自旋(JDK1.6+):自适应自旋会根据锁的竞争情况动态调整自旋的次数。
偏向锁是一种优化锁性能的策略,其核心思想是减少不必要的锁竞争开销。当一个锁被一个线程频繁获取时,JVM 将这个锁"偏向"到这个线程,意味着在此后的几次尝试中,该线程可以无需同步操作就能获取这个锁。这大大减少了锁获取和释放的开销,提升了程序的运行效率。
- 撤销:如果锁对象调用了它的 hashCode 方法,偏向锁将升级为轻量级锁。多个线程访问偏向锁,也会导致偏向锁撤销。撤销是个消耗资源的操作。
- 批量重偏向:当另一个线程获得该锁的次数超过阈值(20次)后,锁将重偏向到该线程。(注:重偏向的前提还是线程相互间没有竞争)
- 批量撤销:如果线程撤销超过阀值(40次)后,JVM 会将锁对象变为不可偏向的。(注:批量撤销的前提是线程竞争加剧的情况)
如果某个锁对象一定不会被其他线程所使用,那么 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 都是 Object 对象的方法,所以 Java 中的所有类,都有这些方法。而且这些方法最终都是 native 方法。
注意:wait 和 notify/notifyAll 只能在 synchronized 关键字包含的代码块或方法中执行,否则会抛出 IllegalMonitorStateException(方法注释的说法为,当前线程必须获得锁才能执行该方法)。
/**
* 点外卖:
* 线程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 表达式、函数式接口等,如果你还没有了解过,可以查看我其他专栏的文章先了解一下。