线程的创建方式
继承Thread类
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println("我是继承了Thread创建的线程");
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
threadTest.start();
}
}
实现Runnable接口
public class RunnableTest implements java.lang.Runnable {
@Override
public void run() {
System.out.println("我是实现Runnable接口创建的线程");
}
public static void main(String[] args) {
Thread thread = new Thread(new RunnableTest());
thread.start();
}
}
实现Callable接口
public class CallableTest implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int a = 0;
for (int i = 0; i < 100; i++) {
a=a+i;
}
System.out.println("执行Callable");
return a;
}
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
FutureTask<Integer> task = new FutureTask<>(callableTest);
Thread thread = new Thread(task);
thread.start();
try {
Integer integer = task.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
下面我们来具体的聊聊这三种方式:
- 继承Thread类:一般我们在实际开发中很少用到这种方法,因为我们的Java是单继承的,所以如果我们这个类还需要继承其他类的时候就傻了。
- 实现Runnable接口:一般推荐使用这种方法,因为java类可以实现多个接口,但是这种方法不带返回值。
- 实现Callable接口:Callable接口可以有返回值,但是操作比Runnable稍微复杂点,在提交任务的时候需要用到我们的FutureTask去包裹我们的Callable用来接收它的返回值。 Future的get方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果
实现 Runnable 接口比继承 Thread 类所具有的优势 :
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免 java 中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享 ,代码和数据独立
- 线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类
- runnable 实现线程可以对线 程进行复用,因为 runnable 是轻量级的对象,重复 new 不会耗 费太大资源,而 Thread 则不然,它是重量级对象,而且线程执行完就完了,无法再次利用
线程的状态
线程的状态主要为五种:新建,就绪,运行,阻塞,死亡
**新建:**当我们new 一个线程时,线程的状态为新建
**就绪:**当我们的线程调用了start()方法时候
**运行:**当线程获取到了CPU的使用权就会开始执行
***阻塞:**阻塞的情况有三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池,并且wait()方法会释放锁
- 同步阻塞:运行的线程在获取对象的同步锁的时候,如果该同步锁被其他线程占用,那么JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行了sleep(),join()方法,或者发出I/O请求时,JVM会把该线程设置为阻塞状态。当sleep()状态超时、或者是join()等待的线程终止,或者是I/O处理完毕,线程会重新进入就绪状态。
**死亡:**线程执行完毕或者是因为异常退出了run()方法
下面上图来理解记忆:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N5hqSsMR-1670407105266)(C:\Users\22924\Desktop\java面试相关\Java知识\java基础\线程状态.jpg)]
一般线程和守护线程的区别
所谓守护线程,就是我们程序在运行的过程中,在我们后台提供一种通用服务的线程。我们JVM中的垃圾回收线程就是一个守护线程。守护线程什么时候结束呢,当我们的程序结束时。所有的非守护线程也结束了 ,只有当所有非守护线程结束后,守护线程才会结束。
使用守护线程的注意点:
- thread.setDaemon(true) 必须在thread.start()之前设置,否则会抛出 IllegalThreadStateException 异常 。
- 在Daemon线程中产生的新线程也是Daemon的
- 守护线程在任何时候都不应该去访问固有资源,因为它在任何情况下都有可能会中断。
sleep wait yield notify notifyAll join
- sleep与wait的区别
- sleep是线程类Thread的方法导致线程暂停执行时间,把执行机会给其他线程,监控状态依旧会保持,时间到后就会恢复,并且不会释放锁。
- wait()方法是Object()类的方法,会释放锁使线程进入等待队列。只有调用notify或者是notifyAll后,线程才会进入锁池等待获取锁。
- wait、notify、notifyAll只能在同步方法,同步代码块中使用,sleep却可以在任何地方使用
- sleep必须捕获异常,wait、notify、notifyAll不用捕获异常。因为sleep过后,线程不会释放他所持有的锁,他不会影响其他线程,但是其他对象有可能会调用该线程的interrupt(), 产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程 就会异常终止,进入 TERMINATED 状态。
- wait方法也同样有可能在wait的过程中被其他对象调用自身的interrupt()方法。
注意: sleep是一个静态方法 所以调用是通过Thread类来直接调用的,只能够使当前线程sleep,不可以使用对象来调用sleep。
- yield join notify notifyAll
- yield()方法使停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会,如果没有的话, 那么 yield() 方法将不会起作用,并且由可执行状态后马上又被执行。 (线程优先级可以用 setPriority(int newPriority) 方法设置)
- join方法是用于某一个线程执行的时候去调用另外一个线程,等待另一个线程结束后再执行当前线程。
- notify 方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程 等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管 理的实现。
- notifyAll 会唤醒所有等待 ( 对象的 ) 线程,尽管哪一个线程将会第一个处理取决于操作系 统的实现。
- LockSupport 比wait notify/notifyAll更加灵活, LockSupport.park():阻塞当前线程
LockSupport.unpark(Thread t):唤醒特定线程
如何中断线程
- 使用退出标志位,使线程正常退出,也就是线程run方法完成后线程终止
- 通过return退出run方法
- 通过对有些状态中断抛异常Thread.interrupt()中断 (当线程在在阻塞状态下,别的线程调用该线程的interrupt()方法可以使线程异常结束)
- 使用stop方法强制终止线程
中断线程可能出现的问题 :
使用 Thread.interrupt() 并不能使得线程被中断,线程还是会执行。最靠谱的方法就是 设置一个全局的标记位,然后再 Thread 中去检查这个标记位,发现标记位改变则中断线程。
什么是死锁,如何避免
死锁的意思:
多个进程或线程互相等待对方的资源,在得到新的资源之前不会释放自己的资源,这样就形成了循环等待,这种现象被称为死锁 。在没有外力的作用死锁永远不会退出。
避免死锁
死锁有四个形成条件:互斥,请求保持,不可剥夺,循环等待。破环其中任何一个条件,就不会形成死锁。
-
互斥:该资源任一时刻只由一个线程占用。
-
请求保持:一个线程因为请求资源被阻塞,对自己已经获得的资源不放。
-
不可剥夺:若干进程或者线程之间形成一种头尾相接的循环资源等待。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
多线程下抛出异常怎么办
再单线程下面,我们线程的异常可以用try…catch捕获,但是在我们多线程下,线程抛出的异常用try…catch不能捕获,所以就得借助我们的UncaughtExceptionHandler接口,我们创建一个类去实现UncaughtExceptionHandler接口,重写里面的uncaughtException方法,最后在线程启动前通过 thread.setUncaughtExceptionHandler将该类的对象设置进去。
public class UnCatchExceptionTest {
public static void main(String[] args) {
Thread thread = new Thread(new java.lang.Runnable() {
@Override
public void run() {
System.out.println(2 / 1);
System.out.println(2 / 0);
System.out.println(2 / 2);
}
});
thread.setUncaughtExceptionHandler(new MyUnCatchException());
thread.start();
}
static class MyUnCatchException implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName()+"异常信息是:"+e.getMessage());
}
}
}
线程通信的方式
- 同步
同步是指多个线程通过 synchronized 关键字这种方式来实现线程间的通信。 - wait/notify 机制
Condition类
condition类的创建依赖于Lock所以 必须被 lock.lock()和lock.unLock()包裹使用。通过Lock…newCondition();创建
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();会释放所占有的锁
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
LockSupport
总结一下LockSupport的使用要点:
线程调用park阻塞自己(类似sleep),直到中断、超时或者其它线程调用unpark;
线程调用park之前,如果其它线程调用了unpark,则会立即返回;
线程唤醒以后,线程并不知道是什么原因被唤醒,需要去判断时间、线程状态(Thread.interrupted());
线程start之前,unpark没有用,线程start之后park仍然会阻塞;
线程调用park阻塞不会释放线程占用的资源(锁);
它里面实际是维护了一个permit许可 默认为0,当调用park()后会判断是否有许可,没有的话就会阻塞线程。当调用unpark()会将许可置为1,此时park()判断有许可就会去消费许可 并且改为0。所以先调用unpark()再调用park()不会阻塞线程。
线程池的创建方式
线程池的创建往大的说有两种方式,
第一种是通过我们的Executor去创建JAVA帮我设置好的线程池
第二种方式是通过ThreadPoolExecutor去自定义线程池。
通过Executor创建线程池
Executors.newFixedThreadPool:创建固定大小的线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// LinkedBlockingQueue队列大小为Integer.MAX_VALUE 容量过大,可能会堆积大量的任务,从而造成OOM(内存溢出)
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//缺点:该线程池允许创建的最大线程数量为Integer.MAX_VALUE,可能会创建出大量线程,导致OOM(内存溢出)
//SynchronousQueue同步队列,容量为0
//特性:可缓存的线程池。注意到60L存活时间,即60s空闲的线程将会被销毁,直到线程池为0.
//缺点:做到了动态地新增线程。却没有考虑到内存。
Executors.newSingleThreadExecutor: 该方法创建了只有一个线程的线程池,如果提交的任务没有空闲的线程去处理,就会被放入阻塞队列中
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
//service.schedule(new Task(), 10, TimeUnit.SECONDS);
//service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);
//service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
/**DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。*/
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
自定义线程池
ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:核心线程数
maximumpoolSize:最大线程数
keepAliveTime:最大存活时间
unit :时间单位
workQueue :任务队列
threadFactory :线程工厂
handler :拒绝策略
四种拒绝策略
AbortPolicy: 直接丢弃任务 然后抛出 RejectedExecutionException 异常
DiscardPolicy:直接丢弃任务什么也不做
DiscardOldestPolicy: 丢弃最早进入队列的线程,然后重新提交拒绝任务
CallerRunsPolicy: 由调用线程处理该任务(谁提交谁负责)
自定义线程池默认的拒绝策略是AbortPolicy。
线程池执行流程
线程池的关闭
线程池的关闭用到两个方法,shutdown和shutdownNow,都是位于ThreadPoolExecutor里面的
shutdown:它会将线程池状态切换成Shutdown,此时是不会影响对阻塞队列中任务执行的,会将剩余的任务全部执行完,但是会拒绝执行新加进来的任务,同时会回收闲置的Worker;
shutdownNow:先将线程池状态设置为STOP,然后拒绝所有提交的任务。最后中断左右正在运行中的worker,同时会回收所有的Worker;
线程池执行任务的方法
线程池可以通过两个方法来执行任务:submit()和execute()
- execute()执行一个没有返回值的任务 Runnable()任务
- submit()字面意思是提交,它有返回值,既可以提交Callable任务,也可以提交Runnable任务。
当提交的为Callable任务的时候,返回一个Future对象包装的结果,通过future.get()方法获取结果
submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。
submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。
Future.get方法会使取结果的线程进入阻塞状态,直到线程执行完成之后,唤醒取结果的线程,然后返回结果
Threadlocal
含义
ThreadLocal叫做线程变量,意思就是ThreadLocal填充的变量属于当前线程的,该变量对于其他线程而言是隔离的。是当前线程独有的变量,ThreadLocal变量在每个线程中都创建了一个副本,每个线程可以访问自己的内部的副本变量。
thread,threadLocal,threadLocalMap三者之间的关系
Thread持有一个ThreadLocalMap(其实就是一个Entry类型的数组),一个ThreadLocalMap里面包含了多个key ThreadLocal,ThreadLocal在ThreadLocalMap的key里面是弱引用。ThreadLocalMap是ThreadLocal里面的一个内部类,这样设计的好处是隐藏实现,使使用者的操作变得简单。
ThreadLocalMap 是一个线程本地的值,它所有的方法都是private 的,也就意味着除了ThreadLocal 这个类,其他类是不能操作ThreadLocalMap 中的任何方法的,这样就可以对其他类是透明的。同时这个类的权限是包级别的,也就意味着只有同一个包下面的类才能引用ThreadLocalMap 这个类,这也是Thread 为什么可以引用ThreadLocalMap 的原因,因为他们在同一个包下面。
虽然Thread 可以引用ThreadLocalMap,但是不能调用任何ThreadLocalMap 中的方法。这也就是我们平时都是通过ThreadLocal 来获取值和设置值ThreadLdocalMap 什么时候开始和Thread 进行绑定的呢
ThreadLdocalMap 什么时候开始和Thread 进行绑定的呢
在第一次调用ThreadLocal set() 方法的时候开始绑定的,来我们看下set 方法的源码
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap
if (map != null)
map.set(this, value);
else//如果为空就新建一个绑定当前线程
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal 内存泄漏
ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。
那么如何避免内存泄漏呢?
在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收
.\bin\windows\kafka-topics.bat --list --bootstrap-server localhost:9092
.\bin\windows\kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test1
线程三剑客
CountDownLatch
CountDownLatch和join有点类似,它里面有一个计数器,通过每次countDown()方法减去一,当countDownLatch.getCountDown()==0的时候,await()后面的程序才得以执行。
它与join的不同点在于。join只有当当前join的线程执行完,当前的线程才能执行,否则便一直wait.
public class Worker extends Thread {
//工作者名
private String name;
//工作时间
private long time;
private CountDownLatch countDownLatch;
public Worker(String name, long time, CountDownLatch countDownLatch) {
this.name = name;
this.time = time;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// TODO 自动生成的方法存根
try {
System.out.println(name+"开始工作");
Thread.sleep(time);
System.out.println(name+"工作完成,耗费时间="+time);
countDownLatch.countDown();
System.out.println("countDownLatch.getCount()="+countDownLatch.getCount());
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
CountDownLatch countDownLatch = new CountDownLatch(2);
Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);
Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);
Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);
worker0.start();
worker1.start();
countDownLatch.await();
System.out.println("准备工作就绪");
worker2.start();
}
}
运行结果:
worker1开始工作
worker0开始工作
worker0工作完成,耗费时间=3174
countDownLatch.getCount()=1
worker1工作完成,耗费时间=3870
countDownLatch.getCount()=0
准备工作就绪
worker2开始工作
worker2工作完成,耗费时间=3992
countDownLatch.getCount()=0
CyclicBarrier
位于java.util.concurrent包下,字面意思回环栅栏,通过它可以实现让一组线程等待直到某个状态后再全部同时执行,叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以重新使用。我们把这个状态称为barrier。当调用await()方法以后,线程就处于barrier状态
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Test {
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N,new Runnable() {
@Override
public void run() {
System.out.println("当前线程"+Thread.currentThread().getName());
}
});
for(int i=0;i<N;i++) {
new Writer(barrier).start();
}
}
static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try {
Thread.sleep(5000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
}
运行结果:
线程Thread-0正在写入数据...
线程Thread-2正在写入数据...
线程Thread-3正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
当前线程Thread-0
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
Process finished with exit code 0
Semaphore
信号量,可以控制同时访问的线程个数,通过acquire()获取一个许可,如果没有就等待,而release释放一个许可
public static void main(String[] args) {
int N = 8; //工人数
Semaphore semaphore = new Semaphore(5); //机器数目
for(int i=0;i<N;i++)
new Worker(i,semaphore).start();
}
static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("工人"+this.num+"占用一个机器在生产...");
Thread.sleep(2000);
System.out.println("工人"+this.num+"释放出机器");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
工人0占用一个机器在生产...
工人4占用一个机器在生产...
工人3占用一个机器在生产...
工人2占用一个机器在生产...
工人1占用一个机器在生产...
工人3释放出机器
工人0释放出机器
工人1释放出机器
工人4释放出机器
工人2释放出机器
工人7占用一个机器在生产...
工人6占用一个机器在生产...
工人5占用一个机器在生产...
工人7释放出机器
工人6释放出机器
工人5释放出机器
Process finished with exit code 0
三剑客总结:
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限
volatile 关键字
volatile 只能用于成员变量, volatile变量可以保证下一个读取操作会在前一个写操作之后发生。线程都会直接从内存中读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。
volatile并不保证原子性,只有一致性和可见性。
public class VolatileTest {
boolean flag = true;
public void updateFlag() {
this.flag = false;
System.out.println("修改flag值为:" + this.flag);
}
public static void main(String[] args) {
VolatileTest test = new VolatileTest();
new Thread(() -> {
while (test.flag) {
}
System.out.println(Thread.currentThread().getName() + "结束");
}, "Thread1").start();
new Thread(() -> {
try {
Thread.sleep(2000);
test.updateFlag();
} catch (InterruptedException e) {
}
}, "Thread2").start();
}
}
结果:
修改flag值为:false
我们可以看到虽然线程Thread2已经把flag 修改为false了,但是线程Thread1没有读取到flag修改后的值,线程一直在运行
.println(“修改flag值为:” + this.flag);
}
public static void main(String[] args) {
VolatileTest test = new VolatileTest();
new Thread(() -> {
while (test.flag) {
}
System.out.println(Thread.currentThread().getName() + "结束");
}, "Thread1").start();
new Thread(() -> {
try {
Thread.sleep(2000);
test.updateFlag();
} catch (InterruptedException e) {
}
}, "Thread2").start();
}
}
结果:
修改flag值为:false
我们可以看到虽然线程Thread2已经把flag 修改为false了,但是线程Thread1没有读取到flag修改后的值,线程一直在运行