一.Java内存模型
学习Java内存模型的思路:
1.Java 内存模型是什么
2.解决什么问题及具体是怎么解决的
3.内存划分
4.垃圾回收机制
5.内存泄露分析
背景:
CPU 和缓存一致性(可见性):CPU 和主存之间增加缓存,在多线程场景下会存在缓存一致性问题,原因是多核多线程
处理器优化(原子性):处理器可能会对输入代码进行乱序执行处理
指令重排(有序性):编译器指令重排
这三个问题会导致并发变成中线程对数据操作的不一致性问题。
怎么解决:
java内存模型就是解决以上问题提供的一种思路或解决方案
1.Java 内存模型是什么
Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范
一句话总结:内存模型就是合理操作内存的一种规范,目的是保证并发编程场景中的原子性、可见性和有序性。
具体规范:
1.1: 内存分为:主内存+线程内存(工作内存),所有变量都存储在主内存,每个线程都有自己的工作内存
1.2: 线程内存中保存了该线程操作的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存。
1.3: 不同线程之间不能访问对方内存中的变量.线程间变量的传递需要自己的工作内存和主内存之间进行数据同步
具体实现:
1.1: 关键字:Volatile、Synchronized、Final、Concurren等
其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字,解决并发编程原子性、有序性和一致性的问题
2.为什么要有 Java 内存模型,Java 内存模型解决了什么问题等
原子性:为了保证原子性,提供了两个高级的字节码指令 Monitorenter 和 Monitorexit,对应Synchronized
可见性:被其修饰的变量在被修改后可以立即同步到主内存。被其修饰的变量在每次使用之前都从主内存刷新,对应volatile(除了 Volatile,Java 中的 Synchronized 和 Final 两个关键字也可以实现可见性)
有序性:可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性,实现方式有所区别:Volatile 关键字会禁止指令重排。Synchronized 关键字保证同一时刻只允许一条线程操作。
3.内存划分
虚拟机栈,堆,方法区,程序计数器,本地方法栈
线程私有:虚拟机栈,本地方法栈,程序计数器 这三块是不进行垃圾回收的,他们的生命周期是同线程同步的
线程共享:堆,方法区 这两块内存是需要GC的
4.垃圾回收机制
虚拟机把堆内存划分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3个区域
为什么要分代:提高GC效率
GC算法:
1.引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1
缺点:无法判断对象之间相互循环引用
2.可达性分析算法
2.1:什么是GC Roots
Java虚拟机中所有引用的根对象,具体那些对象可以作为GC ROOT,如下:
3.从GC Roots垃圾回收机制分析内存泄露问题
垃圾回收器不会回收GC Roots以及那些被它们间接引用的对象,这是造成内存泄露的根本原因。
分析内存泄露时从该点出发,找到引用链上的那些对象是否存在一直被GC ROOTS持有。消除这些一直被持有的对象,做到随用随创建,用完就回收的原则。
GC ROOTS参考:
https://yq.aliyun.com/articles/91017?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ-qun&2017531&utm_content=m_22117
内存泄露分析参考:
https://cloud.tencent.com/developer/article/1334692
二.volatile与synchronized区别
volatile
当一个变量定义为volatile后,它将具备两种特性:1. 可见性,2. 禁止指令重排序。
即:线程A对volatile变量a做了修改,其它线程是能拿到该变量a的最新值。避免了线程安全问题。
我们在写线程安全的单例模式时,使用volatile修饰单例变量:
volatile作用:1.volatile有序性保证了指令不被重排
2.volatile可见性,保证了行程a对单例变量的修改对其它进程是安全可见的
public class SafeDoubleCheckedLocking {
//volatile作用:1.volatile有序性保证了指令不被重排
2.volatile可见性,保证了行程a对单例变量的修改对其它进程是安全可见的
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {//第一次判空是为了缩小锁的范围,提高效率,竞争锁是非常重的操作
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)//第一次判空是为确保只初始化一次
instance = new Instance(); // instance为volatile }
}
return instance;
}
}
}
synchronized
synchronized同步体现在:一次只允许一个线程执行代码块。synchronized使整个线程内存与“主”内存同步。
int i3;
synchronized int geti3() {return i3;}
执行geti3()会执行以下操作:
1、线程从当前对象获取锁。
2、线程内存刷新所有的变量,也就是说,它的所有变量都有效地从“主”内存读取。
3、执行代码块。在本例中,这意味着将返回值设置为i3的当前值,i3可能刚刚从“主”内存中重置。
4、对变量的任何更改通常会被写到“主”内存中,但是对于geti3(),我们没有更改。
5、线程释放这个对象在监视器上的锁。
参考文章:https://www.cnblogs.com/tf-Y/p/5266710.html
http://developer.51cto.com/art/201807/579744.htm
https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
注意:volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值时能够获得最新的值,而不能保证线程修改的安全性。
三.线程安全
1.什么是线程安全问题
网上有各种描述,每个人的理解略有差异。
自己理解的线程安全是:线程安全问题出现的前提是多线程。经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果
2.为什么会有线程安全问题
线程安全问题产生的根本原因是:共享数据存在被并发修改的可能,即一个线程读取时,允许另一个线程修改。
从java的内存模型角度分析:java的内存模型:每个线程都有自己的私有内存空间(线程内存),进程有共享内存(主内存),共享数据是存储在主内存,同时共享数据也会在每个操作它的线程中有一份拷贝。线程修改共享数据时会从主内存中获取一份拷贝到自己的私有内存空间,操作完后会同步到主内存。这样会导致,线程ThreadA修改完共享数据后还没有完全同步到主内存,导致其它线程读取共享数据的时候,没有读取到最新修改值,出现了线程安全问题。不安全体现在:线程读取到的共享数据有可能不是最新的。该内存模型会导致天生就导致了线程安全问题。
线程安全产生的条件:多线程 修改 同一个对象
注意:多线程读取一个对象时不会发生线程安全问题。因为该对象的值是不变的。一定是被修改了,对其它线程的访问造成了影响。
3 Java中怎么解决线程安全问题
解决线程安全问题时,最直接的是找出产生线程安全问题的原因和条件,然后消除掉这些条件。解题思路:
1.“废除”共享变量
因为多线程对共享变量同时操作,导致出现了访问不一致的问题,所以可以考虑将全局共享变量改成局部变量或者变成线程私有变量
如:ThreadLocal,ThreadLocal存储的是每个线程私有的数据,各个线程间独立操作,互不影响,这种处理方式是以牺牲空间换取访问安全的一种方式。这种方式虽然解决了线程安全问题,但是也阻断了线程间的通信。即:两个线程各玩各的。
1.什么是ThreadLocal
/**
* Implements a thread-local storage, that is, a variable for which each thread
* has its own value. All threads share the same {@code ThreadLocal} object,
* but each sees a different value when accessing it, and changes made by one
* thread do not affect the other threads. The implementation supports
* {@code null} values.
*
* @see java.lang.Thread
* @author Bob Lee
*/
官方定义:1.每个线程都一份自己的私有的变量拷贝,对该变量的修改只对该线程可见
2.所有线程共享一个ThreadLocal 对象,该对象是全局的
2.ThreadLocal有什么特点
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
*
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
*
* Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
3.Android中使用ThreadLocal的场景学习分析
消息分发机制中,每个线程都有自己唯一的Looper对象。而这个Looper对象就是线程独有,正符合ThreadLocal使用特点。
线程,ThreadLocal对象,ThreadLocalMap之间的关系:
一个线程有唯一ThreadLocalMap对象,线程是通过ThreadLocal.get()获取ThreadLocalMap。
ThreadLocalMap存储的元素是:key:ThreadLocal对象为key 线程变量为value.
总结:一个线程仅有一个ThreadLocalMap对象,可以有很多个threadlocal对象,这也是ThreadLocal设计的初衷,就是维护线程私有数据的。
参考:https://www.jianshu.com/p/98b68c97df9b
2.同步机制
如果共享变量无法转化成局部变量或线程私有变量,那可以通过同步的机制强制线程对共享变量"独占访问"