当前位置:   article > 正文

线程安全与锁优化(一)_如何优化线程安全机制

如何优化线程安全机制

参考:《深入理解Java虚拟机》,
ps:这是一本相当经典的理解java虚拟机的书,推荐!!!

一、概述与引入

我们之前都有使用过多线程,比如在写聊天室的时候,如果要实现群聊天,那么就是要有很多个客户机一起运行,客户机把消息发到服务器,再由服务器转发给所有在这个群里的客户机,然而线程我们都在使用,我们日常也会听到线程安全这个概念。
在百度百科上是这么去释义的:多线程在不考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者调用方在进行任何协调操作,调用这个对象的行为都可以得到正确结果,那就可以认为这个对象是线程安全的。(在《深入了解java虚拟机》一书中的作者也比较认同这样的阐述的。

二、java的线程安全

Brian Goetz在发表过的一篇论文上将各种操作共享的数据分成了五类,按照“线程安全”的强弱划分。下面我将用自己的语言来谈谈我对这五类情况的划分理解。

1.不可变

可以这么肯定,不可变的对象一定是线程安全的,从而也是不需要线程安全保障措施的。
final:,之前我们了解到volatile关键字的可见性,而我们final同样的具备可见性
不可变对象需要满足几个条件:(1)对象创建之后状态就不能修改;(2)对象所有域是final类型;(3)对象正确创建,this引用没有逃逸。
java.lang.String是一个很典型的不可变对象,看到它的底层代码就会发现:
public final class String
除了String之外,还有java.lang.Number的一些子类,如:

public final class Byte extends Numberimplements Comparable<Byte>
public final class Double extends Numberimplements Comparable<Double>
//Float, Integer, Long, Short 
  • 1
  • 2
  • 3
但是这几个其他子类我并没有发现有final的修饰:
AtomicInteger, AtomicLong, BigDecimal, BigInteger
  • 1
  • 2

AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS(compare-and-swap)技术。
在这里插入图片描述
我们会发现他的底层代码的一些变量会有volatile和final的修饰
它依赖于Unsafe提供的一些底层能力,进行底层操作;以volatile的value字段,记录数值,以保证可见性。
final:参考浅谈Java中的final关键字

2.绝对线程安全

在java里面自己标注是线程安全的类基本都不是线程绝对安全的。
所有关于绝对线程安全的类,小编并没有查到,就套用书上的一个例子去讲一下,标注了线程安全的java.util.vector容器。

    public synchronized void setSize(int newSize) {
        modCount++;
        if (newSize > elementCount) {
            ensureCapacityHelper(newSize);
        } else {
            for (int i = newSize ; i < elementCount ; i++) {
                elementData[i] = null;
            }
        }
        elementCount = newSize;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

翻看一下底层源码就会发现,大多数方法都使用了synchronize关键字或者final,static修饰,尽管这么多的方法都被修饰成同步的,但不是意味着在使用的时候就不需要同步手段了。
例子:在一个线程中访问一个元素,一个线程中删除一个元素,这就可能在删除或者访问的时候发生越界的行为,这时候就需要加上同步。

3.相对线程安全

java中大部分的线程安全类都属于相对线程安全类,譬如:Vector,HashTable,Collections的synchronizeCollection()方法包装的集合。
保证对象单独操作是线程安全的,调用的时候不需要额外的保障措施,但是对于一些特定的顺序的连续调用就可能需要在调用端使用额外的同步手段来保证调用的正确性。

4.线程兼容

对象本身并不是线程安全的,但是可以在调用端正确使用同步手段来保证对象的线程安全,我们可以发现ArrayList 的API上面有这样的阐述:
如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:
List list = Collections.synchronizedList(new ArrayList(…));

5.线程对立

无论采取什么样的措施都是无法再多线程的环境下并发使用的代码。(不过这种代码很少有)
例子:
Thread类里有两个方法:尝试恢复线程和尝试中断线程,无论是否存在同步,目标线程都会产生死锁风险。(但是被废弃的)
但是还存在:System.setIn(),System.setOut()和System.runFinalizersOnExit()

三、线程安全实现方法

上面的一大堆讲的基本是了解线程安全的,当我们了解了多线程的这些线程安全的情况,那么我们也知道线程安全是需要去做到的,线程不安全是有害需要避免的。
线程安全的保证:(1)代码的编写;(2)java虚拟机提供的锁机制和同步。

1.互斥同步

概念

同步: 保证在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)线程使用。
互斥:是实现同步的手段,临界区,互斥量,信号量都是主要的互斥实现方式。

基本手段:synchronize

synchronize:有三个应用
(1)同步普通方法,锁的是当前对象;
(2)同步静态方法,锁的是class对象;
(3)同步块,锁的是块里的对象{};
实现原理:
若是指在同步块,java在编译之后会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。
在之前调用monitor.enter指令,在退出方法插入monitor.exit指令。
本质:对对象监视器(Monitor)进行获取,而获取过程的排他性使得在同一时刻只能实现一个线程访问。
对于没有获得锁的线程就会被堵塞在方法入口,直到有线程释放锁才可能被唤醒,继续获取锁。在这里插入图片描述

wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

其他手段

使用java.util.concurrent包下的重入锁(ReentrantLock)实现同步。
synchronized是基于JVM层面实现的,而Lock是基于JDK层面实现的
ReentrantLock的高级功能主要有三项:

(1)等待可中断

持有锁的线程长期不释放锁的时候,正在等待的线程就放弃等到,去处理其他的事情。

(2)可实现公平锁

公平锁:多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序依次获得锁;
非公平锁:在锁被释放的时候,每个等待的锁都有同等的机会获得。

(3)锁可绑定多个条件

一个ReentrantLock对象可以绑定多个Condition对象,直接多次newCondition()。

2.非阻塞同步

上面我们了解了互斥同步,主要是线程阻塞和唤醒带来了性能上的问题,所以这也叫阻塞同步。
从处理问题的方式来看,互斥同步是一种悲观的并发策略,总是认为不去做正确的同步措施(加锁),那就肯定会产生问题,无论共享数据是否进行竞争,它都要加锁。
与之相对的是基于冲突检测的乐观的并发策略。先进行操作,如果没有数据进行竞争,那就继续执行,若是有冲突,就采取补偿措施,不需要把线程挂起。
但是呢,这个操作和冲突检测是原子性的,这是靠硬件指令来保证的,这些常用的指令包括:
(1)测试并设置
(2)获取并增加
(3)交换
(4)比较并交换
(5)加载链接/条件存储

3.无同步方案

前面主要讲了两种保证线程安全的手段:重入锁,互斥同步
现在看到这个无同步是不是有点惊讶,其实线程安全不一定需要同步,同步只是保证共享数据正确的竞争的时候,但是一些不涉及共享数据,那就无需同步去保证正确性,因为有些就是天生的线程安全的。下面来看看这两个例子。

(1)可重入代码:纯代码

可重入:顾名思义,可以重新进入执行代码,在执行代码执行到一半的时候中断它,转而去执行其他的代码,等重新执行回中断的代码的时候,原来的结果不会发生改变。
可重入代码的特征:
A.不依赖存储在堆上的数据和公用的系统资源;
B.用到的状态量都由参数中传入;
C.不调用非可重入方法

(2)线程本地存储(Thread Local Storage)

如果一段代码中所需要的数据可以保证在同一个线程的范围内可见,那么就无需同步,就不会出现数据争用的问题啦。
经典样例:
A.使用消费队列的架构模式(生产者-消费者模式)
B.web交互模型中“一个请求对应一个服务器线程”

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/560585
推荐阅读
相关标签
  

闽ICP备14008679号