线程安全与锁优化

来源:互联网 发布:it教育培训中心 编辑:程序博客网 时间:2024/06/05 18:18

一、java 语言中的线程安全
1.java 中操作共享的数据的5类操作
(1)不可变:java语言中,如果共享数据是基本类型,只要定义时使用final关键字修饰就可以保证它不可变;如果共享数据类型是对象,那就需要保证对象的行为(对象中的方法)不会对其状态产生任何影响才行,最简单的就是把对象中带有状态的变量都声明为final,在构造函数结束后,这样就是不可变的了。线程安全的。在java API中 java.lang.String| java.lang.Long| java.lang.Double |java.lang.Biginteger |java.lang.BigDcimal 都是不变类,AtomicInteger、AtomicLong不是。

(2)绝对线程安全:java API中不存在线程安全的类。

(3)相对线程安全:在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。例如Vector、HashTable、Collections、SynchronizedCollections方法包装的集合等。

package cn.sy.edu;import java.util.Vector;/** * 对Vector线程安全的测试 * @author shier * */public class VectorTest {    private static Vector<Integer> vector = new Vector<Integer>();    public static void main(String[] args) {        while(true){            for (int i = 0; i < 10; i++) {                vector.add(i);            }            /**             * 在Vector类中remove方法、size方法、get方法都是线程安全的,但是如果             * 调用端在多线程的环境下不同线程同时调用这几个方法,有可能一个线程把这个数据             * 从Vector集合中删除了,另外一个线程刚好get这个数据,这样就会抛出异常             * 所以要在removeThread、printThread线程的run方法上面加互斥锁             */            Thread removeThread = new Thread(new Runnable() {                @Override                public void run() {                    synchronized (vector) {                        for (int i = 0; i < vector.size(); i++) {                            vector.remove(i);                        }                    }                }            });            Thread printThread = new Thread(new Runnable() {                @Override                public void run() {                    synchronized (vector) {                        for (int i = 0; i < vector.size(); i++) {                            System.out.println(vector.get(i));                        }                    }                                   }            });            removeThread.start();            printThread.start();            //不要同时产生过多的线程,否则会导致操作系统假死            while(Thread.activeCount()>20);        }    }}

(4)线程兼容:java API中大部分类都是线程兼容的,例如HashMap、ArrayList等。

(5)线程对立:指无论调用端是否采取同步措施,都不能在多线程的环境中并发使用代码,这样的代码在多线程的环境中容易造成死锁。例如, System.setIn(in)、System.setOut(out)、System.runFinalizersOnExit(value)等方法。

二、线程安全的实现方法
1.互斥同步(悲观并发策略)
(1)synchronized:synchronized关键字编译之后,会在同步块前后分别形成monitorenter、monitorexit两个字节码指令,这两个字节码都需要一个reference类型的参数来指明锁定和解锁的对象。synchronized的同步块队同一个线程来说是可以重入的,不会出现把自己锁死的问题;同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。synchronized是java语言中一个重量级操作。

(2)ReentrantLock:ReentrantLock与synchronized关键字很相似,只是在写法上面有一些区别。还比synchronized增加了一些高级特性:等待可中断,当前持有锁的线程长期不释放锁,正在等待的线程可放弃等待,改作别的事情;公平锁,多个线程在等待同一个锁时,必须按照申请锁的时间来依次获得锁,ReentrantLock可以通过带布尔值的构造函数使用公平锁;锁绑定多个条件,一个ReentrantLock可以同时绑定多个Condition。

(3)synchronized关键字和ReentrantLock区别:一个表现为原生语法层面的互斥锁,一个表现为API层面的互斥锁。jdk1.6以前ReentrantLock在性能上要优于synchronized;但是jdk1.6以后,在虚拟机的性能改进中加入了许多优化原生synchronized操作,它们性能基本持平,以后更应该优先使用synchronized。

2.非阻塞同步(乐观并发策略)
(1)非阻塞同步的定义:首先先进性操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采用其他的补偿措施(最常用的补偿措施就是不断的尝试,直到成功为止),这种乐观的并发的策略不需要把线程挂起,因此称为非阻塞同步。

(2)硬件指令集的发展保证了需要操作和冲突检测这两个步骤的原子性。这类指令常用的有:测试并设置(Test-and-Set);获取并增加(Fetch-and-Increment);交换(Swap);比较并交换(CAS);加载链接/条件存储(LL/SC)。

(3)CAS指令有3个操作数。分别是内存位置(变量的内存地址,用V表示),旧的预期值(用A表示),新值(用B表示),CAS指令执行时,当且仅当V符合A时,处理器用B更新V的值,否则它就不执行更新,但是无论是否更新V的值,都会返回V的旧值,CAS是原子操作。AtomicInteger的incrementAndGet方法就是CAS操作。CAS有一个ABA漏洞问题。

3.无同步方案
(1)可重入代码:所有可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。可重入代码不依赖在堆上的数据和公用的系统资源、用到的状态量都由参数中传入,不调用非可重入的方法。可重入方法代码,返回结果是可以预测的,只要输入相同的数据,就能返回相同的结果。

(2)线程本地存储:生产者-消费者模式,例如web交互模型中的“一个请求对应一个服务器线程”。在 java 语言中,如果一个变量要被一个线程独享,可以通过ThreadLocal类来实现线程本地存储功能。

三、锁优化
1.自旋锁和自适应自旋
(1)定义:如果物理机器有一个以上的处理器,能让两个或者以上的线程同时并行执行,我们可以让后面那个请求锁的那个线程“稍等一下”但不放弃处理器的执行时间,看看持有锁的线程是否能很快的释放锁。为了让线程等待,我们需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。jdk1.6中默认开启,自旋次数的默认值是10次,可以使用-XX:PreBlockSpin来更改。

(2)自旋锁虽然避免了线程切换的开销,在锁被占用时间短的时候效果非常好。但是当一个锁占用的时间长的时候,因为自旋也占用线程,这时候使用自旋锁白白浪费CPU时间。所以jdk1.6引入了自适应自旋锁。

(3)自适应自旋锁:自适应意味着自旋的时间不在固定了,而是有前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋很有可能成功,进而它将允许自旋等待持续相对更长的时间,比如100个自旋。另外,如果对于某个锁,自旋很少成功获得过,那在以后获取这个锁时,将可能省略掉自旋的过程,以避免浪费资源处理器。这就是自适应自旋锁。

2.锁消除
(1)定义:锁消除的主要判定依据是源于逃逸分析的数据支持,如果判断在一段代码中,堆上所有数据都不会逃逸出去从而被其他线程访问到,那就可以帮他们当成栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。

(2)逃逸分析:分析对象的动态作用域,当一个对象在方法中被定义后,他可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

3.锁粗化
(1)定义:如果一系列的连续操作都对同一个对象反复的加锁和解锁,甚至加锁操作是出现在方法的循环体内,那即使没有线程竞争频繁地进行互斥同步的操作也会导致不必要的性能损耗。

4.轻量级锁
(1)定义:轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

(2)轻量级锁的加锁过程:在代码进入同步块时,如果此同步块没有被锁定(锁标志为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Work(存储对象运行时数据)的拷贝(Displaced Mark Work);然后,虚拟机将使用CAS操作尝试将对象的Mark Work更新为指向Lock Record 的指针。如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Work的锁标志位将转变为“00”,表示锁处于轻量级锁定状态;如果更新操作失败,虚拟机首先会检查对象的Mark Work是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个对象被其他线程抢占了。如果有两条以上的线程争用同一个锁,那么轻量级锁就不再有效,要膨胀为重量级锁,锁标志变为“10”,Mark Work中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程将进入阻塞状态。

(3)轻量级锁解锁过程:通过CAS操作来进行,如果对象的Mark Work仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Work和线程中复制的Displaced Mark Work替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明其他线程尝试过获取锁,那就要在释放锁的同时,唤醒被挂起的线程。

这里写图片描述

5.偏向锁
(1)定义:偏向锁就是在无竞争的情况下把整个同步都取消掉,连CAS操作都不做。

(2)偏向锁加锁过程:虚拟机启用了偏向锁,那么,当锁对象第一次被线程获取的时候虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个线程的ID记录在对象的Mark Work中,如果CAS操作成功,持有偏向锁的线程以后进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。

(3)偏向锁解锁过程:当有另外一个锁尝试获取这个锁时,偏向模式宣告结束。根据锁对象是否处于被锁定的状态,撤销偏向后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作和上面的轻量级锁一样执行。

这里写图片描述

这里写图片描述

0 0
原创粉丝点击