目录
文章目录
- 目录
- 一、什么是进程?
- 二、什么是线程?
- 三、线程和进程的区别?
- 四、创建线程有哪几种方式?
- 五、run方法是干什么的?
- 六、为什么调用要start来启动线程而不直接去调用run方法?
- 七、继承Thread和实现Runnable有啥区别?
- 八、线程有哪些状态?
- 九、守护线程是什么?
- 十、并行和并发有什么区别?
- 十一、runnable 和 callable 有什么区别?
- 十二、sleep() 和 wait() 有什么区别?
- 十三、notify()和 notifyAll()有什么区别?
- 十四、创建线程池有哪几种方式?
- 十五、线程池都有哪些状态?
- 十六、线程池的四种拒绝策略
- 十七、线程池中 submit()和 execute()方法有什么区别?
- 十八、在 JAVA 程序中怎么保证多线程的运行安全?
- 十九、多线程中synchronized锁的升级原理是什么?
- 二十、什么是死锁?
- 二十一、怎么防止死锁?
- 二十二、ThreadLocal 是什么?有哪些使用场景?
- 二十三、简述 synchronized 底层实现原理?
- 二十四、能不能将synchronized 添加在run方法上,为什么?
- 二十五、synchronized 和 Lock 有什么区别?
- 二十六、synchronized 和 ReentrantLock 区别是什么?
- 二十七、synchronized 和volatile的区别是什么?
- 二十八、 简述 atomic 的原理?
- 二十七、synchronized 和volatile的区别是什么?
- 二十八、 简述 atomic 的原理?
一、什么是进程?
软件需要安装操作系统之上,软件安装之后,需要找到核心的启动文件来运行软件。
运行软件:就是让软件执行,其实是将软件中的程序从硬盘加载到内存中,在内存中开辟当前软件所需要的内存空间,而负责运行这个软件的那个内存空间就称为当前软件的一个进程。
简而言之,进程就是正在执行的一个软件(程序)。
二、什么是线程?
它是程序在执行过程中的一个独立的运行单元,它是软件在运行过程中真正干活的地方。
一个软件运行之后,有一个独立的进程,这个进程负责整个软件的生命周期。但是在一个进程中最少有一个线程,它负责具体的功能逻辑。
1、多线程
就是一个进程中运行着多个线程。使用多线程的目的,是为了提高程序的执行效率。
2、多线程执行原理
多线程最终都是需要CPU处理,而CPU(例如单核)真正在某个时间点上它本质上只能处理一个线程,而由于CPU 可以在多个线程之间进行高速切换,导致我们误认为好像是多个程序同时在执行。
由于现在的电脑的CPU都是多核,导致某个时间点上同时会有多个核心处理不同的线程。
3、是不是线程越多越好?
在合理的范围内去使用CPU的核心数,是可以提高程序的效率,但是如果无限的开启线程,会降低CPU的执行效率,可能导致死机。
三、线程和进程的区别?
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程是进程的一个实体,是CPU调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
四、创建线程有哪几种方式?
1、 继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
//将类声明为Thread的子类
class Demo extends Thread{
//重写run方法(执行体)
public void run() {
for( int i = 0 ; i < 10 ; i++) {
System.out.println("demo run i = " + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
Demo d = new Demo();
//启动该线程
d.start();
for( int i = 0 ; i < 10 ; i++) {
System.out.println("main i = " + i);
}
}
}
2、通过Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
class Demo implements Runnable{
public void run() {
for(int i = 0 ; i < 10 ; i++) {
System.out.println("Demo run i = " + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 创建实现类的对象
Demo d = new Demo();
// 创建Thread的对象,将实现类对象作为参数传递
Thread t = new Thread(d);
// 开启线程
t.start();
for(int i = 0 ; i < 10 ; i++) {
System.out.println("main i = " + i);
}
}
}
上面2种方式创建线程都有一个缺陷:就是在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
3、通过Callable和Future创建线程
-
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
-
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
-
使用FutureTask对象作为Thread对象的target创建并启动新线程。
-
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
五、run方法是干什么的?
在Java中给给出Thread类来描述(封装)操作线程。最终线程是要被开启执行的,开启一个线程之后,需要告诉线程应该去干什么事情。而具体需要线程做的事情,Java给出了run方法,需要将具体的线程要执行的代码编写在其中。
简而言之,run方法,就是编写线程被启动之后需要执行的代码的方法。
六、为什么调用要start来启动线程而不直接去调用run方法?
创建Thread或Thread的子类对象,这时只是代表有线程本身这个对象而已,如果希望程序中能够出现多个线程同时执行的这种状态,就必须调用线程自身的 start 方法将线程开启。
如果直接通过线程对象本身去调用run方法,这时和普通的对象调用方法没有任何的区别。直接调用run方法,线程根本就没有开启,也就是说不会形成多个线程同时执行。
在调用start方法之后,JVM会自动的开辟一个新的线程,然后让这个新的线程去执行线程中的run方法中的代码。
七、继承Thread和实现Runnable有啥区别?
继承Thread,是让子类变成线程类。可是在某些情况下,类已经有父类,它无法再去继承Thread的。可是类中有部分代码又需要被多线程执行,那么只能让需要多线程操作的类去实现Runnable接口。
Thread类:
它是描述线程本身的,它提供操作线程的各种方法(start、stop、interrupt等等)。
Runnable接口:
它和线程没有直接的关系,它其中仅仅提供一个run方法,目的是将需要线程执行的任务书写在run方法中。最终需要将Runnable接口的实现类的对象交给Thread。
八、线程有哪些状态?
线程通常都有五种状态:创建、就绪、运行、阻塞和死亡。
- 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
- 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
- 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
- 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
九、守护线程是什么?
守护进程(线程)(daemon thread),是个服务线程,它的作用准确地来说就是服务其他的线程。它是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。
守护线程又被称为“服务进程”、“精灵线程”、“后台线程”,是指在程序运行是在后台提供一种通用的线程。 通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。
JAVA里线程分2种:
1、守护线程,比如垃圾回收线程,就是最典型的守护线程。
2、用户线程,就是应用程序里的自定义线程。
十、并行和并发有什么区别?
并行是指两个或者多个事件在同一时刻发生,是在不同实体上的多个事件。如:多个处理器或多核处理器同时处理多个任务(hadoop分布式集群);
并发是指两个或多个事件在同一时间间隔发生,是在同一实体上的多个事件。如:多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
打个比方:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
十一、runnable 和 callable 有什么区别?
- Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
- Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
十二、sleep() 和 wait() 有什么区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,它不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。
类的不同:sleep()来自Thread,wait()来自Object。
释放锁:sleep不释放锁;wait释放锁。
用法不同:sleep时间到会自动恢复;wait可以使用notify() / notifyAll()直接唤醒。
十三、notify()和 notifyAll()有什么区别?
1、如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
2、当有线程调用了对象的 notifyAll ()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
3、优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
十四、创建线程池有哪几种方式?
线程池创建有七种方式,最核心的是最后一种:
1、newSingleThreadExecutor():
它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
2、newCachedThreadPool():
它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列;
3、newFixed ThreadPool(int nThreads):
重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads;
4、newSingleThreadScheduledExecutor0:创建单线程池,返回ScheduledExecutorService,可以进行定时或周期性的工作调度;
5、newScheduled ThreadPool(int corePoolSize):
它和newSingleThreadScheduledExecutor()类似,创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
6、newWorkStealingPool(int parallelism):
这是一个经常被人忽略的线程池,Java8才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
7、ThreadPoolExecutor():
是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
十五、线程池都有哪些状态?
线程池有5种状态:
1、Running
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
2、ShutDown
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、Stop
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、Tidying
(1) 状态说明:当所有的任务已终止,ctl 记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、Terminated
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
说明:
ctl是一个AtomicInteger类型的原子对象。ctl记录了"线程池中的任务数量"和"线程池状态"2个信息。ctl共包括32位。其中,高3位表示"线程池状态",低29位表示"线程池中的任务数量"。
各个状态切换框架图:
十六、线程池的四种拒绝策略
线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施。当任务添加到线程池中之所以被拒绝,可能是由于:第一,线程池异常关闭。第二,任务数量超过线程池的最大限制。
线程池共包括4种拒绝策略,它们分别是:AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy和DiscardPolicy。
AbortPolicy -- 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。
CallerRunsPolicy -- 当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。
DiscardOldestPolicy -- 当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
DiscardPolicy -- 当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。
线程池默认的处理策略是AbortPolicy!
十七、线程池中 submit()和 execute()方法有什么区别?
1、接收的参数不一样
2、submit有返回值,而execute没有
3、submit方便Exception处理
4、execute():只能执行Runnable类型的任务。
submit():可以执行Runnable和Callable类型的任务。
十八、在 JAVA 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
1、原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
2、可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3、有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
方法一:使用安全类,比如Java.util.concurrent下的类。
方法二:使用自动锁 synchronized。
方法三:使用手动锁Lock。
十九、多线程中synchronized锁的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。
synchronized 锁升级原理:在锁对象的对象头里面有一个thread id字段,在第一次访问的时候 thread id 为空,jvm让其持有偏向锁,并将 thread id 设置为其线程id,再次进入的时候会先判断 thread id 是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在Java6之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
锁升级的图示过程:
二十、什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
如:当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就
会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
二十一、怎么防止死锁?
死锁的四个必要条件:
1、互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
2、请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放;
3、不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放;
4、环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
1、尽量使用 tryLock(long timeout,TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),
设置超时时间,超时可以退出防止死锁。
2、尽量使用Java.util.concurrent 并发类代替自己手写锁。
3、尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
4、尽量减少同步的代码块。
二十二、ThreadLocal 是什么?有哪些使用场景?
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 的经典使用场景是数据库连接和 session管理等。
二十三、简述 synchronized 底层实现原理?
synchronized 是由一对monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在Java6之前,monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在Java6的时候,Java虚拟机对此进行了大刀阔斧地改进,提供了三种不同的monitor实现,也就是常说的三种不同的锁:
二十四、能不能将synchronized 添加在run方法上,为什么?
从语法上讲,是可以在run方法上添加同步关键字,让整个run方法同步。但实际是不允许的。
因为run方法是线程要执行的任务方法,也就是线程只有进入到run方法中才能执行任务,而将同步添加在run方法上,意味着线程都无法进入到run方法,更无法执行任务。而线程出run方法,就意味着当前这个线程已经将任务执行完成了。其他线程进入run方法也没有意义。
二十五、synchronized 和 Lock 有什么区别?
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手动释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
二十六、synchronized 和 ReentrantLock 区别是什么?
synchronized 早期的实现比较低效,对比Reentrantlock,大多数场景性能都相差较大,但是在Java6中对synchronized进行了非常多的改进。
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
- ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantLock可以灵活地实现多路通知
- 另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
- ReentrantLock 必须手动获取与释放锁,而 synchronized不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
- volatile 标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
二十七、synchronized 和volatile的区别是什么?
volatile是变量修饰符;synchronized是修饰类、方法、代码段。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
二十八、 简述 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
可用于修饰方法、代码块等。
- volatile 标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
二十七、synchronized 和volatile的区别是什么?
volatile是变量修饰符;synchronized是修饰类、方法、代码段。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
二十八、 简述 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题