线程相关的面试题
线程
线程和进程有什么区别和联系?
- 联系:
- 进程是线程的【上级】和【容器】,一个进程中可以有一个或多个线程(至少一个)。
- 线程概念是进程概念的轻量化,很多线程概念和进程大概念都是一脉相承。
- 区别:
- 进程概念要比线程概念出现更早;
- 进程与进程间是隔离的,不能共享内存空间和上下文。但是,(因为同属于一个进程)一个进程下的线程缺可以;
- 线程占用资源比进程更少,切换线程比切换进程代价更小;
- 进程是程序的一次执行,线程是程序中的部分代码,部分逻辑的执行。
如何保证一个线程执行完再第二个线程?
使用join()
方法,等待上一个线程的执行完之后,再执行当前线程。该方法的作用是等待调用方法的线程对象执行完毕后, 都会执行别的线程内容
public static void main(String[] args) {
System.out.println("主线程");
Thread joinThread = new Thread(() -> {
try {
System.out.println("执行前");
Thread.sleep(1000);
System.out.println("执行后");
} catch (Exception e) {
e.printStackTrace();
}
});
joinThread.start();
/*
try {
joinThread.join();
} catch (InterruptedException e) {
// todo Auto-generated catch block
e.printStackTrace();
}*/
System.out.println("主程序");
}
线程有哪些常用的方法?
currentThread() :返回当前正在执行的线程引用
getName() :返回此线程的名称
setPriority() :设置线程的优先级
getPriority() :返回线程的优先级
isAlive() :检测此线程是否处于活动状态,活动状态指的是程序处于正在运行或准备运行的状态
sleep() :使线程休眠
join() :等待线程执行完成
yield() :让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可以运行的状态.
wait()和sleep()有什么区别?
- 从来源看: sleep()来自Thread, wait()来自Object.
- 从线程状态看: sleep()导致当前线程进入TIMED_WAITING状态,wait()导致当前线程进入WAITING状态.
- 从恢复执行看: sleep()在指定时间之后,线程会恢复执行,wait()则需要等待别的线程使用notify()/notifyAll()来唤醒它.(这和上述
2
有关). - 从锁看: 如果当前线程持有锁,sleep()不会释放锁,wait()会释放锁.
线程状态如下:
守护线程是什么?
守护线程是一种比较低级别的线程,一般用于为其他类型线程提供服务,因此当其他线程都退出时,它也就没有存在的必要了.例如,JVM(Java虚拟机)中的垃圾回收线程.
线程有哪些状态?
在JDK 8中,线程的状态有以下6种.
NEW:尚未启动
RUNNABLE:正在执行中
BLOCKED: 阻塞(被同步锁或者IO锁阻塞)
WAITING: 永久等待状态
TIMED_WAITING: 等待指定的时间重新被唤醒的状态
TERMINATED: 执行完成
这里需要说明的是:经典操作系统线程核心3态是Runnable, Running和Blocked, 而Java线程的状态中与之对应的是:Runnable, Blocked, Waiting, Timed_Waiting.
- Java线程中没有区分Runnable和Running, 把它们都归属于Runnable.
- Java把Blocked状态细分为:Blocked,Waiting和Timed_Waiting三种。
线程中的start()和run()有哪些区别?
- start()方法可以用于启动线程;run()方法用于确定执行线程的运行时代码。
- run()可以重复调用,而start()只能调用一次,换句话说,一个线程可以更换执行的内容,但是启动的过程只有一次。
产生死锁需要具备哪些条件?
产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系;
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
简而言之一句话:每个线程拥有部分资源,不愿意放弃已有资源,并且在等待别人释放资源。
如何预防死锁?
预防死锁的方法如下
- 尽量使用tryLock(long timeout,TimeUnit unit)的方法(ReentrantLock,ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁;
- 尽量使用Java.util.concurrent并发类代替自己手写锁;(比如ConcurrentArrayList)
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁;
- 优化代码逻辑,如果无法获取全部资源,就释放以获得资源,并重新开始获取资源。
如何让两个程序依次输出11/22/33等数字,请写出实现代码?
使用思路是在每个线程输出信息之后,让当前线程等待一会再执行下一次操作,具体实现代码如下:
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("线程一:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("线程二:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
说一下线程的调度策略?
线程调度器选择优先级最高的线程运行,但是如果发生以下情况,就会终止线程的运行;
- 线程体中调用了yield()方法,让出了对CPU的占用权;
- 线程体中调用了sleep()方法,使线程进入睡眠状态;
- 线程由于I/O操作而受阻塞;
- 另一个更高优先极的线程出现;
- 在支持时间片的系统中,该线程的时间片用完.
yield方法的作用: 暂停当前正在执行的线程对象,并执行其他线程.
yield()实际上将调用的线程从正在运行的状态变成可运行状态.Running->Runnable,但是无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中.
线程池: ThreadPoolExecutor
THreadPoolExecutor有哪些常用的方法?
常用方法如下所示:
getCorePoolSize():获取核心线程数
getMaximumPoolSize():获取最大线程数
getActiveCount():正在运行的线程数
getQueue():获取线程池中的任务队列
isShutdown():判断线程是否终止
submit():执行线程池
execute():执行线程池
shutdown():终止线程池
shutdownNow():终止线程池
改下程序执行的结果是什么?
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS,
new LinkedBlockingQueue());
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 2; i++) {
System.out.println("I:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadPoolExecutor.shutdownNow();
System.out.println("java");
程序执行的结果是:
I: 0
Java
java.lang.InterruptedException: sleep interrupted
I: 1
因为程序中使用了
shutdownNow()
会导致程序执行一次之后报错,抛出sleep interrupted
异常,又因为本身有try/catch,所以程序会继续执行打印I: 1
.
shutdown只是将线程池的状态设置为SHUTDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断.而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回.
shutdownNow执行时,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再执行还在池队列中等待的任务,当然,会返回那些未执行的任务.
它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep,wait,Condition
,定时锁等应用,interrupt()方法是无法中断当前的线程的.所以,shutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出.
在ThreadPool中submit()和execute()有什么区别?
submit()和execute()都是用来执行线程池的,不同的是:
- 使用execute()执行线程池不能有返回方法;
- 使用submit()可以使用Future接收线程池执行的返回值.
说一下ThreadPoolExecutor都需要哪些参数?
ThreadPoolExecutor最多包含以下七个参数:
corePoolSize:线程池中的核心线程线程数
maxinumPoolSize: 线程池中最大线程数
keepAliveTime: 闲置超时时间
unit: keepAliveTime 超时时间的单位(时/分/秒等)
workQueue: 线程池中的任务队列
threadFactory: 为线程池提供创建新线程的线程工厂
rejectedExecutionHandler: 线程池任务队列超过最大值之后的拒绝策略
线程池中核心线程和最大线程的区别:\
线程池策略
corePoolSize:核心线程数; maximumPoolSize: 最大线程数
每当有新的任务到线程池时,
第一步:先判断线程中当前数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,若当前线程数量已达到corePoolSize,则进入下一步;
第二步: 判断工作队列(workQueue)是否已满,未满则将新的任务提交到工作队列中,满了则进入下一步;
第三步: 判断线程池中的线程数量是否达到了maximunPoolSize,如果未达到,则新建一个工作线程来执行这个任务,如果达到了则使用饱和策略来处理这个任务.注意: 在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止.直到线程池中线程的数量不大于corePoolSize为止.(由第三步可知,在一般情况下,Java线程池中会长期保持corePoolSize个线程.)
饱和策略
当工作队列满且线程个数达到maximumPoolSize后所采取的策略:
AbortPolicy: 默认策略;新任务提交时直接抛出未检查的异常RejectedExecutionException, 该异常可由调用者捕获,新任务会被舍弃
CallerRunsPolicy: 既不抛弃任务也不抛出异常,使用调用者所在线程运行新的任务.
DiscardPolicy: 丢弃新的任务,且不抛出异常
DiscardOldestPolicy: 调用poll方法丢弃工作队列队头的任务,然后尝试提交新任务
自定义策略: 根据用户需要定制.
在线程池中shutdownNow()和shutdown()有什么区别?
shutdownNow()和shutdown()都是用来终止线程池的,它们的区别是:
- 使用shutdown()程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了shutdown()之后就不能给线程池添加新任务了;
- shutdownNow()会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出java.lang.InterruptedException:sleep interrupted异常
以下线程名称被打印了几次?
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 10L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2),
new ThreadPoolExecutor.DiscardPolicy());
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
线程名称被打印了3次
线程池第1次执行任务时,会新创建任务并执行;第2次执行任务时,因为没有空闲线程所以会把任务放入队列;第3次同样把任务放入队列,因为队列最多可以放两条数据,所以第4次之后的执行都会被舍弃(没有定义拒绝策略),于是就打印了3次线程名称).
线程池: Executors
程序输出结果?
public static void main(String[] args) {
System.out.println("程序开始");
ExecutorService workStealingPool = Executors.newWorkStealingPool();
for (int i = 0; i < 5; i++) {
int finalNumber = i;
workStealingPool.execute(()->{
System.out.println(finalNumber);
});
}
}
newWorkSrealingPool内部实现是ForkJoinPool,它会随着主程序的退出而退出,因为主程序没有任何休眠和等待操作,程序会一闪而过,不会执行任何信息,所以也就不会输出任何结果.
ForkJoinPool
Executor能创建单线程的线程池吗?怎么创建?
Executors可以创建单线程线程池,创建分为两种方式:
newSingleThreadExecutor():创建一个单线程线程池.
newSingleThreadScheduledExecutor():创建一个可以执行周期性任务的单线程池.
Executors中哪个线程适合执行短时间内大量任务?
newCachedThreadPool()
适合处理大量短时间工作任务。它会试图缓存线程并重用,如果没有缓存任务就会新创建任务,如果线程的限制时间超过60秒,则会被移除线程池,因此它比较适合短时间内处理大量任务。
可以执行周期性任务的线程池都有哪些?
可执行周期性任务的线程池有两个,分别是:newScheduledThreadPool()
和newSingleThreadScheduledExecutor()
,其中newSingleThreadScheduledExecutor()
是newScheduledThreadPool()`的单线程版本。
总结起来就是方法中带有Scheduled的线程池。
JDK 8新增了什么线程池?有什么特点?
JDK 8新增的线程池是.newWorkStealingPool(n)
,如果不指定并发数(也就是不指定n),newWorkStealingPool()
会根据当前CPU处理器数量生成相应个数的线程池。它的特点是并行处理任务的,不能保证任务的执行顺序。
newFixedThreadPool和ThreadPoolExecutor有什么关系?
newFixedThreadPool是ThreadPoolExecutor包装,newFixedThreadPool底层也是通过ThreadPoolExecutor实现的。
单线程的线程池存在的意义是什么?
单线程池线程提供了队列功能,如果有多个任务会排队执行,可以保证任务执行的顺序性。单线程线程池可以重复利用已有线程,减低系统创建和销毁线程的性能开销。
线程池为什么建议使用ThreadPoolExecutor创建,而非Executors?
使用ThreadPoolExecutor能让开发者更加明确线程池的运行规则,避免资源耗尽的风险。
Executors返回线程池的缺点如下:\
- FixedThreadPool和SingleThreadPool允许请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,可能会堆积大量请求,可能会导致内存溢出。
- CachedThreadPool和ScheduledThreadPool允许创建线程数量为Integer.MAX_VALUE,创建大量线程,可能会导致内存溢出。
- 总结一下,有两种情况:一,队列的长度太高,造成请求堆积,内存溢出。二,线程池最大数量太多,导致创建太多线程,导致内存溢出。
4.ThreadLocal
ThreadLocal为什么是线程安全的?
ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,因此ThreadLocal是线程安全的,每个线程都有属于自己的变量。
总结一下:\
- 每个线程都有一个ThreadLocalMap成员变量(线程副本);
- ThreadLocal作为ThreadLocalMap的key来获取最终变量的值;
每个线程拿到的ThreadLocalMap变量肯定是线程私有的,所以不会被其他线程拿到,这样也就保证了线程的安全。
以下程序打印的结果是true还是false?
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("老王");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("老王");
new Thread(()->{
System.out.println(threadLcoal.get().equals(threadLcoal2.get()));
}).start();
false
因为threadLocal使用的是InheritableThreadLocal(共享本地线程),所以threadLocal.get()结果为老王,而threadLocal2使用的是ThreadLocal,因此在新线程中threadLocal2.get()的结果为null
ThreadLocal为什么会发生内存溢出?
ThreadLcoal造成内存溢出的原因:如果ThreadLocal没有被直接引用,在GC回收时,由于ThreadLocalMap中的key是弱引用,所以一定会被回收,这样一来ThreadLocalMap中就会出现key为null的Entry的value一直存在一条强引用链:Thread Ref->Thread->ThreadLocalMap->Entry->value并且永远无法回收,从而造成内存泄漏。
解决ThreadLocal内存溢出的关键代码是什么?
关键代码为threadLocal.remove(),使用完ThreadLocal之后,调用remove()方法,清除掉ThreadLocalMap中的无用数据就可以避免内存溢出了。
ThreadLocal和Synchronized有什么区别?
ThreadLocal和Synchronized都用于解决多线程并发访问,防止任务在共享资源上产生冲突,但是ThreadLocal与Synchronized有本质的区别:
- Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种以时间换空间的方式;
- ThreadLocal为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种以空间换时间的方式。
5.synchronized和ReentrantLock
ReentrantLock常用的方法有哪些?
lock():用于获取锁
unlock():用于释放锁
tryLock():尝试获取锁
getHoldCount():查询当前线程执行lock()方法的次数
getQueueLength():返回正在排队等待此锁的线程数
isFair():该锁是否为公平锁
ReentrantLock有哪些优势?
1.ReentrantLock具备非阻塞方式获取锁的特性,使用tryLock()方法。
2.ReentrantLock可以中断获取的锁,使用lockInterruptibly()
方法,当获取锁之后,如果所在的线程被中断,则会抛出异常并释放当前获得的锁。
3.ReentrantLock可以在指定时间范围内获取锁,使用tryLock(long timeout,TimeUnit unit)
方法。
ReentrantLock怎么创建公平锁?
new ReentrantLock()
默认创建的为非公平锁,如果要创建公平锁可以使用new ReentrantLock(true)
。
公平锁的非公平锁有哪些区别?
公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先lock()的线程不一定先获得锁。
ReentrantLock中lock()和lockInterruptibly()有什么区别?
lock()和lockInterruptibly()的区别在于获取线程的途中如果所在的线程中断,lock()会忽略异常继续等待获取线程,而lockInterruptibly()则会抛出InterruptedException异常。
非公平锁的具体机制:
新来的线程直接和队首线程争抢资源,如果争抢到了,则直接获取锁资源,队首线程继续等待。如果新来的线程竞争失败,则去队尾进行排队,只能等待队列前所有线程执行完毕后自己才能获取锁。
注意:锁的公平性只在首次和队首线程进行锁竞争时有体现,竞争失败入列后则与公平锁执行方式一致
Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
// interruptLock.lock();
interruptLock.lockInterruptibly();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);
synchronized和ReentrantLock有什么区别?
- ReentrantLock使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁;
- ReentrantLock只适用于代码块锁,而synchronized可用于修饰方法,代码块等;
- ReentrantLock性能略高于synchronized。
ReentrantLock的tryLock(3,TimeUnit.SECONDS)表示等待3秒后再去获取锁,这种说法对吗?为什么?
不对。tryLock(3,TimeUnit.SECONDS)表示获取锁的最大等待时间为3秒,期间会一直尝试获取,而不是等待3秒之后再去获取锁。
synchronized是如何实现锁升级的?
在锁对象的对象头里面有一个threadid字段,在第一次访问的时候threadid为空,JVM让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。
锁的升级之路
Java1.6中引入了偏向锁和轻量级锁,在JavaSE1.6中,锁一共有4种状态,级别从高到低依次是:无锁状态,偏向锁状态,轻量级锁状态和重量级状态。锁可以升级但不能降级,目的是提高获得锁和释放锁的效率。
synchronized使用的锁是存在Java对象的请求头中的。
6.CAS和各种锁
synchronized是哪种锁的实现?为什么?
synchronized是悲观锁的实现,因为synchronized修饰的代码,每次执行时会进行加锁操作,同时只允许一个线程进行操作,所以它是悲观锁的实现。
new ReentrantLock()创建是锁是公平锁吗?
new ReentrantLock()
等同于new ReentrantLock(false)
,非公平锁。new ReentrantLock(false)
是公平锁。
synchronized使用的是公平锁吗?
synchronized使用的是非公平锁,并且是不可设置的。
这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统调度的基本选择,所以这也是synchronized使用非公平锁原由。
为什么非公平锁吞吐量大于公平锁?
比如A占用锁的时候,B请求获取锁,发现被A占用之后,堵塞等待被唤醒,这个时候C同时来获取A占用的锁,如果是公平锁C后来者发现不可获得锁之后,一定排在B之后等待被唤醒,而非公平锁则可以让C先用,在B被唤醒之前C已经使用完成,从而节省了C等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。
总结之,非公平锁对比公平锁,可以省去进行队列和出队列的消耗。
volatile的作用是什么?
volatile是Java虚拟机提供的最轻量级的同步机制。同synchronized相比(synchronized通常称为重量级锁)
当变量被定义为volatile之后,具备两种特性:
- 保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,修改的新值对于其他线程是可见的(可以立即得知的);
- 禁止指令重排序优化,普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。
volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,解决方案是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。即JDK1.5的J.U.C
volatile对比synchronized有什么区别?
synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。比如,i++如果使用synchronized修饰是线程安全的,而使用volatile会有线程安全的问题。
CAS是如何实现的?
CAS(Compare and Swap)比较并交换,CAS是通过调用JNI(Java Native Interface)的代码实现的,比如,在Windows系统CAS就是借助C语言来调用CPU底层指令实现的。native就是指C语言的调用。
CAS会产生什么问题?应该怎么解决?
CAS是标准的乐观锁的实现,会产生ABA的问题。
ABA通常的解决办法是添加版本号,每次修改操作时版本号加一,这样数据对比的时候就不会出现ABA的问题了。
ABA问题:比较简单的问题,就是对比的数据有可能回退,解决办法是增加一个与业务无关的伴随的记录变量。比如,更改次数,version字段。
判断正确与否
A: 独占锁是指任何时候都只有一个线程能执行资源操作
B: 共享锁指定是可以同时被多个线程读取和修改
C: 公平锁是指多个线程按照申请锁的顺序来获取锁
D: 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
B,共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。
线程的特性:
原子性:一个操作或者一系列操作要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。\
- 共享变量:从JVM内存模型的角度上讲,存储在堆内存上数据都是线程共享的,如实例化的对象,全局变量(元素属性),数组等。存储在线程池上的数据是线程独享,如局部变量,操作栈,动态链接,方法出口等信息。
- 原子性操作:首先一段代码肯定不是原子性的,那么一行代码呢?实际上也不是,volatile那个例子中,可见一斑。
在JMM中定义了8种原子操作,包括read,load,assign,use,store,write,便于理解,只有JMM定义的一些最基本的操作是符合原子性的,如果需要对代码块实行原子性操作,则需要JMM提供的lock,unlock,synchronized等来保证。
3. 保证操作原子性:内置锁(同步关键字)synchronized; 显示锁Lock; 自旋锁: CAS;
可见性:
JMM内存模型,JMM规定多线程之间的共享变量存储在主存中,每个线程单独拥有一个本地内存(逻辑),本地内存存储线程操作的共享变量副本;
- JMM中的变量指的是共享变量(实例变量,static 字段和数组元素),不包括线程私有变量(局部变量和方法参数);
- JMM规定线程对变量的写操作都在自己的本地副本中进行,不能直接写主存中的对应变量;
- 多线程间变量传递通过主存完成(Java线程通信通过共享内存),线程修改变量后通过本地内存写回主存,从主存读取变量,彼此不允许直接通信(本地内存是私有的);
综上,JMM通过控制主存和每个线程的本地内存的数据交互,保证一致的内存可见性;线程之间的变量的共享都要通过刷新主内存,其他线程来完成。这个过程会依赖于java的happens-before原则。
确保可见性:1.volatile轻量级的来保证线程的可见性。2.JVM会尽力保证内存的可见性,即使这个变量没有加同步关键字。例如如果在代码中使用Thread.yield(),CPU有时间,能够让JVM去进行通信过程。但是这种方式是有隐患的,更建议使用volatile来确保线程的可见性。
有序性:有序性的本义是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的,但是在Java内存模型中,是允许编译器和处理器对指令进行重排序的,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。也就是说在多线程中代码的执行顺序,不一定会与你直观上看到的代码编写的逻辑顺序一致。
确保有序性:volatile关键字两个作用,一是确保线程的可见生,二是禁止使用重排序。
happen-before原则:
含义:顾名思义,当一个变量被多个线程读取并且至少被一个线程写入时,如果读操作和写操作没有HB关系,则会产生数据竞争问题。想确保A看到B的结果,那么根据HB关系,B必须在A之前。
HB关系具体来说包含一系列规则:包括程序次序规则,锁定规则(指解锁在上锁之后),volatile变量规则(对此变量的写操作先行发生于读操作)
标签:synchronized,池和锁,ReentrantLock,任务,线程,new,执行 From: https://www.cnblogs.com/jtc-terrence/p/16795881.html总结一下:确保原子性需要加锁,而可见性和有序性可以通过volatile来确保。