文章目录
概念
线程是什么
线程就是一个执行流,每个线程都是按照自己的顺序来执行相应的代码,例如main
方法是一个线程而且它是典型的主线程
它也是按照自己代码的执行顺序执行的
为什么存在线程
线程存在就是为了多并发去执行代码,当单核cpu
的发展遇到了瓶颈就需要多核cpu
而并发编程又能更好的利用多核资源. 而多并发的去调度线程会比进程块 并且创建或撤销进程时,系统付出的开销远大于创建或撤销线程的开销,线程在切换时,开销很小。
进程与线程的区别
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,它是系统进行资源分配和调度的一个独立单位。
并且一个进程里面是至少包含一个进程 它们是包含关系. 同一个进程的线程之间是共用一个内存空间的
创建线程
创建一个线程需要使用到java
封装好的一个类 Thread
, 在Java
标准库中 Thread
类可以视为是对操作系统对线程管理方面提供的 API
进行了进一步的抽象和封装.
要创建一个线程,程序员首先要创建一个从Thread
类的子类对象重写run
方法,或者创建Runnable
接口的实现类重写run
方法利用Thread
的构造方法达到创建Thread
对象,并且用户是不能直接调用run
方法的,而是通过Thread
创建出来的那个对象去调用start()
方法去创建一个线程,而start
会自己去完成一些工作例如调用run
方法, 下面代码案例:
创建一个线程
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("当前是thread进程");
});
thread.start(); // 创建线程
System.out.println("当前是main这个进程");
}
运行结果
下面用5种方式创建进程:
- 创建一个类使它继承
Thread
的方式然后重写run
方法达到创建进程 :
代码演示 :
class MyThread extends Thread {
@Override
public void run() {
System.out.println("通过创建一个类继承Thread达到创建线程");
}
}
public class test2 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();// 创建线程
System.out.println("主线程");
}
}
- 创建一个类实现
Runnable
接口重写接口的run
方法然后利用Thread
的构造方法达到创建线程
代码演示 :
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("通过实现Runnable接口重写run方法达到创建线程");
}
}
public class test3 {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread2());
thread.start();// 创建线程
System.out.println("主线程");
}
}
- 通过匿名内部类方式创建线程
代码演示 :
public class test4 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("通过匿名内部类方式创建的线程");
}
};
thread.start();// 创建线程
System.out.println("主线程");
}
}
- 通过匿名内部类创建
Runnable
的子类对象达到创建线程
代码演示 :
public class test5 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通过匿名内部类创建Runnable的子类对象达到创建线程");
}
});
thread.start();// 创建线程
System.out.println("主线程");
}
}
- 通过拉姆达表达式创建
Runnable
的子类对象达到创建线程
代码演示 :
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("当前是thread进程");
});
thread.start(); // 创建线程
System.out.println("当前是main这个进程");
}
以上是5种创建方式
Thread类的简单了解
Thread 的常用构造方法
方法 | 说明 |
---|---|
Thread() | Thread 类的默认构造方法,创建了一个新的线程对象 |
Thread(String) | 这个构造方法接收一个字符串作为参数。它创建一个新的线程对象,并将这个字符串作为线程的名称。 |
Thread(Runnable target) | 这个构造方法接收一个Runnable 接口的实现类对象作为参数。它创建一个新的线程对象,并将这个Runnable 对象作为线程的执行体。 |
Thread(Runnable target,String name) | 这个构造方法接收一个Runnable 接口的实现类对象和一个字符串作为参数。它创建一个新的线程对象,并将这个Runnable 对象作为线程的执行体,同时将这个字符串作为线程的名称。 |
Thread(ThreadGroup group,Runnable target) | 这个构造方法接收一个ThreadGroup 对象、一个Runnable 接口的实现类对象。它创建一个新的线程对象,并将这个Runnable 对象作为线程的执行体,将这个ThreadGroup 对象作为线程所属的线程组。 |
下面是常用构造方法的代码示例
Thread()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("当前是thread进程");
});
thread.start(); // 创建线程
System.out.println("当前是main这个进程");
}
使用默认的构造方法,创建一个新的线程对象
Thread(String)
public static void main(String[] args) {
Thread thread = new Thread(() -> { // 创建Thread这个实例
System.out.println("当前是thread进程");
},"线程1");
thread.start(); // 通过这个实例对象去创建线程
System.out.println("当前是main这个进程");
}
这里的构造方法接受了一个字符串.并利用这个字符串作为线程的名称,可以利用java jdk
里面的自带的管理工具jconsole
可以查看线程的名称是否为传入的字符串
找到jdk
所在的目录找到bin
目录点进去搜索一下jconsole
即可找到
这里需要让线程执行起来才能查看,得在代码中加个死循环不然类执行的太快线程销毁了
public static void main(String[] args) {
Thread thread = new Thread(() -> { // 创建Thread这个实例
while(true)
System.out.println("当前是thread进程");
},"线程1");
thread.start(); // 通过这个实例对象去创建线程
System.out.println("当前是main这个进程");
}
双击jconsole
找到,选择本地连接选择当前运行的那个类,在点击不安全连接
选择线程
即可看到线程名
Thread(Runnable target)
public class test5 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
});
thread.start();// 创建线程
System.out.println("主线程");
}
}
这个构造方法接收一个Runnable
接口的实现类对象作为参数。它创建一个新的线程对象,并将这个Runnable
实现类对象作为线程的执行体。调用线程对象的 start
方法后,线程会执行这个Runnable
实现类的 run
方法。
Thread(Runnable target,String name)
public class test5 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
},"线程1");
thread.start();// 创建线程
System.out.println("主线程");
}
}
这个构造方法接收一个Runnable
接口的实现类对象和一个字符串作为参数。它创建一个新的线程对象,并将这个Runnable
实现类对象作为线程的执行体,同时将这个字符串作为线程的名称。
看名称和第二个相同
Thread(ThreadGroup group,Runnable target)
public static void main(String[] args) {
// 创建一个新的线程组
ThreadGroup group = new ThreadGroup("8");
// 创建两个新的线程,并指定它们所属的线程组和名称
Thread thread1 = new Thread(group,() -> {
System.out.println("thread1线程");
},"Thread1");
Thread thread2 = new Thread(group,() -> {
System.out.println("thread2线程");
},"Thread2");
// 启动两个新线程
thread1.start();
thread2.start();
// 打印出线程组中的所有线程名称
Thread[] threads = new Thread[group.activeCount()];
group.enumerate(threads); //获取线程组的所有线程,并存储到thread这个数组中
for (Thread thread : threads) { //打印线程的名字
System.out.println(thread.getName());
}
}
这个可以让多个线程指定到一个线程组当中,方便管理和控制
Thread 的常见属性
属性 | 描述 |
---|---|
getName() | 线程的名称 |
getId() | 线程唯一的标识 |
getPriority() | 线程的优先级,数字越大标识优先级越高,默认为5 |
getState() | 线程的状态,包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED |
isDaemon() | 线程是否为守护线程 |
isAlive() | 线程是否存活 |
isInterrupted() | 线程是否中断 |
getThreadGroup() | 线程所在的线程组 |
下面是常见属性的代码示例 :
getName()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
},"线程1");
thread.start();
System.out.println(thread.getName()); //打印线程名
}
调用getName
会获取线程的名称,当没有指定线程的名称时会打印它的默认名称 Thread -
加一个自增数字进行结合
运行结果如下 :
getId()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
},"线程1");
thread.start();
System.out.println(thread.getId()); //获取线程的id
}
调用getId
会获取线程的id
,每个线程都有自己的id
它们是唯一的,是由库自己生成的
运行结果如下 :
getPriority()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
},"线程1");
thread.start();
System.out.println(thread.getPriority()); //获取线程的优先级
}
调用getPriority
会获取线程的优先级,如果自己没有指定优先级会打印默认的优先级5级 ,数字越大,优先级越高 如下
getState()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
},"线程1");
thread.start();
System.out.println(thread.getState()); //获取线程的当前状态
}
调用getState
会获取当前线程的状态
运行结果如下 :
isInterrupted()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
},"线程1");
thread.start();
System.out.println(thread.isInterrupted()); //判断标志位是否被设置
}
调用isInterrupted
会去判断标志位是否被设置,默认位false
运行结果如下 :
getThreadGroup()
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("78");
Thread thread = new Thread(threadGroup,() -> {
},"线程1");
thread.start();
System.out.println(thread.getThreadGroup()); //获取线程所属的那个线程组
}
调用getThreadGroup
会获取线程所属的那个线程组
运行结果如下 :
线程的标志位
方法 | 描述 |
---|---|
interrupt() | 请求对象关联的线程中断,将线程的标志位设置为true,如果线程正在阻塞则以异常方式通知 |
interrupted() | 检测当前线程标志位是否设置为true,调用后清楚标志位,它是由static关键字修饰的只能由类名调用 |
interrupted() | 检测对下那个关联的线程标志位是否设置为true,调用后不清楚标志位 |
**下面是代码实例 : **
-
interrupt
未被阻塞情况下设置标志位
public static void main(String[] args) {
Thread thread = new Thread(() -> {
int i = 0;
while (!Thread.currentThread().isInterrupted()) { //判断标志位是否被设置
if (i == 10) {
Thread.currentThread().interrupt(); //设置标志位
System.out.println(i + " = " + "标志位已设置结束");
}else {
System.out.println(i + " = " + "等待标志位设置");
}
i++;
}
});
thread.start(); //启动线程
}
Thread.currentThread()
是获取当前线程的实例对象
循环十次满足if条件时实例对象会调用 interrupt()
方法来设置标志位。接着,在循环的下一次迭代中,它会检查标志位是否已经设置,如果是,则跳出循环。
运行结果如下 :
阻塞时设置标志位
public static void main(String[] args) {
Thread thread = new Thread(() -> {
int i = 0;
while (!Thread.interrupted()) { //判断标志位是否被设置
try {
System.out.println("睡眠中");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("睡眠结束");
Thread.currentThread().interrupt(); //设置标志位
}
}
});
thread.start(); //启动线程
for (int i = 0; i < 10_000;i++) {
}
thread.interrupt(); //设置标志位
}
sleep
方法会让当前线程暂停执行一段时间,进入阻塞状态,等待指定的时间过去后再继续执行。然而,在线程睡眠的期间,如果另一个线程调用了当前线程的 interrupt()
方法来设置中断信号,那么当前线程的 sleep
方法会被中断,抛出 InterruptedException
异常,并清除中断状态。
运行结果如下 :
interrupted
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("第一次查看标志位 : " + Thread.interrupted()); //查看标志位是否被设置
Thread.currentThread().interrupt(); //设置标志位
System.out.println("第二次查看标志位 : " + Thread.interrupted());//查看标志位是否被设置
System.out.println("第三次查看标志位 : " + Thread.interrupted());//查看标志位是否被设置
});
thread.start(); //启动线程
}
当第一次调用 interrupted()
方法时,如果当前线程的中断状态被设置(即被中断),它会返回 true
,然后会清除中断状态,将中断状态重置为初始状态。如果当前线程的中断状态未被设置(未中断),它会返回 false
。
运行结果如下 :
isInterrupted()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("第一次查看标志位 : " + Thread.currentThread().isInterrupted()); //查看标志位是否被设置
Thread.currentThread().interrupt(); //设置标志位
System.out.println("第二次查看标志位 : " + Thread.currentThread().isInterrupted());//查看标志位是否被设置
System.out.println("第三次查看标志位 : " + Thread.currentThread().isInterrupted());//查看标志位是否被设置
});
thread.start(); //启动线程
}
当第一次查看标志位时因为当前标志位没有被设置所以输出为false
,第二次被设置了输出为true
,但是第三次也为true
因为调用完isInterrupted
它不会清楚标志位
运行结果如下 :
等待线程 join
方法 | 描述 |
---|---|
join() | 等待线程结束 |
join((long millis) | 最多等待millis耗秒 |
join(long millis, int nanos) | 最多等待millis耗秒,但是精度更高了 |
这里只讲解前两个方法
join()
public static void main(String[] args) {
Thread thread = new Thread(() -> {
int i = 0;
while (i++ != 10) {
System.out.println("线程1执行中");
}
System.out.println("线程1执行完成");
},"线程1");
thread.start(); //启动线程
System.out.println("主线程开始等待");
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程开始执行");
}
当在线程 A 中调用线程 B 的 join()
方法时,线程 A 会发生阻塞,等待线程 B 执行完毕后才继续执行。
运行结果如下 :
join((long millis)
public static void main(String[] args) {
Thread thread = new Thread(() -> {
int i = 0;
while (i++ != 10) {
System.out.println("线程1执行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程1执行完成");
},"线程1");
thread.start(); //启动线程
System.out.println("主线程开始等待");
try {
thread.join(8000); //主线程开始等待,当等待到8秒时就不等了
System.out.println("不等了");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程开始执行");
}
当一个线程调用 join(long millis)
方法并指定了等待的最大时间时,如果超过这个限制,线程会自动被唤醒开始执行自己的代码。 优化这段话
运行结果如下 :
线程的状态
状态 | 描述 |
---|---|
NEW | 线程对象被创建但尚未启动 |
RUNNABLE | 线程正在执行或资源等待中 |
BLOCKED | 线程正在等待获取锁或等待其它条件 |
WAITING | 现场在等待其它线程的特定操作或条件 |
TIME_WAITING | 线程在等待一段指定的时间内 |
TERMINATED | 线程执行完其任务或被提前中断后进入终止状态 |
NEW
public static void main(String[] args) {
Thread thread = new Thread(() -> {
},"线程1");
System.out.println(thread.getState());
thread.start(); //启动线程
}
当创建了线程对象并没调用start
方法创建对象时线程所处于的状态 NEW
运行结果如下 :
RUNNABLE
public static void main(String[] args) {
Thread thread = new Thread(() -> {
},"线程1");
thread.start(); //启动线程
System.out.println(thread.getState());
}
当调用start
创建线程后所处于的状态,表明线程在执行中或资源等待中
运行结果如下 :
BLOCKED
static int[] arr = new int[4];
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (arr) {
System.out.println("t1加锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"线程1");
thread1.start(); //启动线程
Thread thread2 = new Thread(() -> {
synchronized (arr) {
System.out.println("t2加锁");
}
},"线程2");
thread2.start(); //启动线程
for (int i = 0; i < 5000; i++) {
System.out.printf("");
}
System.out.println("打印线程2当前状态 : " + thread2.getState());
}
当线程1指定arr这个对象进行加锁时,线程2也想对arr这个对象进行加锁这时候线程2就会发生等待获取锁,此时线程2的这个状态也就是BLOCKED
只有当线程1解锁后线程2才会获取到锁,才会开始执行线程2的代码
运行结果如下 :
WAITING
static final Object object = new Object();
static volatile int i = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (object) {
try {
System.out.println("开始进入等待");
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结束");
}
});
thread1.start();
for (int i = 0; i < 10000; i++) {
System.out.printf("");
}
System.out.println(thread1.getState());
}
当线程中的某个对象调用了 wait()
方法时,线程将进入等待状态,并且其状态将被标记为 WAITING
运行结果如下 :
TIME_WAITING
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"线程1");
thread1.start(); //启动线程
for (int i = 0; i < 5000; i++) {
System.out.printf("");
}
System.out.println(thread1.getState());
}
当线程在等待指定的一段时间时线程的状态就为TIME_WAITING
运行结果如下 :
TERMINATED
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"线程1");
thread1.start(); //启动线程
try {
thread1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println();
}
当线程已经执行完毕或被终止了线程就会处于这个状态TERMINATED
在TERMINATED状态下,线程的资源会被释放,包括内存资源和系统资源。此时,线程对象不能再被重新启动或恢复执行。
运行结果如下 :
线程的安全问题
线程安全的概念
在多线程编程中,多个线程同时访问或修改 共享的数据结构、对象、资源时可能会导致一些问题 :
- 竞态条件(
Race Condition
):多个线程同时修改共享数据,导致数据的最终结果依赖于线程的执行顺序,从而产生不确定的结果。 - 数据不一致性:由于没有适当的同步机制,线程之间的操作可能会破坏数据的一致性,导致数据错误或不一致的状态。
- 死锁(
Deadlock
):多个线程因为相互等待对方持有的资源而无法继续执行,导致程序无法继续运行。 - 活锁(
Livelock
):多个线程在尝试解决死锁的过程中,相互礼让而无法取得进展,导致程序无法正常执行。
为了确保线程安全,可以采取以下方法 :
- 使用锁机制:使用
synchronized
关键字或Lock
接口的实现来保证同一时间只有一个线程可以访问临界区,避免多个线程同时修改共享变量导致的数据竞争和不一致性。 - 使用原子类:使用Java提供的原子类(如
AtomicInteger
、AtomicLong
等)来实现线程安全的原子操作,保证对共享变量的修改是原子性的,不会被其他线程中断。 - 使用
volatile
关键字:将共享变量声明为volatile
,确保对变量的写操作对其他线程的读操作可见,避免了线程间的缓存不一致性问题。 - 使用线程安全的数据结构:使用线程安全的数据结构(如
ConcurrentHashMap
、ConcurrentLinkedQueue
等)来替代非线程安全的数据结构,避免多线程访问时的数据竞争和并发问题。 - 使用并发工具类:使用Java并发工具类(如
CountDownLatch
、Semaphore
、CyclicBarrier
等)来实现线程间的同步和协作,确保线程的有序执行和正确的互斥访问。 - 设计可变对象的不可变副本:对于需要共享的可变对象,将其设计为不可变的,每个线程操作时都创建一个副本,避免多线程修改同一个对象的状态。
- 避免使用全局变量:尽量避免使用全局变量或共享状态,而是尽可能将状态封装在对象内部,使得每个线程都操作自己的局部变量。
- 合理的线程同步策略:根据具体场景和需求,选择合适的线程同步策略,如使用读写锁、条件变量等,以提高并发性能和线程安全性。
synchronized关键字
- synchronized的概述
synchronized
用于实现线程的同步和互斥。它可以用于方法、代码块和静态方法,用于控制对共享资源的访问,以保证多线程环境下的线程安全。
- synchronized的特性
- 互斥访问:
synchronized
可以确保同一时间只有一个线程可以进入被synchronized
修饰的方法或代码块,避免多个线程同时访问共享资源而导致的数据竞争和不一致性。 - 原子性操作:被
synchronized
修饰的方法或代码块中的操作被视为一个原子操作,要么执行完毕,要么不执行,不会出现中间状态。这可以保证多个操作在并发环境下的一致性。 - 内存可见性:
synchronized
不仅保证了线程的互斥访问,还具有内存可见性的特性。即一个线程对共享变量的修改对于其他线程是可见的,保证了数据在多线程之间的一致性。
-
synchronized的代码示例
-
修饰实例方法
public class Demo {
public synchronized void tt() {
}
}
- 修饰静态方法
public class Demo {
public synchronized static void tt() {
}
}
- 修饰代码块
public class Demo {
public void tt() {
synchronized (new Object) {
}
}
}
- synchronized的使用场景
static class Cont {
int i = 0;
int get() {
return i;
}
void set(int i) {
this.i += i;
}
}
public static void main(String[] args) {
Cont cont = new Cont();
Thread thread1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
cont.set(1);
}
},"线程1");
Thread thread2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
cont.set(1);
}
},"线程2");
thread1.start(); //启动线程
thread2.start(); //启动线程
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(cont.get());
}
运行结果如下 :
跟预期结果不一致,因为当没有做任何关于线程安全问题防护措施时同时去用两个线程修改同一个变量就会发生竞争条件、数据不一致、脏读等等…
为了解决此类问题通过synchronized的来避免这种情况
如下 :
static Object object = new Object();
static class Cont {
int i = 0;
int get() {
return i;
}
void set(int i) {
synchronized (object) {
this.i += i;
}
}
}
public static void main(String[] args) {
Cont cont = new Cont();
Thread thread1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
cont.set(1);
}
},"线程1");
Thread thread2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
cont.set(1);
}
},"线程2");
thread1.start(); //启动线程
thread2.start(); //启动线程
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(cont.get());
}
运行结果如下 :
当使用 synchronized
对实例方法、静态方法和代码块进行加锁时,只需提供一个共享的锁对象。当一个线程获取到锁对象的锁后,其他线程必须等待该线程释放锁之后才能获取锁并执行对应的代码块。这样可以确保同一时间只有一个线程能够进入被加锁的代码区域,从而避免多个线程同时访问共享资源导致的数据竞争和不一致性。一旦获取锁的线程执行完毕并释放锁,其他线程才能竞争获取锁并执行自己的代码。这种机制保证了线程之间的互斥执行,确保线程安全。
volatile关键字
- volatile关键字的概述
volatile
是 Java 中的一个关键字,用于修饰变量。它的主要作用是保证被修饰的变量在多线程环境下的可见性和禁止指令重排序。
volatile
的特性
- 可见性:
volatile
保证了当一个线程修改了被修饰的变量的值时,其他线程能够立即看到最新的值。这是因为在每次读取volatile
变量时,都会从主内存中获取最新的值,而不是使用线程的本地缓存。 - 禁止指令重排序:
volatile
关键字还能禁止编译器和处理器对指令进行重排序优化,保证代码的执行顺序与程序中的顺序一致。这样可以避免由于指令重排序而导致的多线程环境下的错误结果。
-
volatile的使用场景
-
内存可见性
static int i = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (i == 0) {
}
System.out.println("内存可见性问题");
}, "线程1");
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
i++;
System.out.println("线程2已执行完毕");
}, "线程2");
thread1.start(); //启动线程
thread2.start(); //启动线程
}
运行结果 :
按平常对代码的认识,我们的第一主观是当线程2走完i++后,则线程1就会退出循环在执行打印语句,可是运行结果确是死循环了
为什么呢?
当代码中存在类似 while (i == 0)
的循环条件,并且循环体内部是空语句时,编译器在优化过程中可能会对这段代码进行一些优化。其中的优化策略包括将变量的值加载到线程的寄存器中,并在后续的循环判断中直接使用寄存器中的值进行比较,而不再每次都去主内存中获取变量的最新值。
这种优化可能会导致一个问题,即当循环条件依赖于变量的最新值时,如果其他线程修改了该变量的值并更新到主内存中,当前线程可能无法立即感知到这个变化,因为它一直在使用寄存器中的旧值进行比较。这种情况下,代码可能陷入死循环,无法退出循环。
为了解决这个问题,可以使用volatile关键字来指定编译器不要对该变量就行优化 如下 :
static volatile int i = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (i == 0) {
}
System.out.println("内存可见性问题");
}, "线程1");
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
i++;
System.out.println("线程2已执行完毕");
}, "线程2");
thread1.start(); //启动线程
thread2.start(); //启动线程
}
运行结果 :
可以看到代码正确的运行结束了
- 指令重排序
static int x = 0;
static int y = 0;
static int flag = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
x = 1; // 写操作
flag = 1; // 写操作
});
Thread thread2 = new Thread(() -> {
if (flag == 1) { // 读操作
y = x; // 读操作
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("y = " + y);
}
运行结果 :
线程1对变量 x
和 flag
进行写操作,而线程2对变量 flag
进行读操作,并根据读到的值决定是否读取 x
的值。
由于指令重排序的存在,编译器和处理器可能会对指令进行优化重排,导致写操作的指令顺序与原代码的顺序不一致。这可能导致线程2在读取 flag
的时候发现值为1,然后读取 x
的值,而实际上线程1的写操作可能还没有完成,x
的值还是旧的。
这个问题的关键在于指令重排序导致了 flag
的写操作比 x
的写操作更早执行,虽然在代码中 flag
的写操作在 x
的写操作之后,但实际执行时可能会发生重排序,从而导致线程2看到了一个不一致的状态。
通过使用 volatile
关键字修饰 flag
变量,可以禁止指令重排序,从而确保线程2在读取 flag
的值时,能够正确地获取 x
的最新值,避免出现不一致的情况 如下 :
static volatile int x = 0;
static volatile int y = 0;
static volatile int flag = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
x = 1; // 写操作
flag = 1; // 写操作
});
Thread thread2 = new Thread(() -> {
if (flag == 1) { // 读操作
y = x; // 读操作
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("y = " + y);
}
通过使用 volatile
关键字修饰变量,可以确保禁止指令重排序,并保证线程之间的可见性和正确的执行顺序。因此,在多线程环境中,如果需要保证变量的可见性和避免指令重排序带来的问题,需要使用 volatile
关键字来修饰相关变量。
wait 和 notify
- wait
static Object object = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (object) {
System.out.println("等待中");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("等待结束");
}
});
thread1.start();
}
运行结果 :
当线程调用wait
方法后,它会释放当前持有对象锁并且进入等待状态,直到被其他线程调用同一个对象的notify
方法才会结束等待并且尝试重新获取锁在执行下面的代码,这里面没有调用线程调用notify
所以线程会一直等待造成死锁
- notify
static Object object = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (object) {
System.out.println("等待中");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("等待结束");
}
},"线程A");
thread1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object) {
System.out.println("线程唤醒");
object.notify();
}
}
当主线程调用相同对象的notify
方法后,该线程会继续执行后续的代码,直到释放了锁。此时,它会唤醒调用了wait
方法并等待同一对象锁的某个线程。
被唤醒的线程并不会立即执行,它会在主线程释放锁后,与其他线程竞争锁资源。只有当被唤醒的线程获取到锁后,才能继续执行自己的代码。
通过调用notify
方法,主线程通知了等待的线程,使其有机会再次尝试获取锁,并继续执行。这样确保了线程间的协作和同步,避免了资源的浪费和死锁的发生。
1fJNIO6-1685180622849)]
当线程调用wait
方法后,它会释放当前持有对象锁并且进入等待状态,直到被其他线程调用同一个对象的notify
方法才会结束等待并且尝试重新获取锁在执行下面的代码,这里面没有调用线程调用notify
所以线程会一直等待造成死锁
- notify
static Object object = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (object) {
System.out.println("等待中");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("等待结束");
}
},"线程A");
thread1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object) {
System.out.println("线程唤醒");
object.notify();
}
}
当主线程调用相同对象的notify
方法后,该线程会继续执行后续的代码,直到释放了锁。此时,它会唤醒调用了wait
方法并等待同一对象锁的某个线程。
被唤醒的线程并不会立即执行,它会在主线程释放锁后,与其他线程竞争锁资源。只有当被唤醒的线程获取到锁后,才能继续执行自己的代码。
通过调用notify
方法,主线程通知了等待的线程,使其有机会再次尝试获取锁,并继续执行。这样确保了线程间的协作和同步,避免了资源的浪费和死锁的发生。