Java多线程之锁怎么使用


本篇内容介绍了“Java多线程之锁怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!首先强调一点:Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁。同时,类锁也是对象锁,类是Class对象核心思想关键字在实例方法上,锁为当前实例关键字在静态方法上,锁为当前Class对象关键字在代码块上,锁为括号里面的对象在进行线程执行顺序的时候,如果添加了线程睡眠,那么就要看锁的对象是谁,同一把锁 / 非同一把锁是不一样的synchronized 是Java提供的关键字,用来保证原子性的synchronized的作用域如下作用在普通方法上,此方法为原子方法:也就是说同一个时刻只有一个线程可以进入,其他线程必须在方法外等待,此时锁是对象作用在静态方法上,此方法为原子方法:也就是说同一个时刻只有一个线程可以进入,其他线程必须在方法外等待,此时锁是当前的Class对象作用在代码块上,此代码块是原子操作:也就是说同一个时刻只有线程可以进入,其他线程必须在方法外等待,锁是 synchronized(XXX) 里面的 XXX先看一段简单的代码执行之后,对其进行执行javap -v命令反编译总结:使用synchronized修饰的同步方法通过反编译我们可以看到,被synchronized修饰的方法,其中的 flags中有一个标记:ACC_SYNCHRONIZED当线程执行方法的时候,会先去检查是否有这样的一个标记,如果有的话,说明就是一个同步方法,此时会为当前线程设置 monitor ,获取成功之后才会去执行方法体,执行完毕之后释放monitor使用synchronized修饰的代码块通过反编译我们看到,在代码块的两侧有JVM指令,在进入代码块之前指令是 monitorenter当线程执行到代码块的时候,会先拿到monitor(初始值为0),然后线程将其设置为1,此时当前线程独占monitor如果当前持有monitor的线程再次进入monitor,则monitor的值+1,当其退出的时候,monitor的次数-1当线程线程退出一次monitor的时候,会执行monitorexit指令,但是只有持有monitor的线程才能获取并执行monitorexit指令,当当前线程monitor为0的时候,当前线程退出持有锁此时其他线程再来争抢但是为什么要有两个 monitorexit呢?这个时候我们会发现synchronized是可重入锁,其实现原理就是monitor的个数增加和减少同时wait / notify方法的执行也会依赖 monitor,所以wait和notify方法必须放在同步代码块中,否则会报错 java.lang.IllegalMonitorstateException因为方法区域很大,所以设置一个标记,现在执行完判断之后,就全部锁起来,而代码块不确定大小,就需要细化monitor的范围ReentrantLock是Lock接口的一个实现类在ReentrantLock内部有一个抽象静态内部类Sync其中一个是 NonfairSync(非公平锁),另外一个是 FairSync (公平锁),二者都实现了此抽象内部类Sync,ReentrantLock默认使用的是 非公平锁 ,我们看一下源码:Lock接口Condition接口为什么官方提供的是非公平锁,因为如果是公平锁,假如一个线程需要执行很久,那执行效率会大大降低ReentrantLock的其他方法总结:1.ReentrantLock是独占锁2.ReentrantLock是可重入锁3.底层使用AbstractQueuedSynchronizer实现4.synchronized 和 ReentrantLock的区别synchronized是是关键字,可以作用在静态方法、普通方法、静态代码块,底层使用monitor实现,synchronized是内置锁,是悲观锁,其发生异常会中断锁,所以不会发生死锁。是非中断锁ReentrantLock是类,作用在方法中,其比synchronized更加灵活,但是必须手动加锁释放锁,是乐观锁,发生异常不会中断锁,必须在finally中释放锁,是可中断的,使用Lock的读锁可以提供效率AQS:AbstractQueueSynchronizer => 抽象队列同步器AQS定义了一套多线程访问共享资源的同步器框架,很多同步器的实现都依赖AQS。如ReentrantLock、Semaphore、CountDownLatch …首先看一下AQS队列的框架它维护了一个volatile int state (代表共享资源)和一个FIFO线程等待队列(多线程争抢资源被阻塞的时候会先进进入此队列),这里的volatile是核心。在下个部分进行讲解~state的访问方式有三种getState()setState()compareAndSetState()AQS定义了两种资源共享方式:Exclusive(独占,只有一个线程可以执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore、CountdownLatch)不同的自定义同步器争用共享资源的方式也不同。自定义免费云主机域名的同步器在实现的时候只需要实现共享资源的获取和释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队)AQS在顶层已经实现好了。自定义同步器时需要实现以下方法即可isHeldExclusively():该线程是否正在独占资源。只有用的Condition才需要去实现它tryAcquire(int):独占方式。尝试获取资源,成功返回true,否则返回falsetryRelease(int):独占方式。尝试释放资源,成功返回true,否则返回falsetryAcquireShared(int):共享方式。尝试获取资源。负数表示失败,0表示成功但没有剩余可用资源,正数表示成功,且还有剩余资源tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则返回fasle以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁,然后将state+1,此后其他线程在调用tryAcquire()就会失败,直到A线程unlock()到state为0为止,其他线程才有机会获取该锁。当前在A释放锁之前,A线程是可以重复获取此锁的(state)会累加。这就是可重入,但是获取多少次,就要释放多少次。再和CountdownLock为例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程的个数一致)。这N个子线程是并行执行的,每个子线程执行完之后countDown一次。state会CAS-1。等到所有的子线程都执行完后(即state=0),会upark()主调用线程,然后主调用线程就会从await()函数返回,继续剩余动作一般来说,自定义同步器要么是独占方法,要么是共享方式,也只需要实现tryAcquire – tryRelease,tryAcquireShared – tryReleaseShared 中的一组即可,但是AQS也支持自定义同步器同时实现独占锁和共享锁两种方式,如:ReentrantReadWriteLockAQS的源码AbstractQueueSynchronizer 继承了 AbstractOwnableSynchronizerAbstractOwnableSynchronizer类AbstractQueueSynchronizer类总结:1.AQS为我们提供了很多实现。AQS内部有两个内部类,ConditionObject和Node节点2.和开头说的一样,其维护了一个state和一个队列,也提供了独占和共享的实现3.总结一下流程调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功就直接返回没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式acquireQueued()使得线程在队列中休息,有机会(轮到自己,会被unpark())会去尝试获取资源。获取到资源之后才会返回。如果在整个等待过程中被中断过,就返回true,否则返回false如果线程在等待过程中被中断过,它不是响应的。只是获取资源之后才再进行自我中断selfInterrupt(),将中断补上4.release() 是独占模式下线程共享资源的底层入口,它会释放指定量的资源,如果彻底释放了(state = 0)5.如果获取锁的线程在release时异常了,没有unpark队列中的其他结点,这时队列中的其他结点会怎么办?是不是没法再被唤醒了?这时,队列中等待锁的线程将永远处于park状态,无法再被唤醒!6.获取锁的线程在什么情形下会release抛出异常呢 ?线程突然死掉了?可以通过thread.stop来停止线程的执行,但该函数的执行条件要严苛的多,而且函数注明是非线程安全的,已经标明Deprecated;线程被interupt了?线程在运行态是不响应中断的,所以也不会抛出异常;7.acquireShared()的流程tryAcquireShared()尝试获取资源,成功则直接返回;失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。8.releaseShared()释放掉资源之后,唤醒和后继7.不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。volatile是Java提供的关键字,是轻量级的同步机制 JSR133提出,Java5增强了语义volatile关键字有三个重要的特点保证内存可见性不保证原子性禁止指令重排序提到volatile,就要提到JMM – 什么是JMMJMM:Java Memory Model本身就是一种抽象的概念,并不真实存在,它描述的是一组规范和规则,通过这种规则定义了程序的各个变量(包括实例字段、静态字段、和构造数组对象的元素)的访问方式JMM关于同步的规定线程解锁前,必须把共享变量的值刷新到主内存线程加锁前,必须读取主内存的最新的值到自己的工作内存加锁和解锁必须是同一把锁happens-before 规则前一个操作对下一个操作是完全可见的,如果下一个操作对下下一个操作完全可见,那么前一个操作也对下下个操作可见重排序JVM对指令的执行,会进行优化重新排序,可以发生在编译重排序、CPU重排序什么是内存屏障?内存屏障分为2种读屏障(LoadBarrier)写屏障(Store Barrier)内存屏障的作用阻止屏障两侧的指令重排序强制把缓冲区 / 高速缓存中的脏数据写回主内存,或者让缓存中相应的的数据失效编译器生成字节码的时候,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样就可以保证在任何处理器平台,任何程序中都有正确的volatile语义在每个volatile写操作之前插入一个StoreStore屏障在每个volatile写操作之后入一个StoreLoad屏障在每个volatile读操作之前插入一个LoadLoad屏障在每个volatile读操作之前插入一个LoadStore屏障原子性问:i++为什么不是线程安全的?因为 i++ 不是原子操作,i++有三个操作如何解决?使用 synchronized使用AtomicInteger [JUC下的原子类]有序性1.计算机在执行程序的时候,为了提高性能,编译器和处理器通常会对指令重排序,一般分为3种-源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行的指令单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致处理器在执行重排序之前必须考虑指令之间的数据依赖性多线程环境种线程交替执行,由于编译器优化重排序的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测2.指令重排序多线程环境种线程交替执行,由于编译器优化重排序的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测此时使用volatile禁用指令重排序,就可以解决这个问题volatile的使用单例设计模式中的 安全的双重检查锁volatile的底层实现根据JMM,所有线程拿到的都是主内存的副本,然后存储到各自线程的空间,当某一线程修改之后,立即修改主内存,然后主内存通知其他线程修改Java代码 instance = new Singleton();//instance 是 volatile 变量 汇编代码:0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp); 有 volatile 变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查 IA-32 架构软件开发者手册可知,lock 前缀的指令在多核处理器下会引发了两件事情。将当前处理器缓存行的数据会写回到系统内存。这个写回内存的操作会引起在其他 CPU 里缓存了该内存地址的数据无效。如果对声明了volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术CAS是原子操作,保证并发安全,而不能保证并发同步CAS是CPU的一个指令(需要JNI调用Native方法,才能调用CPU的指令)CAS是非阻塞的、轻量级的乐观锁我们可以实现通过手写代码完成CAS自旋锁CAS包括三个操作数内存位置 – V期望值- A新值 – B如果内存位置的值与期望值匹配,那么处理器会自动将该位置的值设置为新值,否则不做改变。无论是哪种情况,都会在CAS指令之前返回该位置的值。上述是我们自己书写的CAS自旋锁,但是JDK已经提供了响应的方法Java提供了 CAS 的支持,在 sun.misc.Unsafe 类中,如下参数说明var1:表示要操作的对象var2:表示要操作对象中属性地址的偏移量var4:表示需要修改数据的期望的值var5:表示需要修改为的新值CAS通过调用JNI的代码实现,JNI:Java Native Interface ,允许Java调用其他语言而CompareAndSwapXxx系列的方法就是借助“C语言”CPU底层指令实现的以常用的 Inter x86来说,最后映射到CPU的指令为“cmpxchg”,这个是一个原子指令,CPU执行此命令的时候,实现比较并替换的操作cmpxchg 如何保证多核心下的线程安全系统底层进行CAS操作的时候,会判断当前操作系统是否为多核心,如果是,就给“总线”加锁,只有一个线程对总线加锁,保证只有一个线程进行操作,加锁之后会执行CAS操作,也就是说CAS的原子性是平台级别的CAS这么强,有没有什么问题?高并发情况下,CAS会一直重试,会损耗性能CAS的ABA问题CAS需要在操作值得时候检查下值有没有变化,如果没有发生变化就更新,但是如果原来一个值为A,经过一轮的操作之后,变成了B,然后又是一轮的操作,又变成了A,此时这个位置有没有发生改变?改变了的,因为不是一直是A,这就是ABA问题如何解决ABA问题?解决ABA问题就是给值增加一个修改版本号,每次值的变化,都会修改它的版本号,CAS在操作的时候都会去对比此版本号。下面给出一个ABA的案例Java中ABA解决办法(AtomicStampedReference)AtomicStampedReference 主要包含一个引用对象以及一个自动更新的整数 “stamp”的pair对象来解决ABA问题修改之后完成ABA问题“Java多线程之锁怎么使用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注百云主机网站,小编将为大家输出更多高质量的实用文章!

相关推荐: serialversionuid是什么及有什么作用

本篇内容主要讲解“serialversionuid是什么及有什么作用”,感兴趣的免费云主机域名朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“serialversionuid是什么及有什么作用”吧!答:serialversion…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

Like (0)
Donate 微信扫一扫 微信扫一扫
Previous 05/14 21:52
Next 05/14 21:52

相关推荐