从面向过程到面向对象,将数据和行为看成是对象的一部分。
并发编程首先要确保结果的正确性,在这个基础上再去实现高效。
线程安全的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
线程安全从强到弱:不可变,绝对线程安全,相对线程安全、线程兼容和线程对立。
不可变:
final常量,String,
保证对象行为不影响自己的状态:将对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。
绝对线程安全
不管运行环境(串行还是并行)如何,调用者都不需要任何的同步措施。
相对线程安全
我们通常意义上的线程安全。
线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确使用同步手段来保证对象在并发环境中安全地使用。
线程对立
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。
线程安全的实现方法:
1,互斥同步:互斥是实现同步的方法,同步是互斥的结果。同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用。
临界区,互斥量和信号量都是互斥实现方式。最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果java程序中的synchrinized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或class独享来作为锁对象。
在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器+1,相应地,在执行monitorexit时,将计数器-1;当计数器为0,锁就被释放了。
synchronized同步块对同一线程来说是可重入的,不会出现自己把自己锁死的问题。其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。如果要阻塞或唤醒一条线程,需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。synchronized是一个重量级操作。
2,还可以通过j.u.c的重入锁实现同步,在基本用法上,ReentrantLock与synchronized很相似,它们都具备一样的想成重入特性,互斥锁lock和unlock方法配合try/finally语句块来完成。reentrantLock相比synchronized增加了等待可中断,可实现公平锁,以及锁可以绑定多个条件。
等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间长的同步块很有帮助。
公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会。
通过重入锁实现同步 锁绑定多个条件非阻塞同步:互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也被称为阻塞同步。另外,它属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。
基于冲突检测的乐观并发策略:先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断的重试,直到试成功为止。)这种乐观的策略的许多实现都不需要把线程挂起,因此称为非阻塞同步。
乐观并发策略需要“硬件指令集的发展”才能进行。操作和冲突检测需要具备原子性。只能靠硬件来完成这件事情。
CAS:需要三个操作数,分别是内存设置V,旧的预期值A和新值B。CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是不管是否更新了V的值,都会返回V的旧值。
无同步方案:要保证线程安全,并不一定要同步。同步只是保障共享数据争用时的正确手段。如果一个方法本就不涉及共享数据,那它自然就无须任何同步措施保证正确性。因此有些代码天生就是线程安全的。
1,可重入代码:可以在代码执行的任何时刻终止它,转而去执行另外一段代码。而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是线程安全的代码不一定是可重入的。
可重入代码特征:
1,不依赖存储在堆上的数据和公用的系统资源、
2,用到的状态量都由参数中传入
3、不调用非可重入的方法等。
判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然是线程安全的。
线程本地存储:保证共享数据的代码在同一线程中执行,无须同步也可以保证线程间不出现数据争用的问题。
锁优化:
自旋锁和自适应自旋
互斥同步缺点:互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。在许多应用上,共享数据的锁定状态只会持续很短时间,为了这段时间去挂起和恢复线程并不值得。
解决方法:如果有多个处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程等待,我们只须让线程执行一个忙循环,这项技术就是自旋。
自旋锁并不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,繁殖如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,不会做任何有用的工作。因此自旋等待时间必须有一定限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当挂起线程,自旋次数默认为10。可以使用参数-XX:PreBlockSpin来设置。
自适应的自旋锁。
自适应意味着自旋时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机越来越聪明了。
锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的依据:如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
锁粗化
推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也尽可能的拿到锁。
但是如果一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步也会导致不必要的性能消耗。虚拟机探测到有这样的操作,将会把加锁同步的范围扩展到整个操作序列的外部,这样只需要加锁一次就可以了。
轻量级锁
轻量级是相对于使用操作系统互斥量实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。
轻量级锁并不是用来替代重量级锁的,它的本意是在没有所线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
HotSpot虚拟机的对象头的内存布局
对象头分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希吗,GC分代年龄等,这部分数据的长度在32位和64位。是实现轻量级锁和偏向锁的关键。--Mark Word
另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。
网友评论