当前位置:   article > 正文

java中是如何保证线程安全及线程安全常用的关键字_线程安全计数器 java

线程安全计数器 java

LongAddr ConcurrentHashMap

一、什么样的线程是安全的

《Java并发编程实战》对线程安全作出了一个比较恰当的定义:当多个线程同时访问一个对象时,如果不需要考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

1、Java语言里的线程安全
讨论线程安全,将以多个线程之间存在共享数据访问为前提。否则线程是串行执行还是多线程执行根本没有任何区别。

但线程安全并不是分为安全和不安全两类,而是以【安全程度】来分为5类:

1)不可变

  • 不可变(Immutable) 的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。
  • 只要能正确构建一个不可变对象,该对象永远不会在多个线程之间出现不一致的状态。
  • 多线程环境下,应当尽量使对象成为不可变,来满足线程安全。

如果多线程共享的数据是一个基本数据类型,使用final关键字修饰它就可以保证是不可变的;对于引用数据类型,就需要对象自行保证其行为不会对其状态产生影响。

例如java.lang.String类,它是一个典型的不可变对象,用户调用其substring()、replace()、concat()这些方法都不回影响它原来的值,只会返回一个新构造的字符串对象。

保证对象状态不变的方法有很多种,最简单的就是把对象里带有状态定义的变量都声明为final,例如java.lang.Integer类,它将value定义为final来保障不变。

常见的不可变的类型:

final关键字修饰的基本数据类型
枚举类型、String类型
常见的包装类型:Short、Integer、Long、Float、Double、Byte、Character等
大数据类型:BigInteger、BigDecimal
注意:原子类 AtomicInteger 和 AtomicLong 则是可变的。

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

通过Collections.unmodifiableMap(map)获的一个不可变的Map类型。
Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
例如,如果获得的不可变map对象进行put()、remove()、clear()操作,则会抛出UnsupportedOperationException异常。

2)绝对线程安全
“不管运行时环境如何,调用者都不需要任何额外的同步措施”

Java语言中标注自己是线程安全的类,使用起来都不是绝对线程安全,例如:对于数组集合Vector的操作,如get()、add()、remove()都是有synchronized关键字修饰。有时调用时也需要手动添加同步手段,保证多线程的安全。

3)相对线程安全
就是我们通常意义上讲的线程安全,需要保证对这个对象单词的操作是线程安全的。我们在单次操作调用方法时不需要使用额外的同步措施,但对于一些顺序性的连续调用,就需要额外保障了。

在Java中,大部分声称线程安全的类都属于相对线程安全。例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。

4)线程兼容
线程兼容是指对象本身不是线程安全的,但可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全使用。例如ArrayList、HashMap等。

5)线程对立
不管调用端使用了什么同步措施,这段代码都无法在多线程环境下使用。

  • 线程对立是指:无法通过添加同步措施,实现多线程中的安全使用。
  • 线程对立的常见操作有:Thread类的suspend()和resume()(已经被JDK声明废除),System.setIn()和System.setOut()等

二、线程安全问题

产生原因

从前面的分析,在并发编程(多线程编程)中,可能出现线程安全的问题:

  • 多个线程在操作共享的数据。

  • 操作共享数据的线程代码有多条。

  • 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。

并发的核心概念

三个核心概念:原子性、可见性、顺序性。

  • 原子性:跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

锁和同步(同步方法和同步代码块)、CAS(CPU级别的CAS指令cmpxchg)。

  • 可见性:当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。

volatile关键字来保证可见性。

  • 顺序性:程序执行的顺序按照代码的先后顺序执行。因为处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的-即指令重排序

volatile在一定程序上保证顺序性,另外还可以通过synchronized来保证顺序性。

三、 Java线程安全的实现

1.互斥同步(Mutex Exclusion & Synchronization)是一种常见的并发正确性保障手段。

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

互斥是原因,同步是结果。
同步是目的,互斥是方法。

Java中,最基本的实现互斥同步的手段是synchronized关键字,其次是JUC包中的ReentrantLock。 

(1)关于synchronized关键字:

在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经持有了对象的锁,就把锁的计数器的值加1,而执行monitorexit指令时会将锁的计数器减1。一旦计数器的值为0,锁就被释放了。如果获取对象锁失败,那当前对象就应当被阻塞等待,直到锁被持有它的线程释放为止。

推论:

        被synchronized修饰的同步块对同一条线程来说是可重入的;

        持有对象锁的线程在释放锁之前,会阻塞其他线程的进入,直到释放锁。

缺点:

从执行成本来看,持有锁是一个重量级操作,因为Java的线程实现是1:1映射到系统内核线程的,如果阻塞或者切换线程,就需要产生系统调用从用户态切换到内核态。如果同步块代码特别简单,线程切换带来的开销甚至比执行代码本身还要大。

而虚拟机本身也会进行一些优化,例如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切换线程。

(2)关于ReentrantLock:

Java提供了java.util.concurrent包下的Lock接口,让用户以非块结构来实现互斥同步。

重入锁(ReentrantLock)是Lock接口最常见的一种实现,它也是可重入的,相比于synchronized,ReentrantLock增加了一些高级功能。

1)等待可中断:当前持有锁的线程长期不释放锁时,正在等待的线程可以放弃等待,处理其他的事情

2)公平锁:多个线程在等待一个锁时,必须按照申请锁的顺序来依次获取锁,而非公平锁不保证这一点,例如synchronized。不过公平锁会导致ReentrantLock的性能急剧下降。

3)锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象。
 

2、非阻塞同步 

互斥同步主要面临的问题是线程阻塞和唤醒带来的性能开销,因此也称为阻塞同步。互斥同步也是一种悲观的并发策略。无论共享数据是否出现竞争,互斥同步都会进行加锁,将会导致用户态到内核态的切换等开销。实际上有很多情况都会带来不必要的加锁。

有另一种选择:基于冲突检测的乐观并发策略。

通俗来说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果产生了冲突,再进行其他补偿措施。

这种乐观并发策略不再需要把线程阻塞挂起,称为非阻塞同步。

但使用这种并发方式是有前提的:那就是操作和冲突检测必须具有原子性
 

3、无同步方案:如果一个方法不涉及操作共享数据,那么自然就不需要任何方式去保证正确性。

如果一个变量要被多线程访问,可以将它用volatile关键字声明为”易变的“,但这个关键字的修改不能依赖于他本身。

每一个线程的Thread对象中都有一个ThreadLocalMap对象,存储了以ThreadLocal.threadLocalHashCode为K,本地线程变量为V的K-V键值对。ThreadLocal对象就是这个Map的访问入口。

四、其他用于多线程环境保证线程安全的关键字
 

1.Volatile

Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。

Volatile关键字的作用主要有如下两个:
1.线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 顺序一致性:禁止指令重排序。

 2.Synchronized

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。

synchronized关键字解决的是多个线程之间访问资源的同步性,可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。

Synchronized的作用主要有三个:
(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见
(3)有效解决重排序问题。

Synchronized是悲观锁

在JVM中,对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。
对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Monior:我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
 

Volatile与Synchronized比较
Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

3.ConcurrentHashmap

ConcurrentHashmap和HashMap区别:

1.HashMap是 非线程安全的,而HashTabl e和ConcurrentHashmap都

是线程安全的

2.HashMap的key 和value均可以为null;而HashTable和Concur rentHashMap的key和value均不可以为null

3.HashTable 和ConcurrentHashMap的区别:保证线程安全的方式不同:

3.1.HashTable是通过给整张散列表加锁的方式来保证线程安全,这种方式保证了线程安全,但是并发执行效率低下。

3.2.ConcurrentHashMap在JDK1.8之前,采用分段锁机制来保证线程安全的,这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率(当多线程并发访问不同的segment时,多线程就是完全并发的,并发执行效率会提高)

3.3.从JDK1.8开始, ConcurrentHashMap数据结构与1.8中的HashMap保持一致,均为数组+链表+红黑树,是通过乐观锁+Synchroni zed来保证线程安全的.当多线程并发向同一个散列桶添加元素时。若散列桶为空,此时触发乐观锁机制,线程会获取到桶中的版本号,在添加节点之前,判断线程中获取的版本号与桶中实际存在的版本号是否一致,若一致,则添加成功,若不一致,则让线程自旋。

若散列桶不为空,此时使用Synchronized来保证线程安全,先访问到的线程会给桶中的头节点加锁,从而保证线程安全。

4.LongAdder

LongAdder是用来做线程安全的i++自增操作的,我们知道AtomicLong也可以现实这个功能,那为什么需要LongAdder呢?理由很简单,为了效率。AtomicLong是对整个数进行cas,那么当多个线程并发执行increment操作时,注定只有一个线程成功,其他线程都在失败自旋,很浪费cpu资源,LongAdder你可以把它当成一个cell[cpu个数]数组,由于并行度最多就是cpu个数,那么相当于每个线程只cas操作每一个数组元素,几乎不会失败,最后调用一次sum函数统计数组和即可得到总数。
一句话概括
如果说jdk1.7的ConcurrentHashMap是分段悲观锁的话,那么LongAdder就是分段乐观锁

5.AtomicInteger和AtomicLong

(1)AtomicInteger

AtomicInteger类是系统底层保护的int类型,通过对int类型的数据进行封装,提供执行方法的控制进行值的原子操作。AtomicInteger它不能当作Integer来使用

使用AtomicInteger在多线程进行自增运算的时候,是线程安全的,而普通的int在自增的时候则是线程不安全的。

(2) AtomicLong

AtomicLong 的使用跟 AtomicInteger 使用方式是一样的,但是有一点需要注意的是,在编译器编译成字节码时,会先判断 当前JVM 或者 机器硬件是否支持 8字节的CAS操作,如果支持则通过 Free Lock 操作,如果不支持则会通过加锁处理。 

6.ThreadLocal

ThreadLocal可以使每一个线程都有自己的本地变量。
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使⽤ get() 和 set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题

ThreadLocal 内存泄露问题
ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。这样⼀来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null 的记录。使⽤完ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法。

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

闽ICP备14008679号