Java多线程之线程安全的实现方法

同步互斥(阻塞式同步)

同步互斥是一种常见的并发正确保障手段;其中,同步是指在同一个时刻,多个线程中只能有一个线程去访问共享变量;互斥是指实现同步的一种手段,如临界区、互斥量和信号量;
在Java中,最基本的互斥同步手段就是synchronized关键字;synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit两条字节码指令,
这两条字节码指令隐式地调用了lock和unlock操作,同时,这两条字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象;

synchronized的锁对象:
synchronized关键字使用的锁对象有三种:①如果synchronized关键字修饰实例方法,则锁对象为对应的对象实例;②如果synchronized关键字修饰类方法,则锁对象为
对应的Class对象;③如果synchronized关键字修饰代码块,则锁对象为显式指定的对象;
synchronized的运行原理:
在Java虚拟机规范中,在执行monitorenter指令时,首先尝试获取对象的锁,如果对象没有被锁定或已被当前线程获取,则锁的计数器加1,相应地,执行monitorexit指令时,
锁的计数器减1;当锁的计数器值为0时,锁就被释放;如果获取对象的锁失败,那么将会阻塞当前线程并处于等待状态,知道对象锁被另外一个线程释放为止;
需要注意两点:
①拥有锁的线程是可以再次获取锁的,即可以多次进入被同一锁对象锁住的代码块或方法,保证了线程不会被自己锁死;
②只有拥有锁的线程才可以执行synchronized代码块或方法,没有获取到锁的线程,将会被阻塞;

由于Java的线程是映射到操作系统的原生线程上的,因此线程的阻塞和唤醒是需要操作系统帮忙的,即从用户态转为核心态,状态转换往往需要耗费大量的处理器时间;因此,对于

简单的代码块使用synchronized同步,往往状态转换的时间会比代码块的执行时间还要长,因此,synchronized是Java语言中的一个重量级的锁;
除了synchronized关键字,还可以使用java.util.concurrent包中的重入锁Reentrantlock来实现同步;

synchronized与Reentrantlock的异同点:
相同点:同步互斥、线程可重入
不同点:①synchronized是原生语法层面上的互斥锁,Reentrantlock是API层面的互斥锁;
②Reentrantlock可以通过含参构造器创建公平锁,synchronized只能是非公平锁;
③Reentrantlock可以实现等待可中断,即长时间获取不到锁,可以放弃等待,转去处理其他事情;
④Reentrantlock可以绑定多个条件,通过多次调用newCondition()方法即可,而synchronized的锁对象只能实现一个隐含条件;

JDK1.6以后,性能因素就不再是选择Reentrantlock的理由了,在未来的性能改进中,虚拟机将会更加偏向于原生的synchronized,所以提倡在synchronized能实现需求的情况下,

优先使用synchronized进行同步处理;

非阻塞同步

互斥同步的主要问题在于进行线程阻塞和唤醒需要转入核心态,由此带来的性能问题,因此这种同步也称为阻塞同步;

与之相对应地,非阻塞同步就是不需要将线程挂起,先进行操作,如果没有其他线程争用共享数据,那么操作成功;如果有争用,则采取其他的补偿措施(最常见的就是
不断重试,直到成功为止);

乐观锁与悲观锁:
采用互斥同步实现的锁是悲观锁,即总是认为只要不去做正确的同步措施(如加锁),就会出现线程安全问题,而不论是否真的会出现竞争,都要进行加锁、用户态核心态
转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作;
采用冲突检测的乐观并发策略实现的锁是乐观锁,即先进行操作,如果没有其他线程争用共享数据,那么操作成功;如果有争用,则采取其他的补偿措施(最常见的就是
不断重试,直到成功为止);
悲观并发策略总是认为一定会出现线程安全问题,而乐观并发策略认为有可能出现线程安全问题;

非阻塞同步中常用的操作:测试并设置、获取并增加、交换、比较并交换(CAS)、加载链接/条件存储(LL/SC);这些操作只需要一条处理器指令即可完成;

非阻塞同步需要重点理解CAS操作,因为许多非阻塞同步都是采用CAS实现的;

无同步方案

可重入代码:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的;

线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看这些共享数据的代码是否能保证在同一个线程中执行?如果可以,那么就将共享数据的可见
范围限制在同一个线程之内,这样无需同步也可保证线程之间不会出现数据争用的问题;

Java中通过java.lang.ThreadLocal类来实现线程本地存储的功能;

大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中完成,其中最重要的一个应用实例就是经典Web交互模型中的
“一个请求对应一个服务器线程”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用可以使用线程本地存储来解决线程安全问题;

猜你喜欢

转载自blog.csdn.net/boker_han/article/details/79466965