java笔记JVM(java虚拟机)之线程安全和锁优化

Posted 棉花糖灬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java笔记JVM(java虚拟机)之线程安全和锁优化相关的知识,希望对你有一定的参考价值。

1. 线程安全与锁优化

(1) 线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的

(2) Java语言中的线程安全

可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

  • 不可变:不可变(Immutable)的对象一定是线程安全的
  • 绝对线程安全:绝对的线程安全能够完全满足Brian Goetz给出的线程安全的定义
  • 相对线程安全:相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的
  • 线程兼容:线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用
  • 线程对立:线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码

(3) 线程安全的实现方法

  • 互斥同步(阻塞同步):同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量Mutex)和信号量(Semaphore)都是常见的互斥实现方式。互斥是方法,同步是目的。,最基本的互斥同步手段就是synchronized关键字。会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止
  • 非阻塞同步:互斥同步属于一种悲观的并发策略,其总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享的数据是否真的会出现竞争,它都会进行加锁。乐观并发策略:不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止
  • 无同步方案:有一些代码天生就是线程安全的。可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行

(4) 比较并交换(Compare-and-Swap,下文称CAS)

CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。上述的处理过程是一个原子操作

(5) 锁优化

如适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等

  • 自旋锁与自适应自旋:为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的
  • 锁消除:锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除
  • 锁粗化:如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
  • 轻量级锁:
  • 偏向锁:它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。


(6) TreadLocal原理

ThreadLocal本质是变量 ,它为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 每个线程都有一个map,类似于context,用于存储每一个线程的变量副本。既然是map,key就是ThreadLocal名,value就是变量副本。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题, 同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量,这时该变量是多个线程共享的, 程序设计和编写难度相对较大 。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。

线程里面会有一个context,即上下文,可以往context里面存放东西,随后在线程管辖范围内都可以获取到。ThreadLocal在set存数据到线程context的时候,把自己(this)也放进去

内存泄漏就是东西放在内存里面,但你忘记它放哪里了,它占着一块内存,但是不能回收。 ThreadLocal作为key,存入ThreadLocalMap里面,但是因为key被包装成弱引用,很容易导致内存泄漏 。

//声明一个ThreadLocal变量b,此时开辟了一块内存,里面存放的b对象
ThreadLocal<Integer> b = new ThreadLocal<Integer>();

//注意,这里不是赋值,赋值是=操作符
b.set(10)

(1)10不是放在了b里面,10和b是两个独立存放的东西,不是包含关系。

(2)10和b是两个独立存放的变量,如果其中的一个被清理,那么另外一个不受影响的。

10和b两个的独立存放的东西,只不过我们不能直接访问到10,必须通过b来传话,原因很简单,在map里面,value要通过key来访问。

不过,此时b被包装成了弱引用,也就是说它被打了一个标签,这样它很容易被gc。一旦b被清理了,10就找不到了,从而造成了内存泄漏。

(1)有两个变量a和b,如果后面不再参与计算,则会被自动回收;

(2)有两个变量a和b,存放在map里面,a是key,b是value。如果map一直存在,a和b因为被map关联,则a和b就一直不能被回收;

(3)有两个变量a和b,存放在map里面,a是key(但是a被弱引用包装了一下),b是value。如果map一直存在,a和b因为被map关联,则b就一直不能被回收,但是a可以被回收。一旦a回收了,那么无法通过a找到b了,这就是b出现内存泄漏。

以上是关于java笔记JVM(java虚拟机)之线程安全和锁优化的主要内容,如果未能解决你的问题,请参考以下文章

八JVM视角浅理解并发和锁

Java虚拟机—线程安全和锁优化

java笔记JVM(java虚拟机)之内存模型和线程

深入理解JVM虚拟机读书笔记——锁优化

深入理解JVM虚拟机读书笔记——锁优化

java笔记JVM(java虚拟机)之垃圾收集与内存分配策略