在java 中守护线程和用户线程的区别?
java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
任何线程都可以设置为守护线程和用户线程,通过方法
Thread.setDaemon(bool on);true 则把该线程设置为守护线程,反之则为用户线
程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,
如果全部的User Thread 已经结束,Daemon 没有可服务的线程,JVM 关闭。
扩展:Thread Dump 打印出来的线程信息,含有daemon 字样的线程即为守
护进程
线程与进程的区别
进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
一个程序至少有一个进程,一个进程至少有一个线程。
什么是多线程中的上下文切换
多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU
数量时,为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切
换使用CPU 发生的切换数据等就是上下文切换。
死锁与活锁的区别,死锁与饥饿的区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而
造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
互斥条件:所谓互斥就是进程在某一时间内独占资源。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复
尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”,
而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无
法执行的状态。
synchronized 底层实现原理
synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同
步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令
monitorenter 和monitorexit 来实现,相对于普通方法,其常量池中多了
ACC_SYNCHRONIZED 标示符。
JVM 就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将
会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线
程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放
monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor 对象。
注意,这个问题可能会接着追问,java 对象头信息,偏向锁,轻量锁,重量
级锁及其他们相互间转化。
什么是线程组,为什么在Java 中不推荐使用?
ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程
对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形
式。
1.线程组ThreadGroup 对象中比较有用的方法是stop、resume、suspend 等
方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方
废弃掉了,所以线程组本身的应用价值就大打折扣了。
2.线程组ThreadGroup 不是线程安全的,这在使用过程中获取的信息并不全
是及时有效的,这就降低了它的统计使用价值。
什么是Executors 框架?为什么使用Executor 框架?
Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务
的框架。
每次执行任务创建线程new Thread()比较消耗性能,创建一个线程是比较耗
时、耗资源的。
调用new Thread()创建的线程缺乏管理,而且可以无限制的创建,线程之间
的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替
也会消耗很多系统资源。
接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、
定时定期执行、线程中断等都不便实现。
在Java 中Executor 和Executors 的区别?
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足
业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了Executor 接口并进行了扩展,提供了更多的方法
我们能获得任务执行的状态并且可以获取任务的返回值。
使用ThreadPoolExecutor 可以创建自定义线程池。
什么是原子操作?在Java Concurrency API 中有哪些原子类(atomic classes)?
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子
操作。
在Java 中可以通过锁和循环CAS 的方式来实现原子操作。CAS 操作——
Compare & Set,或是Compare & Swap,现在几乎所有的CPU 指令都支持CAS 的
原子操作。java.util.concurrent.atomic 下提供了大量的原子操作类,比如原子类:
AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference ,原子数组:
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray ,原子属性更新器:
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
Java Concurrency API 中的Lock 接口(Lock interface)是什么?对比
synchronized 它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。
他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关
类的条件对象。
它的优势有:可以使锁更公平,可以使线程在等待锁的时候响应中断,可以
让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,可以在
不同的范围,以不同的顺序获取和释放锁。
整体上来说Lock 是synchronized 的扩展版,Lock 提供了无条件的、可轮询
的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条
件队列的(newCondition 方法)锁操作。另外Lock 的实现类基本都支持非公平锁(默
认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁
是高效的选择。
什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现
生产者-消费者模型?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。
当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,
消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费
者也只从容器里拿元素。
JDK7 提供了7 个阻塞队列。在实现上,主要是利用了Condition 和Lock 的等
待通知模式。
什么是Callable 和Future?
Callable 接口类似于Runnable,从名字就可以看出来了,但是Runnable 不会
返回结果,并且无法抛出返回结果的异常,而Callable 功能更强大一些,被线程
执行后,可以返回值,这个返回值可以被Future 拿到,也就是说,Future 可以
拿到异步执行任务的返回值。
可以认为是带有回调的Runnable。
Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说
Callable 用于产生结果,Future 用于获取结果。
什么是FutureTask?
在Java 并发程序中FutureTask 表示一个可以取消的异步运算。它有启动和
取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结
果才能取回,如果运算尚未完成get 方法将会阻塞。一个FutureTask 对象可以对
调用了Callable 和Runnable 的对象进行包装,由于FutureTask 也是调用了
Runnable 接口所以它可以提交给Executor 来执行。
什么是并发容器的实现?
何为同步容器:可以简单地理解为通过synchronized 来实现同步的容器,如
果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector,Hashtable,
以及Collections.synchronizedSet,synchronizedList 等方法返回的容器。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸
缩性,例如在ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为
分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读
操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作
线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。
多线程同步和互斥有几种实现方法,都是什么?
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个
线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤
醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当
有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,
其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以
看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核
模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用
户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内
核模式下的方法有:事件,信号量,互斥量。
什么是竞争条件?
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运
行的顺序时,则我们认为这发生了竞争条件(race condition)。
为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调
用run()方法?
当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。
但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程
的代码,只会把run 方法当作普通方法去执行。
在Java 中CycliBarriar 和CountdownLatch 有什么区别?
CyclicBarrier 可以重复使用,而CountdownLatch 不能重复使用。
什么是不可变对象,它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,
也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多
不可变类,如String、基本类型的包装类、BigInteger 和BigDecimal 等。
不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。
既然它们的状态无法修改,这些常量永远不会变。
不可变对象永远是线程安全的。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是final 类型;并且,
它被正确创建
notify()和notifyAll()有什么区别?
当一个线程进入wait 之后,就必须等其他线程notify/notifyall,使用notifyall,
可以唤醒所有处于wait 状态的线程,使其重新进入锁的争夺队列中,而notify
只能唤醒一个。
如果没把握,建议notifyAll,防止notigy 因为信号丢失而造成程序异常。
什么是可重入锁(ReentrantLock)?谈谈它的实现。
线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,
synchronized、ReentrantLock 都是可重入的锁。在实现上,就是线程每次获取锁
时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每释放一次锁,
进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。
当一个线程进入某个对象的一个synchronized 的实例方法后,其它线程是
否可进入此对象的其它方法?
如果其他方法没有synchronized 的话,其他线程是可以进入的。
所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所
以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
Java 里面的同步原语synchronized 关键字的实现是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,
所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,可以使用版本号等机制。在Java 中j 原子变量类就是使用了乐观锁的一种实
现方式CAS 实现的。
乐观锁的实现方式:
使用版本标识来确定读到的数据与提交时的数据是否一致。提交
后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
java 中的Compare and Swap 即CAS ,当多个线程尝试使用CAS
同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程
都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以
再次尝试。
什么是CAS 操作,缺点是什么?
CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新
值,否则不做任何事儿,但是要返回原值是多少。每一个CAS 操作过程都包含三
个运算符:一个内存地址V,一个期望的值A 和一个新值B,操作的时候如果这
个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做
任何操作。
CAS 缺点:
ABA 问题:
比如说一个线程one 从内存位置V 中取出A,这时候另一个线程two 也从内
存中取出A,并且two 进行了一些操作变成了B,然后two 又将V 位置的数据变
成A,这时候线程one 进行CAS 操作发现内存中仍然是A,然后one 操作成功。
尽管线程one 的CAS 操作成功,但可能存在潜藏的问题。从Java1.5 开始JDK 的
atomic 包里提供了一个类AtomicStampedReference 来解决ABA 问题。
循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从
而浪费更多的CPU 资源,效率低于synchronized。
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS 的方式来保证原子操
作,但是对多个共享变量操作时,循环CAS 就无法保证操作的原子性,这个时候
就可以用锁。
SynchronizedMap 和ConcurrentHashMap 有什么区别?
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线
程来访为map。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
写时复制容器可以用于什么应用场景?
CopyOnWrite 并发容器用于对于绝大部分访问都是读,且只是偶尔写的并发
场景。比如白名单,黑名单,商品类目的访问和更新场景。
透露的思想
读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突
volatile 有什么用?能否用一句话说明下volatile 的应用场景?
volatile 保证内存可见性和禁止指令重排。
volatile 用于多线程环境下的一写多读,或者无关联的多写。
为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,
但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
在单线程环境下不能改变程序运行的结果;
存在数据依赖关系的不允许重排序
在java 中wait 和sleep 方法的不同?
最大的不同是在等待时wait 会释放锁,而sleep 一直持有锁。Wait 通常被用
于线程间交互,sleep 通常被用于暂停执行。
一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。hread.UncaughtExceptionHandler
是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异
常将造成线程中断的时候JVM 会使用Thread.getUncaughtExceptionHandler()来查
询线程的UncaughtExceptionHandler 并将线程和异常作为参数传递给handler 的
uncaughtException()方法进行处理。
为什么wait, notify 和notifyAll 这些方法不在thread 类里面?
JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获
得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果
wait()方法定义在Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,
由于wait,notify 和notifyAll 都是锁级别的操作,所以把他们定义在Object 类中
因为锁属于对象。
什么是ThreadLocal 变量?
ThreadLocal 是Java 里一种特殊的变量。每个线程都有一个ThreadLocal 就是
每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。
Java 中interrupted 和isInterrupted 方法的区别?
interrupted() 和isInterrupted()的主要区别是前者会将中断状态清除而后者
不会。Java 多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来
中断一个线程就会设置中断标识为true。当中断线程调用静态方法
Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法
isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。
为什么wait 和notify 方法要在同步块中调用?
主要是因为Java API 强制要求这样做,如果你不这么做,你的代码会抛出
IllegalMonitorStateException 异常。
为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等
待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒
来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等
待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更
好的原因
怎么检测一个线程是否拥有锁?
在java.lang.Thread 中有一个方法叫holdsLock(),它返回true 如果当且仅当
当前线程拥有某个具体对象的锁。
你如何在Java 中获取线程堆栈?
kill -3 [java pid]
不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,kill -3
tomcat pid, 输出堆栈到log 目录下。
Jstack [java pid]
这个比较简单,在当前终端显示,也可以重定向到指定文件中。
或者使用Java 提供的拟机线程系统的管理接口
ManagementFactory.getThreadMXBean()。
Java 线程池中submit() 和execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定
义在Executor 接口中。
而submit()方法可以返回持有计算结果的Future 对象,它定义在
ExecutorService 接口中,它扩展了Executor 接口
你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优
先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。
我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的
线程前执行。线程优先级是一个int 变量(从1-10),1 代表最低优先级,10 代表
最高优先级。
java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优
先级有关,如非特别需要,一般无需设置线程优先级。
你如何确保main()方法所在的线程是Java 程序最后结束的线程?
可以使用Thread 类的join()方法(或者CountDownLatch 工具类)来确保所
有程序创建的线程在main()方法退出前结束。
为什么Thread 类的sleep()和yield ()方法是静态的?
Thread 类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其
他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是
静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以
在其他非运行线程调用这些方法。
现在有T1、T2、T3 三个线程,你怎样保证T2 在T1 执行完后执行,T3 在
T2 执行完后执行?
可以用join 方法实现。
你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,
以此来保持它的完整性,你会怎样去实现它?
volatile 关键字,读写锁,写时复制等等都可以实现。
用Java 写代码来解决生产者——消费者问题。
阻塞队列实现即可,也可以用wait 和notify 来解决这个问题,或者用
Semaphore
使用共享变量的方式
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任
务的线程用来作为是否中断的信号,通知中断线程的执行。
使用interrupt 方法终止线程
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?
比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join()方法,
或者Thread.sleep()方法,在网络中调用ServerSocket.accept()方法,或者调用了
DatagramSocket.receive()方法时,都有可能导致线程阻塞,使线程处于处于不可
运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本
无法检查循环标志,当然也就无法立即中断。所以应该尽量使用Thread 提供的
interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使
一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。
JVM 中哪个参数是用来控制线程的栈堆栈大小的
-Xss
如果同步块内的线程抛出异常锁会释放吗?
会
单例模式的双重检查实现是什么?为什么并不安全?如何在Java 中创建线
程安全的Singleton?
不安全的根本原因是重排序会导致
未初始化完成的对象可以被其他线程看见而导致错误。创建安全的单例模式有:
延迟占位模式、在声明的时候就new 这个类的实例、枚举
写出3 条你遵循的多线程最佳实践
给你的线程起个有意义的名字。这样可以方便找bug 或追踪。
OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比Thread-1.
Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所
有的主要框架甚至JDK 都遵循这个最佳实践。
避免锁定和缩小同步的范围锁花费的代价高昂且上下文切换更耗费时间空
间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢
同步块,它给我拥有对锁的绝对控制权。
多用同步类少用wait 和notify 首先,CountDownLatch, Semaphore,
CyclicBarrier 和Exchanger 这些同步类简化了编码操作,而用wait 和notify 很难
实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK
中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹
灰之力获得优化。
多用并发集合少用同步集合这是另外一个容易遵循且受益巨大的最佳实践,
并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。
比如并发编程的黄金原则,尽量无锁化编程等等………
请概述锁的公平和非公平,JDK 内部是如何实现的。
公平锁是指所有试图获得锁的线程按照获取锁的顺序依次获得锁,而非公平
锁则是当前的锁状态没有被占用时,当前线程可以直接占用,而不需要等待。在实
现上,非公平锁逻辑基本跟公平锁一致,唯一的区别是,当前线程不需要判断同
步队列中是否有等待线程。
非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真
正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu 的时间片,
尽量的减少cpu 空闲的状态时间。
使用场景的话呢,其实还是和他们的属性一一相关,比如:如果业务中线程
占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公
平锁可以保证不会有线程被饿死。
请概述AQS
是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、
ReentrantReadWriteLock 和CountDownLatch 就是基于AQS 实现的。它使用了一
个int 成员变量表示同步状态,通过内置的FIFO 队列来完成资源获取线程的排队
工作。它是CLH 队列锁的一种变体实现。它可以实现2 种同步方式:独占式,共
享式。
AQS 的主要使用方式是继承,子类通过继承AQS 并实现它的抽象方法来管
理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步
工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared 等等。
这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与
同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同
步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程
的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注
的领域。
在内部,AQS 维护一个共享资源state,通过内置的FIFO 来完成获取资源线
程的排队工作。该队列由一个一个的Node 结点组成,每个Node 结点维护一个
prev 引用和next 引用,分别指向自己的前驱和后继结点,构成一个双端双向链
表。
同时与Condition 相关的等待队列,节点类型也是Node,构成一个单向链表。
请概述volatile
volatile 关键字的作用主要有两点:
多线程主要围绕可见性和原子性两个特性而展开,使用volatile 关键字修饰
的变量,保证了其在多线程之间的可见性,即每次读取到volatile 变量,一定是
最新的数据。但是volatile 不能保证操作的原子,对任意单个volatile 变量的读/
写具有原子性,但类似于++这种复合操作不具有原子性。。
代码底层在执行时为了获取更好的性能会对指令进行重排序,多线程下可能
会出现一些意想不到的问题。使用volatile 则会对禁止重排序,当然这也一定程
度上降低了代码执行效率。
同时在内存语义上,当写一个volatile 变量时,JMM 会把该线程对应的本地
内存中的共享变量值刷新到主内存,当读一个volatile 变量时,JMM 会把该线程
对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
在Java 中对于volatile 修饰的变量,编译器在生成字节码时,会在指令序列
中插入内存屏障来禁止特定类型的处理器重排序问题、强制刷新和读取。
在具体实现上,volatile 关键字修饰的变量会存在一个“lock:”的前缀。它
不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock 会对CPU 总线和
高速缓存加锁,可以理解为CPU 指令级的一种锁。
同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内
存的操作会使在其他CPU 里缓存了该地址的数据无效。