线程安全的实现方法-动力节点

线程作为java知识体系中一个重要的支撑,线程安全问题也变得尤为重要。Java线程安全是整个java系统安全的核心,实现线程安全并不仅仅和代码的编写有关,虚拟机提供的同步和锁机制也起到了至关重要的作用。那么线程安全是怎么实现的呢?接下来,为大家揭晓答案:
一、互斥同步
互斥同步是常见的一种并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。如此来看的话,在这4个字里面,互斥是因,同步是果,互斥是方法,同步是目的。
在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
重入锁(ReentrantLock):除synchronized外,还可使用ReentrantLock来实现同步,相比synchronized,ReentrantLock提供了一些高级功能,主要有下面三项:
等待可中断:某线程在等待锁的时候可以放弃等待,转而去处理其他的事情
公平锁:多个线程在等待一个锁时,必须按照申请锁的时间顺序来获得锁。synchronized中的锁是非公平的,释放的一瞬间任何线程都可能获得锁。ReentrantLock默认是非公平的,但可以通过带bool值得构造函数要求使用公平锁。
绑定多个条件:指一个ReentrantLock对象可以同时绑定多个对象,而synchronized中通过wait()和notify()可以实现一个条件的绑定,如果要实现duoy多于一个的绑定,不得不再添加一个锁了。
还有不同的是synchronized和ReentrantLock的效率问题了,ReentrantLock的效率相对较高。但官方再不断对synchronized进行优化,并且synchronized是原生de ,所以还是tich提倡synchronized能实现的情况下,优先考虑synchronized来实现同步。
在JDK 很多针对锁的优化措施,所以在此之后,synchronized和ReentrantLock的性能基本上是完全持平了,因此,性能因素不再是选择ReentrantLock的理由,而虚拟机在未来的性能改进中会更加偏向于原生的synchronized,所以提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
二、非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。而另外一种选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
乐观并发策略需要”硬件指令集的发展”才能进行,因为我们需要操作和冲突检测这两个步骤具备原子性,只能靠硬件来完成,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:
测试并设置(Test-and-Set)
获取并增加(Fetch-and-Increment)
交换(Swap)
比较并交换(Compare-and-Swap,下文称CAS)
加载链接/条件存储(Load-Linked/Store-Conditional,下文称LL/SC)
其中,前面的3条是20世纪就已经存在于大多数指令集之中的处理器指令,后面的两条是现代处理器新增的,而且这两条指令的目的和功能是类似的。
CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示),CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作。
三、无同步方案
线程安全的同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的,比如如下两类:
可重入代码(Reentrant Code):这种代码也叫纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的
可重入代码有些共同的特性,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,就可以把共享数据的可见范围限制在同一个线程内,这样,无须同步也能保证线程之间不会出现数据争用的问题
每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。
通过上面的关于线程安全的实现方法的详细介绍,我们清楚地知道了线程安全的重要性以及其实现方式。只有熟练掌握了线程安全的实现方式,我们才能保证我们编写的程序的线程安全性,为养成良好的编程习惯和提高我们的编程能力打下坚实的基础。

你可能感兴趣的:(java,多线程)