java-并发编程

并发编程

线程安全问题由来
JMM(Java内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量在主存中读取
具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的
由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题

保证并发安全的三大特性
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排

线程五大状态

创建状态:当用new操作符创建一个线程的时候。
就绪状态:调用start方法,处于就绪状态的线程并不一定马上就执行run方法,还需要等待CPU的调度。
运行状态:CPU开始调度线程,并开始执行Run方法。
阻塞状态:线程的执行过程中可能因为一些原因进入阻塞状态,比如调用sleep方法,获取尝试得到一个锁等等。、
死亡状态:Run方法执行完或者执行中遇到异常。
基本状态:创建状态,就绪状态,运行状态

线程之间的协作

线程之间的协作有:wait、notify、notifyAll
(1)wait()方法,调用某个对象的wait()方法能够让当前线程阻塞,前提是当前线程拥有这个对象的锁,此时线程会释放对象锁,待线程被notify()方法或者notifyAll()方法唤醒并重新获得对象锁后,线程会从阻塞的地方向后继续执行程序。wait()方法里还有个timeout的参数,意思是超过了这个时间(秒级),如果没有其他线程获取了对象锁,调用wait(timeout)方法的线程会重新获取对象锁,执行wait(timeout)方法后面的程序。

(2)notify()方法,调用某个对象的notify()方法能够唤醒一个正在等待这个对象锁的线程,前提是调用notify()方法的线程获取了对象锁。注意:调用完notify()方法后,被唤醒的线程不是立刻就获取对象锁执行任务,而是要等待执行notify()方法的线程执行完剩余任务释放了对象锁后,才能有机会获取到对象锁执行wait()后面的程序,这里有机会是说:线程执行完notify并且执行完同步块里的代码后,该线程处于就绪状态,会和notify唤醒后的线程共同竞争,有可能线程执行完一次notify后,又执行了notify,而没让被唤醒的线程获得锁执行。

(3)notifyAll()方法,调用某个对象的notifyAll()方法可以唤醒正在等待这个对象锁的所有线程,前提是调用notifyAll方法的线程获取了对象锁。
(4)join()方法,理解:线程的合并指的是:将指定的线程加入到当前的线程之中,可以将两个交替执行的线程合并为顺序执行的线程,如果在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。作用:让“主线程”等待“子线程”结束之后才能继续运行
(5)yield()方法,暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。
(6)首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。而 Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。

java锁

Synchronized

如果修饰的是具体对象:锁的是对象
如果修饰的是成员方法:那锁的就是 this
如果修饰的是静态方法:锁的就是这个对象.class

原理

对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
在这里插入图片描述
和锁相关的显然就是对象头里存储的那几个内容:

  • 其中的重量级锁也就是通常说 synchronized 的对象锁,其中指针指向的是 monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,monitor 是由ObjectMonitor实现的,C++实现。
  • 注意到还有轻量级锁,这是在 jdk6 之后对 synchronized 关键字底层实现的改进。

1.Synchronized修饰方法
方法级的同步是隐式的。 无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否被声明为同步方法。(静态方法也是如此)
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程(Monitor)(加锁),然后才能执行方法,最后当方法完成 (无论是正常完成还是非正常完成)时释放管程。
在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
2.Synchronized修饰代码块
显式的用指令去操作管程(Monitor)了。
同步一段指令集序列的情况。Java虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义。(monitorenter 和 monitorexit 两条指令是 C 语言的实现)正确实现 synchronized 关键字需要 Javac 编译器与 Java 虚拟机两者共同协作支持。Monitor的实现基本都是 C++ 代码,通过JNI(java native interface)的操作,直接和cpu的交互编程。

改进后的锁(jdk6)

偏向锁->自旋锁->轻量级锁->重量级锁。按照这个顺序,锁的重量依次增加。
从加锁到最后变成以前的那种重量级锁的过程里,新实现出状态不同的锁作为过渡

  • 偏向锁
    它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
    例如:循环结构中的list.append()方法。
  • 轻量级锁
    当偏向锁的条件不满足,亦即的确有多线程并发争抢同一锁对象时,但并发数不大时,优先使用轻量级锁。一般只有两个线程争抢锁标记时,优先使用轻量级锁。 此时,对象头的Mark Word 的结构会变为轻量级锁结构。
  • 自旋锁
    自旋锁是一个过渡锁,是从轻量级锁到重量级锁的过渡。也就是CAS。自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
    但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。
    如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
    CAS的缺点:
    ABA问题:线程1和线程2同时去修改一个变量,将值从A改为B,但线程1突然阻塞,此时线程2将A改为B,然后线程3又将B改成A,此时线程1将A又改为B,这个过程线程2是不知道的,这就是ABA问题,可以通过版本号或时间戳解决
  • 重量级锁:Synchronized

其他不同的锁

按照锁的特性分类:
悲观锁:独占锁,会导致其他所有需要所的线程都挂起,等待持有所的线程释放锁,就是说它的看法比较悲观,认为悲观锁认为对于同一个数据的并发操作,一定是会发生修改的。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。比如前面讲过的,最传统的 synchronized 修饰的底层实现,或者重量级锁。(但是现在synchronized升级之后,已经不是单纯的悲观锁了)
乐观锁:每次不是加锁,而是假设没有冲突而去试探性的完成操作,如果因为冲突失败了就重试,直到成功。比如 CAS 自旋锁的操作,实际上并没有加锁。

按照锁的顺序分类:
公平锁。公平锁是指多个线程按照申请锁的顺序来获取锁。java 里面可以通过 ReentrantLock 这个锁对象,然后指定是否公平非公平锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。使用 synchronized 是无法指定公平与否的,他是不公平的。
独占锁(也叫排他锁)/共享锁:
独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Sychronized 而言都是独占锁。
共享锁:是指该锁可被多个线程所持有。对 ReentrantReadWriteLock 而言,其读锁是共享锁,其写锁是独占锁。读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。
独占锁/共享锁是一种广义的说法,互斥锁/读写锁是java里具体的实现。

lock

在 jdk 5 之后,在 juc 包里有了显式的锁,Lock 完全用 Java 写成,在java这个层面是无关JVM实现的。虽然 Lock 缺少了 (通过 synchronized 块或者方法所提供的) 隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。
Lock 是一个接口,实现类常见的有:
重入锁(ReentrantLock)
读锁(ReadLock)
写锁(WriteLock)
实现基本都是通过聚合了一个同步器(AbstractQueuedSynchronizer 缩写为 AQS)的子类来完成线程访问控制的。
使用方法:

Lock lock = new ReentrantLock();
lock.lock();//获取锁的过程不要写在 try 中,因为如果获取锁时发生了异常,异常抛出的同时也会导致锁释放
try{

}finally{
    lock.unlock();//finally块中释放锁,目的是保证获取到锁之后最后一定能释放锁。
}

基础父类:AbstractQueuedSynchronizer(以下简称同步器或者 AQS)

用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法来进行操作,因为它们能够保证状态的改变是安全的。

这三个方法分别是:
protected final int getState(),// 获取当前同步状态
protected final void setState(int newState),// 设置当前同步状态
protected final boolean compareAndSetState(int expect, int update),// 使用 CAS 设置当前状态,该方法能够保证状态设置的原子性
子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件 (ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 等)。

ReentrantLock

重入锁 ReentrantLock,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。
除此之外,该锁的还支持获取锁时的公平和非公平性选择。
ReentrantLock 支持公平与非公平选择,内部实现机制为:
内部基于 AQS 实现一个公平与非公平公共的父类 Sync ,(在代码里,Sync 是一个内部类,继承 AQS)用于管理同步状态;
FairSync 继承 Sync 用于处理公平问题;
NonfairSync 继承 Sync 用于处理非公平问题。

重入锁的实现原理
通过为每个锁关联一个请求计数器和一个获得该锁的线程。当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁。

ReentrantReadWriteLock

维护了一个读锁、一个写锁,其中读锁是共享锁,写锁是排他锁。

总结

到这里我们知道了 java 的对象都有与之关联的一个锁,这个锁称为监视器锁或者内部锁,通过关键字 synchronized 声明来使用,实际是 jvm 层面实现的,向下则用到了 Monitor 类,再向下虚拟机的指令则是和 CPU 打交道,插入内存屏障等等操作。

而 jdk 5 之后引入了显式的锁,以 Lock 接口为核心的各种实现类,他们完全由 java 实现逻辑,那么实现类还要基于 AQS 这个队列同步器,AQS 屏蔽了同步状态管理、线程排队与唤醒等底层操作,提供模板方法,聚合到 Lock 的实现类里去实现。

这里我们对比一下隐式和显式锁:
隐式锁基本没有灵活性可言,因为 synchronized 控制的代码块无法跨方法,修饰的范围很窄;而显示锁则本身就是一个对象,可以充分发挥面向对象的灵活性,完全可以在一个方法里获得锁,另一个方法里释放。
隐式锁简单易用且不会导致内存泄漏;而显式锁的过程完全要程序员控制,容易导致锁泄露;
隐式锁只是非公平锁;显示锁支持公平/非公平锁;
隐式锁无法限制等待时间、无法对锁的信息进行监控;显示锁提供了足够多的方法来完成灵活的功能;
一般来说,我们默认情况下使用隐式锁,只在需要显示锁的特性的时候才选用显式锁。
对比完了 synchronized 和 Lock 两个锁。对于 java 的线程同步机制,往往还会提到的另外两个内容就是 volatile 关键字和 CAS 操作以及对应的原子类。

因此这里再提一下:

volatile 关键字常被称为轻量级的 synchronized,实际上这两个完全不是一个东西。我们知道了 synchronized 通过的是 jvm 层面的管程隐式的加了锁。而 volatile 关键字则是另一个角度,jvm 也采用相应的手段,保证:
被它修饰的变量的可见性:线程对变量进行修改后,要立刻写回主内存;
线程对变量读取的时候,要从主内存读,而不是缓存;
在它修饰变量上的操作禁止指令重排序。
CAS 是一种 CPU 的指令,也不属于加锁,它通过假设没有冲突而去试探性的完成操作,如果因为冲突失败了就重试,直到成功。那么实际上我们很少直接使用 CAS ,但是 java 里提供了一些原子变量类,就是 juc 包里面的各种Atomicxxx类,这些类的底层实现直接使用了 CAS 操作来保证使用这些类型的变量的时候,操作都是原子操作,当使用他们作为共享变量的时候,也就不存在线程安全问题了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值