Java基础(五)-多线程-2

  • 问:如何实现子线程先执行,主线程再执行
    答: 启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行.

扩展阅读

Thread类提供了让一个线程等待另一个线程完成的方法--join()方法. 当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完成为止.

  • 问: 说一说synchronized与Lock的区别
    答:
  1. synchronized是Java的关键字,在JVM层面实现加锁和加锁;Lock的一个接口,在代码层面实现加锁和解锁.
  2. synchronized可以用在代码块上,方法上;Lock只能写在代码里.
  3. synchronized在代码执行完或出现异常时会自动释放锁;Lock不会自动释放锁,需要在finally中显式释放锁
  4. synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间
  5. synchronized无法得知是否获取锁成功;Lock则可以通过tryLock()得知是否加锁成功
  6. synchronized锁可重入,不可中断,非公平;Lock锁可重入,可中断,可公平\不公平,并可以细分读写锁以提高效率.
  • 问: 说一说synchronized的底层实现原理
    答:
  1. 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指令出现了两次,第一次为同步正常退出释放锁,第二次为发生异步退出释放锁.
  2. 方法的同步并没有通过monitorenter和monitorexit指令来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标志符.JVM就是根据该标示符来实现方法的同步:
    当方法调用时,调用指令会检查方法的ACC_SYCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取方法成功之后才能执行方法体,方法执行完成后再释放monitor.在方法执行期间,其他线程都无法再获得同一个monitor对象.
  3. 总结
    两种同步方法本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成. 两个指令的执行是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的内部结构

    首先ReentrantLock实现了Lock接口,然后有3个内部类,其中Sync内部类继承自AQS,另外的两个内部类继承自Sync,这两个类分别是用来公平锁和非公平锁的. 通过Sync重写的方法tryAcquire,tryRelease可以知道,ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁.

  • 问 如果不使用synchronized和Lock,如何保证线程安全?
    答:

  1. volatile
    关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值. 需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
  2. 原子变量
    在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步. 例如 AtomicInteger表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer. 可拓展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问.
  3. 本地存储
    可以通过ThreadLocal类来实现线程本地存储的功能,每个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以TheadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量.
  4. 不可变的
    只有一个不可变的对象被正确地构建出来,那其他外部的课件状态永远不会改变,永远不会看到它在多个线程之中处于不一致的状态. 可以参考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的内部结构

    ReentrantLock的公平锁和非公平锁都委托 了AbstractQueuedSynchronizer#acquire去请求获取

public final void acquire(int arg){
   if(!tryAcquire(arg) &&   acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}

  1. tryAcquire是一个抽象方法,是公平与非公平的实现原理所在
  2. addWaiter是将当前线程结点加入等待队列之中. 公平锁在锁释放后会严格按照等到队列去取后续值,而非公平锁在对于新晋线程有很大优势.
  3. acquireQueued在多次循环中尝试获取到锁或者将当前线程阻塞
  4. 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之后,它将具备两个特性;
  1. 保证可见性
    当写一个volatile变量时,JVM会把该线程本地内存中的变量强制刷新到主内存中去,这个写操作会导致其他线程中的volatile变量缓存无效;
  2. 禁止指令重排
    使用volatile关键字修饰共享变量可以禁止指令重排序,volatile禁止指令重排序有一些规则:
    当程序执行到volatile变量的读写操作时,在其前面的操作的更改肯定全部已经进行,且结果对后面的操作可见,在其后面的操作肯定还没有进行.
    在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放在其前面执行.
    即执行到volatile变量时,其前面的所有语句都执行完,后面的语句都未执行,且前面的语句的结果对volatile变量及其后面语句可见.
  • 注意,虽然volatile能够保证可见性,但不能保证原子性.volatile变量在各个线程的工作内存中不存在一致性的问题的.但Java的运算操作符不是原子操作,这导致volatile变量的运算在并发下一样不安全.

  • 问: 谈谈volatile的实现原理
    答: volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性. 在JVM底层volatile是采用"内存屏障"来实现的. 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,提供3个功能

  1. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存
  3. 如果是写操作,它会导致其他CPU中对于的缓存行无效
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容