Volatile是Java虚拟机提供的轻量级的同步机制
JMM本身是一种抽象的概念!并不真实存在!它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式
由于JVM运行程序的实体是线程,而每个线程创建时,JVM都会为其创建一个栈空间。
且栈空间是每个线程的私有数据区域。
而 JMM 规定所有变量都存储在 主内存,主内存是共享区域,所有线程都可以访问。
但是线程对变量的操作必须在工作区域(栈内存)进行,首先要将变量从主内存拷贝到自己的工作内存空间中,然后对变量进行操作,操作完成后再将变量写回主内存
以上就是可见性!!!!!!!!
(一个线程AAA修改了共享变量X的值,但是还未写回主内存时,此时另外一个线程BBB又对主内存中同一个共享变量X进程操作,但此时A线程工作内存中共享变量X对编程B来说并不可见)
原子性:不可分割,完整性!某个线程正在做某个具体业务时,中间不可以被加塞或者被分割!即要么同时成功,要么同时失败!
Volatile不保证原子性解决:
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排!
源代码 ---》 编译器优化的重排 ---》 指令并行的重排 ---》内存系统的重排 ---》最终执行的指令
处理器在进行重排序时必须要考虑指令之间的 数据依赖性
由于重排的存在,两个线程中使用的变量能否保证一致性是无法确定的!
CompareAndSet是一条CPU并发原语!
它的功能是:判断内存某个位置是否为预期值,如果是则更改为新值,这个过程是原子的
CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题!(连续的,执行过程不会被打断!)
Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地方法来访问
Unsafe相当于一个“后门”,基于该类可以直接操作特定内存的数据
注意Unsafe类的所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
var1:AtomicInteger对象本身
var2:该对象值的引用地址
var4:需要变动的数量
var5:通过var1 var2 找出的主内存中真实的值
用该对象当前的值与var5比较,若相同,更新var5+var4并且返回true,若不同,继续取值然后再比较,直到更新完成
即 CAS就是比较当前工作内存中的值和主内存的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止!(!!!自旋)
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当前仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做!
CAS的缺点:
eg:一个线程One从内存位置V中取出A,这时候另一个线程two也从内存中取出A,
? 且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,
? 这时候线程One进行CAS操作发现内存中仍然是A,然后线程one操作成功。
即!尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的!
package cn.imut.two;
import lombok.*;
import java.util.concurrent.atomic.AtomicReference;
@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
String userName;
int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zl = new User("zl", 22);
User leiz = new User("leiz", 21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(zl);
System.out.println(atomicReference.compareAndSet(zl, leiz) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(zl, leiz) + "\t" + atomicReference.get().toString());
}
}
举一个ArrayList不安全的例子:
package cn.imut.two;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class ArrayListDemo {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
//add 没有加锁
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
java.util.ConcurrentModificationException
解决方案:
new Vector<>();
Collections.synchronizedList(new ArrayList<>());
若不允许使用上述工具类
List<String> list = new CopyOnWriteArrayList<>();
写时复制:
CopyOnWrite容器即写时复制的容器。往一个容器添加元素时,不直接往当前容器Object[]添加
而是先将当前容器object[]进行拷贝,复制出一个新的容器object[] newElements,然后新的容器
object[] newElements添加元素,添加完后再将原容器的引用指向新容器。
这样写的好处是,可以对CopyOnWrite进行并发的读,而不需要加锁,因为不会添加任何元素
即,CopyOnWrite容器是一种读写分离的思想!
公平锁:指多个线程按照申请锁的顺序来获取锁,类似排队
非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程
? 优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象!
并发包中ReentranrLock的创建可以指定构造函数的boolean类型来得到公平锁/非公平锁,默认是非公平锁
非公平锁优点在于:吞吐量大于公平锁
对于Synchronized而言,也是一种非公平锁!
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁时,在进入内层方法会自动获取锁
即!线程可以进入任何一个它已经拥有的锁所同步着的代码块!
典型锁:ReentrantLock/Synchronized就是一个典型的可重入锁!
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处就是减少线程上下午切换的消耗,缺点是循环会消耗CPU
阻塞队列,它首先是一个队列
Thread1 ------------ BlockingQueue ----------- Thread2
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
在多线程领域:所谓阻塞,在某些情况下会挂起线程(阻塞,一旦条件满足,被挂起的线程又会自动被唤醒)
而BlockingQueue可以使我们不关心什么时候需要阻塞线程,什么时候需要唤醒线程
原始构成:
synchronized是关键字,属于JVM层面
Lock是具体类,是api层面的锁
使用方法:
synchronized 不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放锁占用
Lock则需要用户去手动释放锁,可能会产生死锁
需要Lock()与unlock()方法配合try/finally语句来完成
等待是否可中断:
synchronized不可中断
lock可中断
加锁是否公平:
synchronized非公平
Lock两者都可以
锁绑定多个条件:
synchronized没有
lock可以精确唤醒
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
其主要特点为:线程复用,控制最大并发数,管理线程
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
ExecutorService threadPool = Executors.newFixedThreadPool(5); //一池5个处理线程
ExecutorService executorService = Executors.newSingleThreadExecutor(); //一池1个处理线程
ExecutorService executorService1 = Executors.newCachedThreadPool(); //一池N个处理线程
在创建了线程池后,等待提交过来的任务请求
当调用execute()方法添加一个请求任务时,线程池会做如下判断:
? 若正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
? 若正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
? 若此时队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
? 若队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
当一个线程完成任务时,他会从队列中去下一个任务来执行
当一个线程无事可做超过一定的时间时,线程会判断:
? 若当前运行的线程数大于corePoolSize,那么这个线程就被停掉
? 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
等待队列已经排满(塞不下新任务),线程池的max线程也达到了(无法继续为新服务服务),此时需要拒绝策略机制处理问题!
JDK内置拒绝策略
以上内置拒绝策略均实现了 RejectedExecutionHandler接口
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式
什么是垃圾?
简单的说就是内存中已经不再被使用到的空间就是垃圾
如何判断可以被回收?
引用计数法(不用)
Java中引用和对象是有关联的,若要操作对象必须用引用进行。
引用计数法即:给对象添加一个引用计数器,每当有一个地方引用它,计数器值+1
? 每当有一个引用失效时,计数器值-1
任何时刻,计数器值为0的对象就是不可能在被使用的,那么这个对象就是可回收对象!
!!!但是它很难解决循环引用问题!
可达性分析(根搜索路径)
所谓“GC roots”或者说“tracing GC”的“根集合” 就是一组必须活跃的引用
基本思路就是通过一系列名为“GC Roots”的对象作为起始点!
从 “GC Roots” 对象开始向下搜索,若一个对象到 GC roots没有任何引用链相连,说明此对象不可用!
(即通过引用关系遍历对象图,能被遍历到的对象就被判定为存活,没有被遍历到的就被判定为死亡)
java中可以作为 GC Roots的对象
-Xms与-Xmx:
-Xms:初始大小内存,默认为物理内存1/64
-Xmx:最大分配内存,默认为物理内存1/4
-Xss:设置单个线程栈大小,一般为512k~1024k
-Xmm:设置年轻代大小
-XX:MetaspaceSize:设置元空间大小
元空间本质与永久代类似,都是对JVM规范中方法区的实现
不过元空间与永久代之间最大的区别在于:
元空间并不在虚拟机中,而是使用本地内存
即,正常情况下,元空间的大小仅受本地内存限制
强引用
强引用对象,出现OOM也不会对该对象进行回收!死也不回收!
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能证明对象还“活着”,垃圾收集器不会碰这种对象!
同时,强引用也是造成 Java内存 泄漏的主要原因之一
软引用
软引用用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集
对于只有软引用的对象来说:
? 当系统内存充足时不会被回收
? 当系统内存不足时就会被回收
软引用常用在 高速缓存中!(内存够用就保留,不够用就回收!)
弱引用
弱引用只要垃圾回收机制一运行,不管 JVM的内存空间是否足够,都会回收该对象占用的内存
package cn.imut.two;
import java.util.HashMap;
import java.util.Map;
public class WeakHashMapDemo {
public static void main(String[] args) {
myHashMap();
}
private static void myHashMap() {
Map<Integer, String> map = new HashMap<>();
Integer key = 1;
String value = "HashMap";
map.put(key,value);
System.out.println(map);
System.out.println("=========================");
key = null;
System.out.println(map);
System.out.println("=========================");
System.gc();
System.out.println(map);
}
}
虚引用
若一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列联合使用!
为单线程环境涉及且只使用一个线程进行垃圾回收,会暂停所有的用户线程!
不适用于服务器环境!
多个垃圾收集线程并工作,此时用户线程是暂停的,适用于弱交互场景
用户线程和垃圾收集线程同时执行(并行或者交替执行),互联网公司常用!
不需要停止用户线程
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
新生代
串行GC(Serial/Serial Copying)
稳定!单线程使用!JVM默认运行在Client模式下默认的新生代垃圾回收器
并行GC(ParNew)
Serial的并行版本,配合老年代的CMS GC工作,运行在Server模式下新生代默认垃圾收集器
并行回收GC(Parallel/Parallel Scavenge)
新生代垃圾收集器,使用复制算法,串行收集器在新生代和老年代的并行化
老年代
G1收集器是一种服务器端垃圾收集器,它像GMS收集器一样,能与应用线程并发执行
它整理空闲空间更快,且需要更多的时间来预测GC停顿时间,不希望牺牲大量的吞吐性能
GQ收集器设计的目标是取代CMS收集器,优势是不会产生很多内存碎片,且更可控
G1整体上采用标记-整理算法、局部通过复制算法,不会产生内存碎片
宏观上G1不在区分年轻代和老年代,把内存划分成多个独立的子区域,类似一个棋盘
但是小范围还是会区分年轻代和老年代
原文:https://www.cnblogs.com/yfyyy/p/13704390.html