Java集合中线程安全的有哪些?
-
Vector->SynchronizedList->CopyOnWriteArrayList
-
SynchronizedSet->CopyOnWriteArraySet
-
SynchronizedMap->ConcurrentHashMap
线程的创建方式
-
继承Thread类创建线程
public class ThreadTest extends Thread{
//重写run()方法,run()方法的方法体是线程执行体
public void run(){
for(int i=0;i<5;i++){
//使用线程的getName()方法可以直接获取当前线程的名称
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args){
//输出Java程序运行时默认运行的主线程名称
System.out.println(Thread.currentThread().getName());
//创建第一个线程并开始执行
new ThreadTest().start();
//创建第二个线程并开始执行
new ThreadTest().start();
}
}
-
实现Runnable接口创建线程
public class ThreadTest implements Runnable{
private int i;
//重写run()方法,run()方法的方法体是线程执行体
public void run(){
//实现Runnable接口时,只能使用如下方法获取线程名
System.out.println(Thread.currentThread().getName() + " " + i);
i++;
}
public static void main(String[] args){
ThreadTest tt = new ThreadTest();
//创建第一个线程并开始执行
//输出 新线程1 0
new Thread(tt,"新线程1").start();
//创建第二个线程并开始执行
//输出 新线程2 1
new Thread(tt,"新线程2").start();
//使用Lambda表达式创建Runnable对象
new Thread(()->{
System.out.print("hello");
System.out.println("'world");
}).start();
}
}
-
使用Callable和Future创建线程
public class ThreadTest {
public static void main(String[] args){
//使用FutureTask来包装Callable对象
//使用Lambda表达式来创建Callable<Integer>对象
FutureTask<Integer> task = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() + " " + "开始执行任务!");
return 0;
});
//实质还是以Callable对象来创建并启动线程
//输出 新线程 开始执行任务!
new Thread(task,"新线程").start();
try{
//获取线程的返回值
//输出 0
System.out.print(task.get());
}catch (Exception ex){
ex.printStackTrace();
}
}
}
-
使用线程池例如用Executor框架
public class ThreadTest {
public static void main(String[] args){
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
Arrays.sort()底层原理
多线程解析一个超大文件怎么处理,如果文件切分的时候关键信息被分到了不同的解析线程中怎么办
-
synchronized原理
-
线程池参数
-
newFixedTheadPool底层,优缺点
-
线程安全的Map?分段锁是如何实现的?JDK 1.8之后有哪些优化?
-
Lock和Synchronized区别
-
AQS实现
-
锁优化
-
ConcurrentMap 源码
-
CMS
-
线程生命周期(状态)
-
ReentrantLock与AQS同步框架
-
CAS原理是什么?
-
synchronize原理是什么?
-
两个线程如何交替打印A和B
-
100万个数,数据类型是int,给你8核CPU,8G内存,如何求数组的和?
-
利用java现有的东西,让你设计一个对象,实现类似synchronize的功能,使得多个线程不冲突,你如何设计?(ThreadLocal玩起来)
-
synchronize锁定.class和锁定一个实例有什么区别?
-
什么时候发生线程的上下文切换?
-
CAS是硬件实现还是软件实现
-
除了wait和notifyall,还有什么办法实现类似的功能
-
微信抢红包设计(只讲了类似多线程抢、Semphore,缓存)、海量文件找重复次数最多的个数(分治)
wait()和sleep()区别
两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁。
两者都可以暂停线程的执行。
wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
wait方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify或者notifyAll方法。sleep方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
synchronized和volatile区别
synchronized的修饰范围:
个人的理解是:因为同步关键字Synchronized不能修饰变量(不能直接使用synchronized声明一个变量),不能使变量得到共享,故引入了轻量级的Volatie
volatile:
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor 3 个最重要的参数:
ThreadPoolExecutor 其他常见参数:
大小:
如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。 但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
线程池的参数定义,大小
先放一波变量定义
-
修饰一个代码块
-
修饰一个方法
-
修饰一个类
-
修饰一个静态的方法
-
volatile可以修饰变量,共享变量。
-
保障了共享变量对所有线程的可见性。即可保证在线程A将其修改时,线程B可以立刻得到。
-
禁止指令重排序
-
corePoolSize
:核心线程数定义了最小可以同时运行的线程数量。 -
maximumPoolSize
:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -
workQueue
:当新任务来的时候会先判断当前运行的线程数量是否达到了核心线程数,如果达到的话,信任就会被从存放到队列中中。 -
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁 -
unit
:keepAliveTime
参数的时间单位 -
threadFactory
:executor创建新线程的时候会用到。 -
handle
:饱和策略。关于饱和策略下面单独介绍 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor
定义一些策略: -
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。 -
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
Thread和Runnable开启线程有什么区别?
避免了Java单继承的局限性;
把线程代码和任务的代码分离,解耦合(解除线程代码和任务的代码模块之间的依赖关系)。代码的扩展性非常好;
线程池
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor
3 个最重要的参数:
ThreadPoolExecutor
其他常见参数:
ThreadPoolTaskExecutor
定义一些策略:
-
corePoolSize
: 核心线程数线程数定义了最小可以同时运行的线程数量。 -
maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -
workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 -
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁; -
unit
:keepAliveTime
参数的时间单位。 -
threadFactory
:executor
创建新线程的时候会用到。 -
handler
:饱和策略。关于饱和策略下面单独介绍一下. -
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException来拒绝新任务的处理
。 -
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
如何实现多线程
继承Thread类创建线程
public class ThreadTest extends Thread{
//重写run()方法,run()方法的方法体是线程执行体
public void run(){
for(int i=0;i<5;i++){
//使用线程的getName()方法可以直接获取当前线程的名称
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args){
//输出Java程序运行时默认运行的主线程名称
System.out.println(Thread.currentThread().getName());
//创建第一个线程并开始执行
new ThreadTest().start();
//创建第二个线程并开始执行
new ThreadTest().start();
}
}
public class ThreadTest implements Runnable{
private int i;
//重写run()方法,run()方法的方法体是线程执行体
public void run(){
//实现Runnable接口时,只能使用如下方法获取线程名
System.out.println(Thread.currentThread().getName() + " " + i);
i++;
}
public static void main(String[] args){
ThreadTest tt = new ThreadTest();
//创建第一个线程并开始执行
//输出 新线程1 0
new Thread(tt,"新线程1").start();
//创建第二个线程并开始执行
//输出 新线程2 1
new Thread(tt,"新线程2").start();
//使用Lambda表达式创建Runnable对象
new Thread(()->{
System.out.print("hello");
System.out.println("'world");
}).start();
}
}
public class ThreadTest {
public static void main(String[] args){
//使用FutureTask来包装Callable对象
//使用Lambda表达式来创建Callable<Integer>对象
FutureTask<Integer> task = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() + " " + "开始执行任务!");
return 0;
});
//实质还是以Callable对象来创建并启动线程
//输出 新线程 开始执行任务!
new Thread(task,"新线程").start();
try{
//获取线程的返回值
//输出 0
System.out.print(task.get());
}catch (Exception ex){
ex.printStackTrace();
}
}
}
public class ThreadTest {
public static void main(String[] args){
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
-
实现Runnable接口创建线程
-
使用Callable和Future创建线程
-
使用线程池例如用Executor框架
如何创建线程池
如上
线程池的参数
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
)
}
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize
:核心线程数定义了最小可以同时运行的线程数量。
maximumPoolSize
:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue
:当新任务来的时候会先判断当前运行的线程数量是否达到了核心线程数,如果达到的话,信任就会被从存放到队列中中。
ThreadPoolExecutor 其他常见参数:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor
定义一些策略:
java有哪些锁?
公平锁/非公平锁
公平锁指多个线程按照申请锁的顺序来获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象(很长时间都没获取到锁-非洲人...),ReentrantLock,了解一下。
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,典型的synchronized,了解一下
synchronized void setA() throws Exception {
Thread.sleep(1000);
setB(); // 因为获取了setA()的锁,此时调用setB()将会自动获取setB()的锁,如果不自动获取的话方法B将不会执行
}
synchronized void setB() throws Exception {
Thread.sleep(1000);
}
独享锁/共享锁
互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是其具体的实现
乐观锁/悲观锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁
总结
Java锁机制可归为Sychornized锁和Lock锁两类。Synchronized是基于JVM来保证数据同步的,而Lock则是硬件层面,依赖特殊的CPU指令来实现数据同步的。
CAS的有哪些实现?
我们在读Concurrent包下的类的源码时,发现无论是ReenterLock内部的AQS,还是各种Atomic开头的原子类,内部都应用到了
CAS
涉及一下底层
public class Test {
public AtomicInteger i;
public void add() {
i.getAndIncrement();
}
}
我们来看getAndIncrement
的内部:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
再深入到getAndAddInt
():
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
现在重点来了,compareAndSwapInt(var1, var2, var5, var5 + var4)
其实换成compareAndSwapInt(obj, offset, expect, update)
比较清楚,意思就是如果obj
内的value
和expect
相等,就证明没有其他线程改变过这个变量,那么就更新它为update
,如果这一步的CAS
没有成功,那就采用自旋的方式继续进行CAS
操作,取出乍一看这也是两个步骤了啊,其实在JNI
里是借助于一个CPU
指令完成的。所以还是原子操作。
CAS底层
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
p是取出的对象,addr是p中offset处的地址,最后调用了Atomic::cmpxchg(x, addr, e)
, 其中参数x是即将更新的值,参数e是原内存的值。代码中能看到cmpxchg有基于各个平台的实现。
ABA问题
描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。
目前在JDK的atomic包里提供了一个类AtomicStampedReference
来解决ABA问题。
public class ABADemo {
static AtomicInteger atomicInteger = new AtomicInteger(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("=====ABA的问题产生=====");
new Thread(() -> {
atomicInteger.compareAndSet(100, 101);
atomicInteger.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
// 保证线程1完成一次ABA问题
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicInteger.compareAndSet(100, 2020) + " " + atomicInteger.get());
}, "t2").start();
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("=====解决ABA的问题=====");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 第一次获取版本号
System.out.println(Thread.currentThread().getName() + " 第1次版本号" + stamp);
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
}, "t4").start();
}
}
-
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁 -
unit
:keepAliveTime
参数的时间单位 -
threadFactory
:executor创建新线程的时候会用到。 -
handle
:饱和策略。关于饱和策略下面单独介绍 -
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。 -
独享锁:是指该锁一次只能被一个线程所持有。
-
共享锁:是该锁可被多个线程所持有。
-
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待兵法同步的角度。
-
悲观锁认为对于同一个人数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出现问题。
-
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作时没有事情的。
-
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁带来大量的性能提升。
-
悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子类操作的更新。重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁
-
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来哦实现高效的并发操作。
-
以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)
-
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
-
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
-
这三种锁是锁的状态,并且是针对Synchronized。在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
-
偏向锁的适用场景:始终只有一个线程在执行代码块,在它没有执行完释放锁之前,没有其它线程去执行同步快,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;在有锁竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。
-
轻量级锁是指当锁是偏向锁的时候,被另一个线程锁访问,偏向锁就会升级为轻量级锁,其他线程会通过自选的形式尝试获取锁,不会阻塞,提高性能。
-
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
-
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
-
Synchronized是一个非公平、悲观、独享、互斥、可重入的重量级锁。
-
ReentrantLock是一个默认非公平但可实现公平的、悲观、独享、互斥、可重入、重量级锁。
-
ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。
-
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
-
自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。
-
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适用使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。
-
谈一下并发跟并行的区别
并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
并行:单位时间内,多个任务同时执行。
-
有哪些同步方法,讲一下
-
线程的创建方式
-
线程池的原理
并行并发的区别:1
-
voatile:6
-
synchronized:10
-
ReentranLock:10
-
CAS:4
-
线程池:5
-
ThreadLocal的原理:5
-
创建线程的方式:2
-
公平锁和非公平锁的区别:1
-
死锁:4
-
sleep与wait方法什么区别:2
-
对象锁和类锁的区别?:1
-
线程的几种状态?:1
-
-
多线程之间的通信。:1
-
Thread里面的run和start:1
进程线程的区别
进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程
线程是一个比进程更小的执行单位
一个进程在其执行的过程中可以产生多个线程
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程
线程切换为什么比进程切换开销小
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 实际上就是任务从保存到再加载的过程就是一次上下文切换。 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
jvm内存模型,volatile关键字
公平锁和非公平锁如何实现
ReentrantLock的公平锁和非公平锁 学习AQS的时候,了解到AQS依赖于内部的FIFO同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个Node对象并将其加入到同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
ReentrantLock 默认采用非公平锁,除非在构造方法中传入参数 true 。
//默认 public ReentrantLock() { sync = new NonfairSync(); } //传入true or false public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
公平锁的lock方法
static final class FairSync extends Sync { final void lock() { acquire(1); } // AbstractQueuedSynchronizer.acquire(int arg) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
我们可以看到,在注释1的位置,有个!hasQueuedPredecessors()条件,意思是说当前同步队列没有前驱节点(也就是没有线程在等待)时才会去compareAndSetState(0, acquires)使用CAS修改同步状态变量。所以就实现了公平锁,根据线程发出请求的顺序获取锁。
非公平锁的lock方法
static final class NonfairSync extends Sync { final void lock() { // 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // AbstractQueuedSynchronizer.acquire(int arg) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } /** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //3.这里也是直接CAS,没有判断前面是否还有节点。 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
非公平锁的实现在刚进入lock方法时会直接使用一次CAS去尝试获取锁,不成功才会到acquire方法中,如注释2。而在nonfairTryAcquire方法中并没有判断是否有前驱节点在等待,直接CAS尝试获取锁,如注释3。由此实现了非公平锁。
总结
非公平锁和公平锁的两处不同: 1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
操作系统里进程状态及转换
yield sleep结合状态转换讲一下
-
sleep:不会释放锁 让当前正在执行的线程先暂停一定的时间,并进入阻塞状态。 在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。 即使此时系统中没有任何其他可执行的线程,处于sleep()中的线程也不会执行。 因此sleep()方法常用来暂停线程的执行。当sleep()结束后,然后转入到 Runnable(就绪状态),这样才能够得到执行的机会。
-
yield:线程让步,不会释放锁 让一个线程执行了yield()方法后,就会进入Runnable(就绪状态),不同于sleep()和join()方法,因为这两个方法是使线程进入阻塞状态】。 除此之外,yield()方法还与线程优先级有关,当某个线程调用yield()方法时,就会从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或者更高优先级的线程去执行。
线程池的三个主要参数的意思
-
corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量。
-
maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
-
workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
Synchronize的底层实现和优化
可重入锁是如何实现的
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,典型的synchronized,了解一下
AQS和synchronize的区别
Threadlocal
解释线程池的三个主要参数
-
corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量。
-
maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
-
workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
线程池都有哪几种工作队列
-
ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
-
LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
-
SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool(5)使用了这个队列。
-
PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
Synchronize和ReentrantLock的区别
-
两者都是可重入锁:两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
-
Synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API」:synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
-
ReenTrantLock 比 Synchronized 增加了一些高级功能
-
等待可中断:过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
-
可实现公平锁
-
可实现选择性通知(锁可以绑定多个条件):线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”
-
性能已不是选择标准:在jdk1.6之前synchronized 关键字吞吐量随线程数的增加,下降得非常严重。1.6之后,synchronized 和 ReenTrantLock 的性能基本是持平了。
-
公平锁和非公平锁是如何实现的
ReentrantLock的公平锁和非公平锁 学习AQS的时候,了解到AQS依赖于内部的FIFO同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个Node对象并将其加入到同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
-
ReentrantLock 默认采用非公平锁,除非在构造方法中传入参数 true 。
//默认 public ReentrantLock() { sync = new NonfairSync(); } //传入true or false public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
公平锁的lock方法
static final class FairSync extends Sync { final void lock() { acquire(1); } // AbstractQueuedSynchronizer.acquire(int arg) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
我们可以看到,在注释1的位置,有个!hasQueuedPredecessors()条件,意思是说当前同步队列没有前驱节点(也就是没有线程在等待)时才会去compareAndSetState(0, acquires)使用CAS修改同步状态变量。所以就实现了公平锁,根据线程发出请求的顺序获取锁。
非公平锁的lock方法
static final class NonfairSync extends Sync { final void lock() { // 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // AbstractQueuedSynchronizer.acquire(int arg) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } /** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //3.这里也是直接CAS,没有判断前面是否还有节点。 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
非公平锁的实现在刚进入lock方法时会直接使用一次CAS去尝试获取锁,不成功才会到acquire方法中,如注释2。而在nonfairTryAcquire方法中并没有判断是否有前驱节点在等待,直接CAS尝试获取锁,如注释3。由此实现了非公平锁。
总结
非公平锁和公平锁的两处不同: 1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
AQS都有什么公共方法
-
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
-
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
-
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
-
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
-
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
线程和进程
进程
-
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。
-
系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,
-
它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。
-
换句话说,当程序在执行时,将会被操作系统载入内存中。
-
线程是进程划分成的更小的运行单位。
-
线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
-
从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程
-
线程与进程相似,但线程是一个比进程更小的执行单位。
-
一个进程在其执行的过程中可以产生多个线程。
-
与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
独占锁,共享锁,乐观锁讲一下
-
独享锁:是指该锁一次只能被一个线程所持有。
-
共享锁:是该锁可被多个线程所持有。
-
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作时没有事情的。常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子类操作的更新。
NIO是什么?
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。 NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。 阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性; 对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
线程和进程概念
进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程
-
线程是一个比进程更小的执行单位
-
一个进程在其执行的过程中可以产生多个线程
-
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程
synchronized和Lock的区别
-
两者都是可重入锁:两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
-
Synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API:synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
-
ReenTrantLock 比 Synchronized 增加了一些高级功能
-
等待可中断:过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
-
可实现公平锁
-
可实现选择性通知(锁可以绑定多个条件):线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”
-
性能已不是选择标准:在jdk1.6之前synchronized 关键字吞吐量随线程数的增加,下降得非常严重。1.6之后,synchronized 和 ReenTrantLock 的性能基本是持平了。
-
volatile的作用
-
可序性(禁止重排)
-
可见性
-
不能保证原子性
synchronized底层实现
-
synchronized 同步语句块的情况
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。 相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
-
synchronized 修饰方法的的情况
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
1.6之后的优化
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 锁主要存在四中状态,依次是:「无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态」,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
-
偏向锁
-
引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。
-
偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!
-
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 升级过程:
-
访问Mark Word中偏向锁的标识是否设置成1,锁标识位是否为01,确认偏向状态
-
如果为可偏向状态,则判断当前线程ID是否为偏向线程
-
如果偏向线程未当前线程,则通过cas操作竞争锁,如果竞争成功则操作Mark Word中线程ID设置为当前线程ID
-
如果cas偏向锁获取失败,则挂起当前偏向锁线程,偏向锁升级为轻量级锁。
-
轻量级锁
-
轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。
-
如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁! 升级过程:
-
线程由偏向锁升级为轻量级锁时,会先把锁的对象头MarkWord复制一份到线程的栈帧中,建立一个名为锁记录空间(Lock Record),用于存储当前Mark Word的拷贝。
-
虚拟机使用cas操作尝试将对象的Mark Word指向Lock Record的指针,并将Lock record里的owner指针指对象的Mark Word。
-
如果cas操作成功,则该线程拥有了对象的轻量级锁。第二个线程cas自旋锁等待锁线程释放锁。
-
如果多个线程竞争锁,轻量级锁要膨胀为重量级锁,Mark Word中存储的就是指向重量级锁(互斥量)的指针。其他等待线程进入阻塞状态。
AQS底层实现(非公平锁,公平锁)
ReentrantLock的公平锁和非公平锁 学习AQS的时候,了解到AQS依赖于内部的FIFO同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个Node对象并将其加入到同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
-
ReentrantLock 默认采用非公平锁,除非在构造方法中传入参数 true 。
//默认 public ReentrantLock() { sync = new NonfairSync(); } //传入true or false public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
公平锁lock方法
static final class FairSync extends Sync { final void lock() { acquire(1); } // AbstractQueuedSynchronizer.acquire(int arg) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
我们可以看到,在注释1的位置,有个!hasQueuedPredecessors()条件,意思是说当前同步队列没有前驱节点(也就是没有线程在等待)时才会去compareAndSetState(0, acquires)使用CAS修改同步状态变量。所以就实现了公平锁,根据线程发出请求的顺序获取锁。
非公平锁的lock方法
static final class NonfairSync extends Sync { final void lock() { // 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // AbstractQueuedSynchronizer.acquire(int arg) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } /** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //3.这里也是直接CAS,没有判断前面是否还有节点。 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
非公平锁的实现在刚进入lock方法时会直接使用一次CAS去尝试获取锁,不成功才会到acquire方法中,如注释2。而在nonfairTryAcquire方法中并没有判断是否有前驱节点在等待,直接CAS尝试获取锁,如注释3。由此实现了非公平锁。
总结
非公平锁和公平锁的两处不同: 1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
单例为什么加锁,volatile什么作用
-
如果单例不加锁,在高并发情况下可能出现多个实例的情况
-
volatile的有序性特性则是指禁止JVM指令重排优化。因为实例初始化的时候,并非原子性,如
memory =allocate(); //1. 分配对象的内存空间 ctorInstance(memory); //2. 初始化对象 instance =memory; //3. 设置instance指向刚分配的内存地址
上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下:
memory =allocate(); //1. 分配对象的内存空间 instance =memory; //3. 设置instance指向刚分配的内存地址 ctorInstance(memory); //2. 初始化对象
这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。
所以我们在用这种方式实现单例模式时,会使用volatile关键字修饰instance变量,这是因为volatile关键字除了可以保证变量可见性之外,还具有防止指令重排序的作用。当用volatile修饰instance之后,JVM执行时就不会对上面提到的初始化指令进行重排序优化,这样也就不会出现多线程安全问题了。