- 问:如何实现子线程先执行,主线程再执行
答: 启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行.
扩展阅读
Thread类提供了让一个线程等待另一个线程完成的方法--join()方法. 当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完成为止.
- 问: 说一说synchronized与Lock的区别
答:
- synchronized是Java的关键字,在JVM层面实现加锁和加锁;Lock的一个接口,在代码层面实现加锁和解锁.
- synchronized可以用在代码块上,方法上;Lock只能写在代码里.
- synchronized在代码执行完或出现异常时会自动释放锁;Lock不会自动释放锁,需要在finally中显式释放锁
- synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间
- synchronized无法得知是否获取锁成功;Lock则可以通过tryLock()得知是否加锁成功
- synchronized锁可重入,不可中断,非公平;Lock锁可重入,可中断,可公平\不公平,并可以细分读写锁以提高效率.
- 问: 说一说synchronized的底层实现原理
答:
- synchronized作用在代码块时,它的底层是通过monitorenter,monitorexit指令来实现的.
monitorenter:
每个对象都是一个监视器锁(monitorenter),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. 如果其他线程已经占有了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权.
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor持有者.指令执行时,monitor的进入数减1,如果减1后进入数为0,那该线程退出monitor,不再是这个monitor的所有者.其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权.
monitorexit指令出现了两次,第一次为同步正常退出释放锁,第二次为发生异步退出释放锁. - 方法的同步并没有通过monitorenter和monitorexit指令来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标志符.JVM就是根据该标示符来实现方法的同步:
当方法调用时,调用指令会检查方法的ACC_SYCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取方法成功之后才能执行方法体,方法执行完成后再释放monitor.在方法执行期间,其他线程都无法再获得同一个monitor对象. - 总结
两种同步方法本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成. 两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起,等待重新调度,会导致"用户态和内核态"两个态之间的来回切换,对性能有较大影响.
问 synchronized可以修饰静态方法和静态代码块吗?
答: synchronized可以修饰静态方法,但不能修饰静态代码块.
当修饰静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久带. 因此静态方法锁相当于该类的一个全局锁.-
问 谈谈ReentrantLock的实现原理?
答: ReentrantLock是基于AQS实现的,AQS即AbstractQueueSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列.其中同步队列是一个双向链表,里面存储的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单项链表,里面存储的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作.
在同步队列中,还存在2种模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁.
AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state,而ReentrantLock就是通过重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock.
首先ReentrantLock实现了Lock接口,然后有3个内部类,其中Sync内部类继承自AQS,另外的两个内部类继承自Sync,这两个类分别是用来公平锁和非公平锁的. 通过Sync重写的方法tryAcquire,tryRelease可以知道,ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁.
问 如果不使用synchronized和Lock,如何保证线程安全?
答:
- volatile
关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值. 需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量 - 原子变量
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步. 例如 AtomicInteger表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer. 可拓展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问. - 本地存储
可以通过ThreadLocal类来实现线程本地存储的功能,每个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以TheadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量. - 不可变的
只有一个不可变的对象被正确地构建出来,那其他外部的课件状态永远不会改变,永远不会看到它在多个线程之中处于不一致的状态. 可以参考String设计一个不可变的类.
问 说一下Java中的乐观锁和悲观锁的区别
答:悲观锁:总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.Java中的悲观锁是通过synchronized关键字或Lock接口来实现的
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁. 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据. 乐观锁适用于多读的应用型,这样可以提高吞吐量. 在JDK1.5中新增java.util.concurrent(J.U.C)就是建立在CAS之上的. 相对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现,所以J.U.C在性能上有了很大的提升.-
问: 公平锁和非公平锁是怎么实现的?
答: 在Java中实现锁的方式有两种,一种是使用Java自带的关键字synchronized对相应的类或方法以及代码块进行加锁. 另一种是ReentrantLock,前者只能是非公平锁,而后者是默认非公平,但可实现公平的一把锁.
ReentrantLock是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的,并且它的实现依赖于Java同步框架AbstractQueueSynchronizer(AQS),AQS使用一个整形的volatile变量state来维护同步状态,这个volatile变量是实现ReentrantLock的关键
ReentrantLock的公平锁和非公平锁都委托 了AbstractQueuedSynchronizer#acquire去请求获取
public final void acquire(int arg){
if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
- tryAcquire是一个抽象方法,是公平与非公平的实现原理所在
- addWaiter是将当前线程结点加入等待队列之中. 公平锁在锁释放后会严格按照等到队列去取后续值,而非公平锁在对于新晋线程有很大优势.
- acquireQueued在多次循环中尝试获取到锁或者将当前线程阻塞
- selfInterrupt如果线程在阻塞期间发生了中断,调用Thread.currentThread().interrupt()中断当前线程
公平锁与非公平锁在锁的获取上都使用到了volatile关键字修饰的state字段,这是保证多线程环境下锁的获取与否的核心. 但是当并发情况下多个线程都读取到state =0时,则必须使用到CAS技术,一门CPU的原子锁技术,可通过CPU对共享变量加锁的形式,实现数据变更的原子操作. volatile和CAS的结合是并发抢占的关键.
公平锁 FairSync
公平锁的实现机理在每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,如果有,当前线程会执行如下步骤:
if(!hasQueuedPredecessors() && compareAndSetState(0,acquires)) {
setExclusiveOwnerThread(current);
return true;
其中hasQueuedPredecessors是用于检查是否有等待队列的
public final boolean hasQueuedPredecessors(){
Node t = tail;
//Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平锁 NonfairSync
非公平锁在实现的时候多次强调随机抢占
if(c==0){
if(compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
return true;
}
}
与公平锁的区别在于新晋获取锁的进程会有多次机会去抢占锁,被加入了等待队列后则跟公平锁没有区别.
- 问: volatile关键字有什么用?
答: 当一个变量被定义成volatile之后,它将具备两个特性;
- 保证可见性
当写一个volatile变量时,JVM会把该线程本地内存中的变量强制刷新到主内存中去,这个写操作会导致其他线程中的volatile变量缓存无效; - 禁止指令重排
使用volatile关键字修饰共享变量可以禁止指令重排序,volatile禁止指令重排序有一些规则:
当程序执行到volatile变量的读写操作时,在其前面的操作的更改肯定全部已经进行,且结果对后面的操作可见,在其后面的操作肯定还没有进行.
在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放在其前面执行.
即执行到volatile变量时,其前面的所有语句都执行完,后面的语句都未执行,且前面的语句的结果对volatile变量及其后面语句可见.
注意,虽然volatile能够保证可见性,但不能保证原子性.volatile变量在各个线程的工作内存中不存在一致性的问题的.但Java的运算操作符不是原子操作,这导致volatile变量的运算在并发下一样不安全.
问: 谈谈volatile的实现原理
答: volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性. 在JVM底层volatile是采用"内存屏障"来实现的. 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,提供3个功能
- 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存
- 如果是写操作,它会导致其他CPU中对于的缓存行无效