synchronized引出:解决内存不可见问题(可见:一个线程对共享变量进行修改后,其他线程可以立刻看到)。可以通过synchronized解决,其实进入 synchronized 块就是把在 synchronized 块内使用到的变量从线程的本地内存中擦除,这样在 synchronized 块中再次使用到该变量就不能从本地内存中获取了,需要从主内存中获取,解决了内存不可见问题。
volatile和synchronized:synchronized 关键字可以保证并发编程的三大特性:原子性、可见性、有序性,而 volatile 关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的 synchronized。
synchronized有四种状态:
四种状态引出:为了减少获得锁和释放锁带来的性能消耗而引入的
(1)无锁:无锁的特点是修改操作在循环内进行,线程会不断的尝试修改共享资源。若没有冲突就修改成功并退出,否则就会继续循环尝试。也就是CAS(CAS是基于无锁机制实现的 - CompareAndSwap ,也就是先比较再进行修改,修改失败,自旋)。
(2)偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。
(3)轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。但是一直自旋会导致cpu消耗大。
(4)重量级锁:当多个线程在等待线程时候,此时该锁会升级为重量级锁,会有线程阻塞,会带来巨大的性能消耗。
装箱:就是自动将基本数据类型转换为包装器类型(int–>Integer);调用方法:Integer的
valueOf(int) 方法。Integer i = 10; 即自动装箱。
拆箱:就是自动将包装器类型转换为基本数据类型(Integer–>int)。调用方法:Integer的
intValue方法。
注意:如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象
int和Integer区别:
int a=1,Integer b=1; a==b是true还是false
Integer与int比较时,Ingeger都会自动拆箱(jdk1.5以上)。故为true,两个Integer对象进行比较时候,如果数值在[-128,127]之间,且数值相同就返回true,两个是一个Integer对象,否则为false。
String特性:
String str=new String(“abc”); 这行代码创建了几个String对象? 答案:一个或两个。分析:
也可以想到 String str = "abc"创建了0个对象或者1个对象。**
String s = “abc” 和 String s = new String(“abc”)区别?
Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示。
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
u2是u2是无符号的16位整数,因此理论上允许的的最大长度是2^16=65536。而 java class 文件是使用一种变体UTF-8格式来存放字符的,null 值使用两个 字节来表示,因此只剩下 65536- 2 = 65534个字节。
Object 类提供的 clone 是只能实现 浅拷贝的。怎么实现深拷贝?
让每个引用类型属性内部都重写clone() 方法 (待完善)
利用序列化
//深度拷贝
public Object deepClone() throws Exception{
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
假如货架是在深圳,那 JVM 的平台无关性就相当于是客人可以在各个地方购买你在淘宝上发布的商品,不是只有在深圳才能购买货架上的商品。
Java内存模型
Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程有自己的工作内存(Working Memory),线程的工作内存中保存了线程使用到的变量的内存副本。
线程对变量副本的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量。
不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递都要通过主内存来完成。
主内存与工作内存的交互操作
Java 内存模型中定义了 8 种操作来完成主内存与工作内存之间具体的交互协议,虚拟机实现时必须保证每一种操作都是原子、不可再分的。
这 8 种操作又可分为作用于主内存的和作用于工作内存的操作。
作用于主内存的操作
lock(锁定)
作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁)
作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定。
read(读取)
作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便 load 时使用。
write(写入)
作用于主内存的变量,它把 store 操作从工作内存中得到的变量值放入主内存的变量中。
作用于工作内存的操作
load(载入)
作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用 )
作用于工作内存的变量,它把一个工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码执行时会执行这个操作。
assign(赋值)
作用于工作内存的变量,它把一个执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码执行时执行这个操作。
store(存储)
作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的 write 操作使用。
JVM 是怎么划分内存的?
JVM 在执行 Java 程序的过程中会把它管理的内存分为若干个数据区域,而这些区域又可以分为线程私有的数据区域和线程共享的数据区域。
线程私有数据区域:
(1)程序计数器
为了线程切换后能恢复到正确的执行位置,每条线程都有一个私有的程序计数器。
程序计数器在 Java 虚拟机规范中没有规定任何 OOM 情况的区域。
(2)虚拟机栈
虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame),栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。
在 Java 虚拟机规范中,对虚拟机栈规定了下面两种异常。
StackOverflowError
当执行 Java 方法时会进行压栈的操作,在栈中会保存局部变量、操作数栈和方法出口等信息。
JVM 规定了栈的最大深度,如果线程请求执行方法时栈的深度大于规定的深度,就会抛出栈溢出异常 StackOverflowError。
OutOfMemoryError
如果虚拟机在扩展时无法申请到足够的内存,就会抛出内存溢出异常 OutOfMemoryError。
(3)本地方法栈
本地方法栈(Native Method Stack)的作用与虚拟机栈非常相似,它有下面两个特点。
本地方法栈与虚拟机栈的区别是虚拟机栈为 Java 方法服务,而本地方法栈为 Native 方法服务。
与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
线程共享的数据区域:
(1) Java 堆
Java 堆(Java Heap)也就是实例堆,它用于存放我们创建的对象实例。特点以下:
最大:对于大多数应用来说,Java 堆是 JVM 管理的内存中最大的一块内存区域。
线程共享:Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。
存放实例:堆的唯一作用就是存放对象实例,几乎所有的对象实例都是在这里分配内存。
GC:堆是垃圾收集器管理的主要区域,所以有时也叫 GC 堆。
(2)方法区
方法区(Method Area)存储的是已被虚拟机加载的数据,它有下面几个特点:
线程共享:方法区和堆一样,是所有线程共享的内存区域。
存储的数据类型:类信息、常量、静态变量
异常:方法区又可分为运行时常量池和直接内存两部分。
运行时常量池:常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
直接内存:直接内存不是从虚拟机运行时数据区的一部分。在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道与缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作,这样能避免在 Java 堆和 Native 堆中来回复制数据。
作用:
volatile是Java虚拟机提供的最轻量级的同步机制。
volatile怎么保证的可见性?
当volatile执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存中
并且写操作会让其他线程中的变量缓存无效化
这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量就是最新的值,实现了线程的可见性。
独占锁:JDK中的synchronized和java.util.concurrent(JUC)包中Lock的实现类就是独占锁。
共享锁:共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。
互斥锁:是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。
读写锁:读写锁相比于互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。
自旋锁:自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。在 Java 中,AtomicInteger 类有自旋的操作。
分段锁:分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。
锁升级
底层:创建Set集合底层其实创建了一个Map集合。
HashSet的底层是HashMap,添加元素时候,就把该值当作key,value为Object对象,实现了单列数据存储。
treeSet的底层其实就是一个TreeMap
linkedHashSet底层LinkedHashMap
遍历:for循环、iterator。
方案一:和list一样,使用Colletcions这个工具类syn方法类创建个线程安全的set.
Set synSet = Collections.synchronizedSet(new HashSet<>());
方案二:使用JUC包里面的CopyOnWriteArraySet
Set copySet = new CopyOnWriteArraySet<>();
区别: