多线程

1.进程和线程的区别?
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
地址空间:同一进程的线程共享本进程的地址空间,而进程之间是独立的地址空间。
关系:一个程序至少一个进程,一个进程至少一个线程。

2.Thread和Runnable的关系?
Thread是实现Runnable接口的类,使得run支持多线程,因为类的单一继承原则,推荐多使用Runnable接口。
如果一个类继承Thread,则不适合资源共享;但实现Runnable接口的话,就很容易实现资源共享。
Runnable是接口,而Thread是类,且实现Runnable接口。
实现Runnable接口相比继承Thread类的好处:类有单一继承的局限,但一个类可以实现多个接口。

3.synchronized底层如何实现?锁优化,怎么优化?
synchronized是Java内建的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
底层实现:1)同步代码块是使用monitorenter和monitorexit指令实现的,当且一个monitor被持有后,它处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所应对的monitor所有权,即尝试获取对象的锁;
2)同步方法依靠的是方法修饰符上的ACC_Synchronized实现。synchronize方法是在Class文件的方法表中将该方法的access_flags字段中的synchronize标志置1,表示该方法是同步方法并调用该方法的对象或者该方法所属的Class在JVM的内部对象表示作为锁对象。
Java的对象头和monitor是实现synchronized的基础。
synchronized存放的位置:synchronized用的锁时存在Java对象头里的。
monitor:可以理解为一个同步工具,通常被描述成一个对象,是线程私有的数据结构。
jdk1.6对锁的实现引入了大量的优化。锁主要存在四种状态:无锁状态、偏向锁状态、轻量级锁、重量级锁。锁只能升不能降级。
偏向锁:当没有竞争出现时,默认使用偏向锁。JVM会利用CAS操作在对象头部分设置线程ID表示这个对象偏向于当前线程,并不涉及真正的互斥锁。
自旋锁:让线程循环执行等待一段时间,不会被立即挂起,看有锁的线程是否会尽快释放锁,然后再去获取锁。
轻量级锁:当关闭偏向锁或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
重量级锁:加锁后,线程会被挂起,操作系统实现线程之间的切换需要从用户态到内核态,切换成本非常高。
多线程中synchronized锁升级原理是什么?
在锁对象头里有一个Threadid字段,在第一次访问的时候Threadid为空,JVM让其持有偏向锁,并将线程id的值赋值给Threadid。再一次访问的时候回判断Threadid与线程id是否一致,如果一致就可以直接使用此对象,如果不一致则将偏向锁升级为轻量级锁,然后再进行自旋,当自旋一定次数后仍为获取到要使用的对象,此时就会把轻量级锁升级为重量级锁,此过程就是synchronized锁的升级。
锁升级的目的?
锁升级是为了减低锁带来的性能消耗。使用偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减轻锁带来的性能消耗。

4.Synchronized和Lock的区别?
1)实现层面:synchronized(JVM层面)、Lock(JDK层面)
2)响应中断:Lock可以让等待锁的线程响应中断,而使用synchronized时,等待的线程会一直等待下去,不能够响应中断
3)立即返回:Lock可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间;而synchronized却无法办到
4)读写锁:Lock可以实现公平锁,而synchronized天生就是非公平锁
5)显示获取和释放:synchronized在发生异常时,会自动释放线程占用的锁,因此不会导致死锁现象;而Lock在发生异常时,如果没有主动通过unLock去释放锁,则可能造成死锁现象,因此使用Lock时需要再finally块中释放锁

5.synchronized和ReentrantLock有什么区别?
1)ReentrantLock可以对获取锁的等待时间进行设置,但必须要有释放锁的配合动作;
2)synchronized不需要手动释放和开启锁,而ReentrantLock必须手动获取和释放锁。
3)ReentrantLock只适用于代码块锁,而synchronized可用于修饰方法、代码块等。
4)synchronized是关键字,而ReentrantLock是类。
5)synchronized是依赖于JVM实现的,而ReentrantLock是JDK实现的
6)ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。
7)ReentrantLock提供一个Condition条件类,用来实现分组唤醒需要唤醒的线程,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
8)ReentrantLock提供了一种能够中断等待锁的线程机制,通过lock.lockInterruptibly()来实现这个机制。

6.线程池的工作原理?Java并发类库提供的线程池有哪些?分别有什么特点?
1)线程池判断核心线程池里面的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务,如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满了。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3)线程池判断线程是否都处于工作状态。如果没有则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。在这里插入图片描述线程池的作用?
减少资源开销,可以减少每次创建销毁线程的开销,提高响应速度。由于线程已经创建成功提高线程的可管理性。

线程池的组成部分?
主要有两部分组成,多个工作线程和一个阻塞队列。其中工作线程是一组已经处在运行中的线程,他们不断地向阻塞队列中领取任务执行,而阻塞对列用于存储工作线程来不及处理的任务。

细说一下线程的组成,创建一个线程池需要的一些核心参数?
corePoolSize:基本线程数量 它表示希望线程池达到的值,线程池会尽量把实际线程数量保持在这个值的上下。
maximumPoolSize:最大线程数量 如果实际线程数量达到这个值且阻塞队列未满,任务将存入阻塞队列等待执行;阻塞队列已满则调用饱和策略。
keepAliveTime:空闲线程存活时间 当实际线程数量超过基本线程数量时,若线程空闲的时间超过该值,就会被停止。
runnableTaskQueue:任务队列 这是一个存放任务的阻塞队列,可以有如下几种选择:ArrayBlockingQueue(是由数组实现的阻塞队列,FIFO)LinkedBlockingQueue(是由链表实现的阻塞队列,FIFO)SynchronousQueue(是没有存储空间的阻塞队列)PriorityBlockingQueue(是一个优先权阻塞队列,当阻塞队列已满时,就会调用饱和策略)

线程池的运行机制?
当有请求到来时:
1)若当前实际线程数量少于corePoolSize,即使有空闲线程,也会创建一个新的工作线程;
2)若当前线程数量处于corePoolSize和maximumPoolSize之间,并且阻塞队列没满,则任务将被放入阻塞队列中等待执行;
3)若当前实际线程数量小于maximumPoolSize,但阻塞队列已满,则直接创建新线程处理任务;
4)若当前实际线程数量已经到达maximumPoolSize,并且阻塞队列已满,则采用饱和策略。

Java并发类库提供的线程池有哪些?分别有什么特点?
线程池创建配置:
1)newCachedThreadPool():用来处理大量短时间工作任务的线程池。当无缓存线程可以用时,就会创建新的工作线程;如果线程闲置的时间超过60秒,则被终止并移除缓存;其内部使用SynchronousQueue为工作队列。适用于:执行很多短期异步的小程序或者负载较轻的服务器。
2)newFixedThreadPool(int nThreads),重用指定数目(n Threads)的线程,其背后使用的是无界的工作队列,任何时候最多有n Threads
个工作线程是活跃的。适用于:执行长期的任务,性能好很多。
3)newSingleThreadExecutor():他的特点在于工作线程数目被限制为了1,操作一个无界的工作队列,所以它保证了所有任务都是被顺序执行,做多有一个任务处于活跃状态。适用于:一个任务一个任务执行的场景。
4)newSingleThreadExecutor()和newScheduledThreadPool(int corePoolSize),创建的是一个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于是单一工作线程还是多个工作线程。newScheduleThreadPool适用于周期性执行任务的场景。
5)newWorkStealingPool(int parallelism),Java 8才加入这个创建方法,并行处理任务,不保证处理顺序。
6)ThreadPoolExecutor():还最原始的线程池创建,上面的1-3都是对ThreadPoolExecutor的封装。

线程池有哪些状态?
Running :接收新的任务,处理等待队列中的任务。
ShutDown:不会接受新的任务提交,但是会继续处理等待队列中的任务。
Stop:不接受新任务的提交,也不再处理等待队列中的任务,中断正在执行任务的线程。
Tidying:所有任务都销毁了,workCount为0,线程池的状态在转化为Tidying状态时,会执行terminated()方法。
Terminated:terminated()方法结束后,线程池的状态就会变成这个。

7.AtomicInteger底层实现原理是什么?
AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS(compare and swap)技术。从AtomicInteger的内部属性可以看出,它依赖于Unsafe提供的一些底层能力,进行底层操作,以volatile的value字段,记录数值,以保证可见性,Unsafe会利用value字段的内存地址偏移,直接完成操作。

8.voliate的实现原理?
volatile可以保证线程可见性且禁止指令重排序,但是无法保证原子性。在JVM底层voliate是采用“内存屏障”来实现的,加入volatile关键字时,汇编后会多出一个lock前缀指令。lock前缀指令就相当于一个内存屏障。当读取一个被volatile修饰的变量时,会从共享内存中读,而非线程专属的存储空间中读;当volatile变量被写入后,线程本地内存中共享变量就会置为失效状态,因此线程B需要再从主内存中读取该变量的最新值。当一个共享变量被volatile修饰时,它会保证修改的值会立即更新到主内存,当有其他线程需要读取时,它会去内存中读取新值。

9.synchronized和volatile的区别是什么?
1)volatile是变量修饰符;synchronized是修饰类、方法、代码块。
2)volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized可以保证变量的修改可见性和原子性。
3)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

10.ThreadLocal的底层原理?
ThreadLocal又叫线程本地量,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序的执行性能。在每个线程Thread内部有一个ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。ThreadLocal相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。在进行get之前必须先set,否则会报空指针异常;否则必须重写initialValue()方法。
使用场景:数据库连接和session管理 该类提供了线程局部变量,这些变量不同于他们的普通对应物,因为访问某个变量(通过get或set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。每个线程都是保持对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal实例是可以访问的;线程消失之后,其线程局部实例的所有副本都会被垃圾回收。

实现原理:
ThreadLocal并不维护ThreadLocalMap,并不是一个存储数据的容器,他只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等;而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,并且该容器由Thread维护。每一个Thread对象均含一个ThreadLocalMap类型的成员变量ThreadLocals,它存储本线程中所有ThreadLocal对象以及其对应的值(ThreadLocalMap是个弱引用类,内部一个Entry由ThreadLocal对象和value值构成。为什么要用弱引用呢?因为采用弱引用,使用完之后垃圾回收器会自动清理key,不用再关注指针问题;如果直接new一个对象的话,使用完后还将值设置为null后才能被垃圾回收器清理。)进行set、get等操作都是先获取当前线程对象,然后再获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,再做相应的处理。在ThreadLocalMap中只有key为弱引用,value仍为强引用。每次进行set、get、remove操作时,ThreadLocal都会讲key为null的Entry自动清理,从而避免了内存泄漏问题。

11.CountDownLatch与CyclicBarrier之间的区别以及使用场景?
CountDownLatch:一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier:N个线程互相等待,任何一个线程完成之前,所有线程必须等待。
CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。
CyclicBarrier是所有线程进行等待,直到所有线程都准备好进去await()方法之后,所有线程才同时开始执行。
总结:1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。
2)CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
3)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来知道阻塞的线程是否被中断。
使用场景:需要等待某个条件达到要求后才能做后面的事情,同时当线程都完成后也会触发事件,可以使用CountDownLatch。CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。

12.死锁是什么?产生死锁的条件?如何避免死锁?
死锁是指两个或两个以上的进程在执行过程中,因为争夺资源而造成的一种相互等待的现象,若无外力作用,它们都无法继续推进下去。
死锁产生的条件:1.互斥条件:一个资源每次只能被一个进程使用。2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。3.不剥夺条件:进程已获得的资源在未使用完之前,不嫩强行剥夺。4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁的方法:1)尽量避免使用多个锁,并且只有在需要时才持有锁,嵌套的synchronized或者lock非常容易出问题。2)如果必须要使用多个锁,尽量设计好锁的获取顺序。3)使用带超时的方法为程序带来更多可控性。4)尽量不要几个功能使用同一把锁。

怎么定位死锁线程?
最常见的方式就是利用jstack等工具获取线程栈,然后就能定位相互之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack等就能直接定位,jConsole甚至可以在图形界面进行有限的死锁检测。如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决,只能重启、修正程序本身问题。所以代码开发阶段相互审查或者利用工具进行预防性排查往往也是很重要的。首先可以使用jps或者系统的ps命令、任务管理器等工具,确定进程ID;其次调用jstack获取线程栈,最后分析得到结果。

怎么写一个死锁?
定义两个ArrayList,将他们都加锁锁A,B。线程1拿住锁A,请求锁B;线程2拿住锁B,请求锁A,在等待对方释放锁的过程中谁也不让出已获得的锁。
public class DeadLock{ public static void main(String[] args){ final List<Integer> list1=Arrays.asList(1,2,3); final List<Integer> list2=Arrays.asList(4,5,6); new Thread(new Runnable(){ @Override public void run(){ synchronized(list1){ for (Integer i:list1){ System.out.println(i);} try{ Thread.sleep(1000); }catch(InterruptException e){ e.printStackTrace(); } synchronized(list2){ for(Integer i:list2){ System.out.println(i); } } } } }).start(); new Thread(new Runnable(){ @Override public void run(){ synchronized(list2){ for (Integer i:list2){ System.out.println(i);} try{ Thread.sleep(1000); }catch(InterruptException e){ e.printStackTrace(); } synchronized(list1){ for(Integer i:list1){ System.out.println(i); } } } } }).start(); } }

13.Java多线程中调用wait()和sleep()方法有什么不同?
基本的差别:sleep是Thread类的方法,而wait是Object类中定义的方法;sleep()方法可以在任何地方使用,而wait()方法只能在synchronized方法或者synchronized块中使用。
最主要的本质区别:Thread.sleep只会让出CPU,不会导致锁行为的改变即不会释放锁资源;Object.wait不仅让出CPU,还会释放已经占有的同步资源锁。

14.一个线程两次调用start()方法会出现什么情况?谈谈线程的生命周期和状态转移。
Java的线程是不允许启动两次的,第二次调用必然会抛出InlegalThreadStateException,这是一种运行时异常,多次调用start会被认为是编程错误。
6个状态:
新建(New):创建后尚未启动的线程状态。
运行(Runnable):包含Running和Ready。
无限期等待(waiting):不会被分配CPU执行时间,需要显式被调用才会继续执行。
限期等待(Timed Waiting):在一定时间后会有系统自动唤醒。
阻塞(Blockd):等待获取排它锁。
结束(terminated):已终止线程的状态,线程已经结束执行。
在这里插入图片描述线程的run()和start()有什么区别?
在这里插入图片描述

1.start()方法用于启动线程,run()方法用于执行线程的运行时代码。2.run()可以重复调用,而start()只能调用一次。
底层start()
方法是用C语言写的,调用JVM_StartThread,开启子线程,然后调用里面的run方法。

Runnable和Callable的区别?
1.Callable规定可以重写的方法是call(),而Runnable规定可以重写的方法是run()。2.Callable的任务执行后可以有返回值,而Runnable的任务完成后不能有返回值,3.Call()方法可以抛出异常,run()方法不可以。

15.什么是CAS?
CAS是compare and swap的缩写,即比较交换。CAS是一种乐观锁,包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B.CAS是通过无限循环来获取数据的,若在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才能有可能会执行。
缺点:1.ABA问题:线程将数值a修改成了b,另外一个线程又将b修改成了a,此时CAS认为是没有变化的,但其实已经变化了。解决方法:在数值改变后加上一个版本号用于标识。
2.CAS机制只能保证一个变量的原子性操作,而不能保证整个代码块的原子性。
3.CAS造成CPU利用率增加。

16.什么是AQS?
AQS是AbstractQueueSynchronizer的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore。是CAS自旋Volatile变量:它使用了一个Volatile成员变量表示同步状态,通过CAS修改该变量的值,修改成功的线程获得该锁;若没有修改成功,或者发现state是加锁状态,则被添加到等待队列中,并挂起等待被唤醒。
AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞是会进入此队列)。

17.final不可变对象对多线程有什么帮助?为什么喜欢用final变量?
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
final变量在并发当中,原理是通过禁止CPU的指令集重排序,保证了对象的内存可见性,final域能确保初始化过程中的安全性,防止对象引用在被完全构造完成之前被其他线程拿到并使用(final可以保证正在创建中的对象不能被其他线程访问到)。

18.进程间的通信方式有几种?
管道:可用于具有亲缘关系的父子进程间的通信又名管道。
信号:用于通知接收进程某个事件已经发生。
消息队列:
共享内存:这是最有用的进程之间的通信方式,多个进程可以访问同一块内存空间。
信号量:进程之间及同一种进程的不同线程之间的同步和互斥手段。
套接字:用于网络中不同机器之间的进程通信。

19.线程同步的方式?
互斥量:Synchronized/Lock
信号量:Wait/Notify

20.为什么线程通信的方法wait(),notify()和notifyAll()被定义在Object类里?
在Java中,任意对象都可以当做锁来使用,由于锁对象的任意性,所以这些通信方法需要被定义在Object类中。

21.stop()和suspend()方法的区别?
stop()方法是不安全的,它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们,所以不推荐使用。
suspend()方法容易发生死锁,调用suspend()方法时,目标线程会停下来,但却仍然持有在这之前获得的锁,此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。

22.一个线程运行时发生异常会怎样?
如果异常没有被捕获,该线程将会停止执行。

23.分布式锁的实现,目前比较常用的几种方案?
分布式锁应该怎么实现?
1)可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
2)这把锁要是一把可重入锁,避免死锁。
3)这把锁最好是一把阻塞锁。
4)有高可用的获取锁和释放锁功能。
5)获取锁和释放锁的性能要好。

比较常见的几种方案:
1.基于数据库实现分布式锁
最简单的方式就是直接创建一张锁表。存在的问题:1)这把锁强依赖数据库的可用性。数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。2)这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得锁。3)这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错,没有获得锁的线程并不会进入排队队列,想要再次获得锁就要再次触发获得锁的操作。4)这把锁是非重入的,同一个线程再没有释放锁之前无法再次获得该锁,因为数据库中的数据已经存在了。
如何解决这些问题?
数据库是单点的?搞两个数据库,数据双向同步,一旦挂掉。快速切换到备用库上。
没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
非阻塞的?搞一个while循环,直到insert操作成功再返回。
非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候直接先查询数据库,如果当前主机信息和线程信息能够在数据库中查询到的话,直接把锁分配给它就行。
2)借助数据中自带的锁来实现分布式锁
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。(InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁,通过connection.commit()操作来释放锁)。
3)基于缓存(Redis,Memcached,tair)实现分布式锁
基于Redis的Setnx()、Expire()方法设置过期时间来做分布式锁。
4)基于Zookeeper实现分布式锁

24.HashTable的size()方法为什么要同步?
对于类的非同步方法,可以多条线程同时访问。如果线程A执行了put方法,而线程B正在执行size方法,则会导致数据不一致。

25.ConcurrentHashMap的size()方法如何实现同步?
JDK1.7和JDK1.8对size的计算是不一样的。1.7中是先不加锁计算三次,如果三次结果不一样,再加锁;1.8是通过对baseCount和counterCell进行CAS计算,最终通过遍历CounterCell数组得出size。

26.如何判断线程是否安全?
考虑原子性、可见性、有序性。
1)明确哪些代码是多线程运行的代码。
2)明确共享数据对共享变量的操作是不是原子操作,保证原子性是通过加锁或者同步,synchronized和volatile能保证有序性,但volatile不能保证原子性。
3)明确多线程运行代码中哪些语句是操作共享数据。
4)该对象是否会被多个线程访问修改,是的话是否有加锁操作。
5)注意静态变量,由于静态变量时属于该类和该类下所有对象共享的,可以通过类名直接访问或修改,因此在多线程的环境下,所有对静态变量的修改都会发生线程安全问题。

27.notify()和notifyAll()方法的区别?
notify只会唤醒等待该锁的其中一个进程,而notifyAll是唤醒等待该锁的所有线程。
注意:在while循环里使用wait,而不是if语句中使用,因为while循环在线程睡眠前后都会检查wait的条件,并在条件实际上未改变的情况下处理唤醒通知。永远在synchronized的函数或者对象里使用wait、notify和notifyAll,不然Java虚拟机会生成InlegalMonitorStateException。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值