JavaSE面试题
1. 自增变量
给出最后 i、j、k 的值
public static void main(String[] args) {
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i=" + i);
System.out.println("j=" + j);
System.out.println("k=" + k);
}
结果: i = 4, j = 1, k = 11;
过程如下:
小结:
- 赋值操作
=
最后计算,将操作数栈赋值到局部变量表; =
右侧元素从左到右加载值依次压入操作数栈;- 操作数栈中的运算要依照运算符优先级;
- 自增、自减操作直接在局部变量表中改变变量的值,不经过操作数栈;
- 最后的赋值之前,临时结果也存储在操作数栈中。
2. 单例模式 Singleton Pattern
单例模式是Java中最简单的设计模式之一,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注:
- 单例类只能有一个实例:构造器私有化;
- 单例类必须自己创建自己的唯一实例:含有一个该类的静态变量来保存这个唯一的实例。
- 单例类必须给所有其他对象提供这一实例:对外提供获取该实例对象:(1)直接暴露;(2)用静态变量的get方法获取;
几种常见形式:
饿汉式 ,直接创建对象,不存在线程安全问题,具体分为:
- 直接实例化饿汉式(简洁直观)
- 枚举式(最简洁)
- 静态代码块饿汉式(适合复杂实例化)
懒汉式, 延迟创建对象,具体分为:
- 线程不安全(适用于单线程)
- 线程安全(适用于多线程)
- 静态内部类形式(适用于多线程)
直接实例化饿汉式(线程安全 )
/**
* 饿汉式
* 直接创建实例对象,不管是否需要这个对象
* (1)构造器私有化
* (2)自行创建,并且用静态变量保存
* (3)向外提供这个实例
* (4)强调这是一个单例,我们可以用final修改
*/
public class Singleton1 {
public static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {}
}
测试
public class TestSingleton1 {
public static void main(String[] args) {
Singleton1 s = Singleton1.INSTANCE;
System.out.println(s);
}
}
输出
枚举(线程安全)
/**
* 枚举类型
* 表示该类型的对象是有限的几个
* 我们可以限定为一个,就成了单例
*/
public enum Singleton2 {
INSTANCE
}
枚举在JAVA中和普通类一样,都能拥有字段与方法,而且枚举实例创建时线程安全的,在任何情况下它都是一个单例,我们可以直接以Singleton2.INSTANCE
方式调用。
静态代码块
静态代码块其实是第一种写法的变形,在静态代码块中进行单例对象的创建,代码块的建立为了可以读取配置文件中的内容,例在/src
目录下的single.properties
文件;
public class Singleton3staticblock {
String info;
public static final Singleton3staticblock INSTANCE;
static {
try {
Properties pro = new Properties();
pro.load(Singleton3staticblock.class.getClassLoader().getResourceAsStream("single.properties"));
INSTANCE = new Singleton3staticblock(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Singleton3staticblock(String info) {
this.info = info;
}
}
懒汉式(线程不安全)
多线程获取实例的话,就有可能同时通过if
的校验,错误的声明两个实例变量;
/**
* 饿汉式
* 延迟创建这个实例对象
* (1)构造器私有化
* (2)用一个静态变量保存这个唯一的实例
* (3)提供一个静态方法,获取这个实例对象
*/
public class Singleton4idler {
private static Singleton4idler instance;
private Singleton4idler() {
}
// 加锁即可实现线程安全
public synchronized static Singleton4idler getInstance() {
//多线程获取实例的话,就有可能同时通过if的校验,错误的声明两个实例变量;
if(instance == null) {
instance = new Singleton4idler();
}
return instance;
}
}
验证
public class TestSingleton4idler {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//单线程的访问是没有问题的
// Singleton4thehungry s1 = Singleton4thehungry.getInstance();
// Singleton4thehungry s2 = Singleton4thehungry.getInstance();
// System.out.println(s1);
// System.out.println(s2);
// System.out.println(s1 == s2);
//多线程访问
Callable<Singleton4idler> c = new Callable<Singleton4idler>() {
@Override
public Singleton4idler call() throws Exception {
return Singleton4idler.getInstance();
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton4idler> f1 = es.submit(c);
Future<Singleton4idler> f2 = es.submit(c);
Singleton4idler s3 = f1.get();
Singleton4idler s4 = f2.get();
System.out.println(s3);
System.out.println(s4);
System.out.println(s3 == s4);
es.shutdown();
}
}
结果
双重锁懒汉式(Double Check Lock)(线程安全)
public class Singleton5ThreadSafe {
// 一定要用 volatile修饰
public static volatile Singleton5ThreadSafe instance;
private Singleton5ThreadSafe() {
}
public static Singleton5ThreadSafe getInstance() {
//1.防止线程都进入到下面方法,阻塞在这里
if(instance == null) {
//2.加锁保证线程安全
synchronized (Singleton5ThreadSafe.class) {
//3.这里是应对多个线程通过了1的校验,阻塞在了这里,防止重复新建实例。
if (instance == null) {
// 不加volatile,可能会产生指令重排
// a.分配内存
// b.初始化对象
// c.指向刚分配的地址
// 若bc发生重排,执行了c后还没执行b,则没初始化对象就返回了对象。
instance = new Singleton5ThreadSafe();
}
}
}
return instance;
}
}
双重锁DCL模式的优点是,只有在对象需要被使用时才创建,且第一次if
判断instance==null
为了避免非必要加锁,当第一次加载时才对实例进行加锁实例化。但是,由于JVM存在乱序执行的功能,DCL也会出现线程不安全的情况,分析如下:
instance = new SingleTon();
这个步骤,在JVM里面的执行分为三步:
- 在堆内存开辟内存空间;
- 在堆内存中实例化SingleTon里面的各个参数;
- 把对象指向堆内存空间;
由于JVM存在乱序执行功能,所以可能步骤3执行在步骤2之前,如果线程A先执行完步骤3,此时instance
已经非空了,此时再切换到线程B上,线程B会直接将instance
拿来用,这样的就出现了著名的异常——DCL失效问题。
在JDK1.5之后,官方发现了这个问题,故而具体化了volatile
,即在JDK1.6及之后,只要定义为private volatile static SingleTon instance = null;
就可解决DCL失效问题。volatile
确保instance
每次均在主内存中读取,这样虽然会牺牲一点效率但无伤大雅。
静态内部类(线程安全)
/**
* 静态内部类
* 在内部类被加载和初始化时,才创建INSTANCE实例对象;
* 静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独加载和初始化的;
* 因为是在内部类加载和初始化时创建的(类加载器完成的),因此是线程安全的;
*/
public class Singleton6 {
private Singleton6 {
}
private static class Inner {
private static Singleton6 instance = new Singleton6();
}
public static Singleton6 getInstance() {
return Inner.instance;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不会做初始化,故而不占内存。 即当Singleton6
第一次被加载时,并不需要去加载内部类Inner
,只有当getInstance()
方法被第一次调用时,虚拟机才会加载Inner
类,才会初始化instance
。这种方法不但能保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
静态内部类的致命缺点,就是传参问题,静态内部类无法传参。
3.类初始化和实例初始化
类初始化
- 一个类要创建实例,需要先加载并初始化该类
main
方法所在类需要先加载和初始化 - 一个子类要初始化需要先初始化父类
- 一个类初始化就是执行类构造器
<clinit>()
方法
1)<clinit>()
方法由静态类变量显式赋值代码和静态代码块组成;(编译器会在.java
文件编译成.class
文件时,收集所有类初始化代码和static{}
域代码,收集在一起成为<clinit>
方法)
2) 类变量显式赋值代码和静态代码块按照书写从上到下顺序执行
3)一个类<clinit>()
方法只执行一次,JVM保证<clinit>
线程安全,且同一时间只有一个线程执行;
实例初始化
- 实例初始化就是执行
<init>()
方法
1)<init>()
方法可能重载有多个,有几个构造器就有几个<init>()
方法;
2)<init>()
方法由非静态实例变量显式赋值代码和非静态代码块、对应构造器代码块组成;
3)执行顺序:super()
-> 非静态实例变量显式赋值代码 与 非静态代码块 按照书写顺序执行 -> 对应构造器的代码最后执行;
4)每次创建实例对象,调用对应构造器,执行的就是对应的<init>()
方法
5)<init>()
方法的首行是super()
或super(实参列表)
,即对应父类的<init>()
方法; - 非静态方法前面其实有一个默认的对象
this
,this
在构造器即<init>()
表示的是正在创建的对象
总结类和实例是如何初始化和被构造的:
- 父类类初始化
<cinit>()
; - 子类类初始化
<cinit>()
; - 父类
<init>()
+ 父类构造器; - 子类
<init>()
+ 子类构造器;
4. 方法的参数传递机制
- 基本数据类型传入:传递数据值;
- 引用数据类型:传递地址值;
注:String
与包装类(Bye,Short,Integer,Long,Float,Double,Boolean,Character
)具有不可变性,即指向对象身不可改变,如需改变需要新建对象再重新指向。
//方法的参数传递机制
public class Code_04_ParameterPassing {
public static void main(String[] args) {
//基本数据类型
int i = 1;
//引用类型,对象具有不可变性
String str = "hello";
//包装类,对象具有不可变性
Integer num = 200;
//引用类型
int[] arr = {1, 2, 3, 4, 5};
//引用类型
MyData myData = new MyData();
change(i, str, num, arr, myData);
System.out.println("i = " + i); // 1
System.out.println("str = " + str); // hello
System.out.println("num = " + num); // 200
System.out.println("arr = " + Arrays.toString(arr)); // 2, 2, 3, 4, 5
System.out.println("my.a = " + myData.a); // 11
}
public static void change(int j, String s, Integer n, int[] a, MyData m) {
j += 1;
s += "world";
n += 1;
a[0] += 1;
m.a += 1;
}
}
class MyData {
int a = 10;
}
重点说一下包装类Integer
与String
类的计算:
Integer
与String
虽然是引用传递,但是由于其值的不可变性,对原引用进行新值赋值时,需要新建一个对象,之后将引用的地址改为新建的对象上;而在进行形参的传递过程中,我们新建了局部变量的引用,我们只是在修改局部变量引用的地址;而原引用还是指向原对象;在形参的传递中,只有对引用指向地址的对象本身内容进行改变,才能影响原变量的值;
6. 成员变量与局部变量
变量分类:
- 成员变量:类变量,实例变量(相当于类的属性,定义在类中,但在任何方法之外,当一个对象被实例化后,每个实例变量的值就跟着确定);
- 局部变量;
局部变量与成员变量的区别:
1.声明位置:
局部变量:函数体{}
,代码块{}
,形参;
成员变量:类的方法外,具体分为两类:
- 类变量:有
static
修饰; - 实例变量:无
static
修饰;
2.修饰符
- 局部变量:
final
; - 成员变量:
public,protected,private,private, final,static,volatile,transient
;
3.值存储位置
- 局部变量:栈
Stack
; - 实例变量:堆
Heap
; - 类变量:方法区;
4.作用域:
- 局部变量: 声明处开始,到所属的
}
结束; - 实例变量: 在当前类中,当前类
this.variable
,在其它类中类名Class.variable
或对象名object.variable
访问;(当前类中this
可以省略。但是当前类中有同名局部变量,则根据就近原则指代局部变量,实例变量则需要通过this.variable
访问) - 类变量: 在当前类中,当前类
Class.variable
(有时类名可以省略),在其它类中Class.variable
或object.variable
访问;
5.生命周期:
- 局部变量:每一个线程,每一次调用执行都是新的生命周期;
- 实例变量:随着对象的创建而初始化,随着对象回收而消亡,每一个对象的实例变量都是独立的;
- 类变量:随着类的初始化而初始化,随着类的卸载而消亡,该类的所有对象的类变量时共享的;
JVM内存
1.堆Heap:次内存区域的唯一目的就是存放对象实例,所有对象实例以及数组都要在堆上分配;
2.栈Stack:即虚拟机栈。用于存放局部变量表,局部变量表存放了各种基本数据类型(boolean,byte,char,short,int,float,long,double
), 对象引用(reference
类型,是对象在堆内存的首地址)。方法执行完,自动释放。
3.方法区(Method Area):用于存储已被虚拟机加载的类信息、方法信息、常量(常量池)、静态变量、即时编译器编译后的代码等文件;
/**
* 成员变量和局部变量
*/
public class Code_06_LocalAndMemberVariable {
public static int s;
int i;
int j;
{
int i = 1;
i++;
j++;
s++;
}
public void test(int j) {
j++;
i++;
s++;
}
public static void main(String[] args) {
Code_06_LocalAndMemberVariable obj1 = new Code_06_LocalAndMemberVariable();
Code_06_LocalAndMemberVariable obj2 = new Code_06_LocalAndMemberVariable();
obj1.test(10);
obj1.test(20);
obj2.test(30);
System.out.println(obj1.i + "," + obj1.j + "," + obj1.s); // 2 1 5 在static类中变量s可以不用通过类名也可以直接访问
System.out.println(obj2.i + "," + obj2.j + "," + obj2.s); // 1 1 5
}
}
0.Exam5
类加载时,在方法区中新建Exam.class
空间,空间中进行类构造器方法<clinit>()
:静态类变量显式赋值代码和静态代码块组成。a.static int s = 0
实例变量默认初始化为0。
1.栈中main
的区域,加入加入两个引用变量Exam5 obj1、obj2
,先是obj1
,它指向堆中新建立的实例变量int i, int j
,实例变量是有默认初始化的,即i = 0, j = 0
;
2.obj1
的<init>()
实例初始化:包括显式的成员变量赋值、非静态代码块、对应构造器。栈中开辟obj1.<init>()
空间:a.无显式的变量赋值;b.非静态代码块:空间新建局部变量int i = 1
,随后根据就近原则将局部变量i++
变为2,static int s++
在方法区中由0->1。obj1.<intit>()
执行完成从栈中移除空间。
之后是obj2.<init>()
实例初始化,同理操作…执行移除后,obj2
引用指向堆中的实例变量int i = 0, int j = 0->1
,方法区中的static int s = 1 ->2
。
3.在栈中开辟obj1.test(10)
的空间,空间新建参数变量int j
传入10。之后根据就近原则参数变量j++ = 10->11
。堆中obj.i++ = 0->1
,方法区中static int s = 2->3
。完成后移除obj1.test(10)
空间
obj1.test(20)
同理,执行并移除栈中空间后,堆中obj1.i++ = 1->2
,方法区中static int s = 3->4
。
obj2.test(30)
同理,执行并移除栈中空间后,堆中obj2.i++ = 0->1
,方法区中static int s = 4->5
。
第二季面试题
volatile
参考
volatile 关键字,你真的理解吗?
volatile可见性实现原理
深入浅出ConcurrentHashMap1.8+CAS+volatile
volatile
是JVM提供的轻量级同步机制, 具有特性:
- 保证可见性
- 不保证原子性
- 保证有序性, 禁止指令重排
1.可见性: 一个线程修改共享变量, 能够被其他线程同时感知。
Volatile用以声明 共享变量的值可能随时会被别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中的值更新会使缓存中的值失效(非volatile变量不具备这种特性,非volatile变量的值会被线程缓存,线程A修改了这个值后还没有写回主内存,其他线程读取到的值并不是线程A对变量更新过后的值)。
JMM(Java Memory Model) Java内存模型:
由于JVM运行的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域。而Java内存模型中规定所有变量都存储在主内存,主内存是共享的内存区域,所有线程都可以访问。但是线程对变量的操作(读取、赋值等)必须在私有的工作内存中进行,所以首先要在主内存中将变量拷贝到私有的工作内存中,对变量进行操作,操作完成后再写回主内存。线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
2.volatile
不保证原子性
原子性: 线程正在做某个业务时,中间不可被打断,业务整体要么同时完成,要么同时失败。
volatile为什么不能保证原子性?
在某一时刻线程1将i的值load取出来,放置到cpu缓存中,然后再将此值放置到寄存器A中,然后A中的值自增1(寄存器A中保存的是中间值,没有直接修改i,因此其他线程并不会获取到这个自增1的值)。如果在此时线程2也执行同样的操作,获取值i10,自增1变为11,然后马上刷入主内存。此时由于线程2修改了i的值,实时的线程1中的i10的值缓存失效,重新从主内存中读取,变为11。接下来线程1恢复。将自增过后的A寄存器值11赋值给cpu缓存i。这样就出现了线程安全问题。
例如count++
的操作不是原子性操作,多线程下进行count++
操作volatile
无法使其有原子性。
count++;//可以拆分成字节码操作下的
getstatic //读取静态变量(count)
iconst_1 //定义常量1
iadd //count增加1
putstatic //把count结果同步到主内存
3.禁止指令重排
内存屏障 Mermory Barrier:是一个CPU指令,保证特定操作的执行顺序,保证某些变量的内存可见性;
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障来禁止在内存屏障前后的指令与voltaile
指令执行重排序优化。内存屏障的另一个作用是强制刷出CPU的缓存数据,因此CPU上任何的线程都能够读取到这些数据的最新版本(可见性)。
volatile写操作
前插入StoreStore屏障,后插入StoreLoad屏障
volatile读操作
前插入LoadLoad屏障,后插入LoadStore屏障;
- 对于Load Barrier来说,在指令前插入LoadBarrier,可以让高速缓存中的数据失效,强制从主内存加载数据;
- 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见;
Java内存屏障通常所谓的四种为上述两种的组合:
- LoadLoad屏障:对于这样的语句
Load1;LoadLoad;Load2;
,在Load2及以后的读操作的数据被访问前,保证Load1要读取的数据读取完毕。 - StoreStore屏障:对于这样的语句
Store1;StoreStore;Store2;
,在Store2及后续写操作执行前,保证Store1的写操作对其他处理器可见; - LoadStore屏障:对于
Load1;LoadStore;Store2;
,在Store2及后续写入操作被刷出到主内存之前,保证Load1读取的数据读取完毕; - StoreLoad屏障:对于
Store1;StoreLoad;Load2;
,在Load2及后续读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四个屏障中最大的,在大多数处理器实现中,这个屏障是万能屏障,兼具其他三种内存屏障功能;
CAS
CAS是什么?
CAS的全称为Compare-And-Swap,他是一条CPU并发原语。功能是判断内存某个位置的值是否为预期值,如果是则更改为新值,这个过程是原子的。
Compare And Swap示例程序
public class CASDemo{
public static void main(string[] args){
AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data: "+atomicInteger.get());
}
}
输出结果:
true 2019
false 2019
AtomicInteger.compareAndSet(int expect, int update)
这里看一下AtomicInteger.compareAndSet(int expect, int update)
的源码:
// AtomicInteger.class
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
函数功能为在当前值value
与expect
相等的情况下,会将value
的值更新为update
,具体调用的函数为unsafe.compareAndSwapInt(this, valueOffset, expect, update)
。
什么是UnSafe
?
Unsafe
是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地方法(native
修饰的方法)来访问,Unsafe
相当于一个后门,基于该类可以直接操作特定内存的数据。
CAS并发原语体现在Java语言中就是sun.misc.Unsafe
类中的各个方法。调用UnSafe
类中的CAS方法,JVM会帮助实现CAS汇编指令,指令完全依赖硬件,通过它实现原子操作。原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致的问题。(原子性)
注:UnSafe
类中的所有方法都是native
修饰的,所以Unsafe
类中的方法都可以直接调用操作系统底层资源执行相应任务。
这里放一下AtomicInteger.class
的源码:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
...
//执行value++操作
public final int getAndIncrement() {
//this是当前对象, valueOffset是
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
AtomicInteger.value
是有volatile
修饰的,保证多线程之间的内存可见性;AtomicInteger
的静态属性volueOffset
,是value
值在内存中的偏移地址。我们可以将对象实例想象成一块内存,这个内存包含了一些属性,每个属性在内存中的偏移量Offset
不同,每个属性的偏移量在类加载或者之前就已经确定了(该类的不同实例的同一属性偏移量相等),所以sum.misc.Unsafe
可以通过一个对象实例和该属性的偏移量用原语获取该对象的属性值。
Unsafe.compareAndSwapInt(o, offset, expected, x)
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
Unsafe.compareAndSwapInt(o, offset, expected, x)
为Java的native
方法,并不由Java语言实现。
方法的作用为,读取传入对象o
在内存中偏移量为offset
位置的值与期望值expected
比较。若相等则将x
值赋给offset
位置,并且返回true
;若不相等,则取消赋值,方法返回false
;
Unsafe.getAndAddInt(object var1, long var2, int var4)
AtomicInteger
的自增函数getAndIncrement()
是通过Unsafe
的getAndAddInt(object var1, long var2, int var4)
函数实现的。
// Unsafe.class
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
找到的主内存中存储的值;
var = this.getIntVolatile(var1, var2)
获取的是主内存中的值;
当前线程调用compareAndSwapInt(var1, var2, var4, var5)
,可以获取到当前对象实例var1
配合偏移量var2
得到的属性值。并与主内存找到的var5
值比较,如果值相同则更新值主内存中的值为var5 + var4
,并且返回true
;如不同说明已经有线程修改了var1 + var2
处的值,继续通过getIntVolatile(var1, var2)
在主内存中取值var5
然后和当前线程的工作内存中的值(通过对象实例var1
和偏移量var2
)比较,直到两者equal(保证原子性)。
这里体现了JMM中的特性,线程通过getIntVolatile(var1, var2)
到主内存中获取值到私有的工作内存中(相当于拷贝副本),再通过compareAndSwapInt(var1, var2, var5, var5+var4)
比对之前获取到的副本和现在主内存中的值是否相等,若不相等(期间有线程修改了主内存中的值)则再去主内存中拷贝到自己的工作内存,直到相等将要修改的值写入主内存。
没有修改成功一直在while循环中尝试,这就是自旋锁的实现思想。相比于synchronized
,自旋锁在保障线程安全性的同时还保证了并发性,是线程不断尝试,而不是锁住一块代码不让其他线程进来。
参考:
CAS底层原理
从原子类和Unsafe来理解Java内存模型,AtomicInteger的incrementAndGet方法和Unsafe部分源码介绍,valueOffset偏移量的理解 - rhyme - 博客园
集合类不安全之并发修改一场
ArrayList
是线程不安全类,多线程操作时可能会报出java.util.ConcurrentModificationException
异常。
解决:
使用
1.Vector
2.Collections.synchronizedList(new ArrayList<>())
3.CopyOnWriteArrayList
Vector
对所有操作都加了synchronized
,Collections.synchronizedList
也几乎对所有操作都加了synchronized
。
CopyOnWriteArrayList
采用写时复制,CopyOnWriteArrayList
容器在添加元素时,不直接往原本的容器中添加,而是对原本的容器进行copy,复制出一个新的容器且容量+1,再向新容器中添加元素。添加完成后,将原容器引用指向新容器。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
...
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
...
}
CopyOnWriteArrayList
的get(index)
方法与普通集合并没有什么特殊的地方,但是数组array
用了volatile
声明,保证了读取的那一刻一定读取的是最新的数据对CopyOnWriteArrayList
容器可以进行并发读,无需加锁,因为对容器写入时并不是在
Java公平锁与非公平锁
- 公平锁:指多个线程按照申请锁的顺序来获取锁。
- 非公平锁:多个线程获取锁的顺序不保证按照申请锁的顺序。获取锁先尝试直接占有,若失败再采用类似公平锁的方式。在高并发情况下,有可能发生优先级反转或饥饿现象。优点在于吞吐量比公平锁大。
JUC包中ReentrantLock
的构造函数ReentrantLock(boolean fair)
可以设定公平锁或非公平锁,默认为非公平锁。
Synchronized
是一种非公平锁。
Java可重入锁
可重入锁:指同一线程在外层函数获取该锁之后,内层函数依然可以获取该锁,不会发生死锁。
ReentrantLock
、Synchronized
是可重入锁。可重入锁最大的作用是不会发生死锁,可以无限递归调用(加锁与释放锁的数量要匹配)。
Synchronized
可重入锁演示
public class reentrantLock {
public static void main(String[] args) {
Phone phone = new Phone();
// new Thread(() -> {
// phone.sendSMS();
// }, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
phone.sendSMS();
}
}, "t1").start();
new Thread(() -> {
phone.sendSMS();
}, "t2").start();
}
}
class Phone {
public synchronized void sendSMS() {
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
// 在同步方法中,调用另外一个同步方法
sendEmail();
}
public synchronized void sendEmail() {
System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
}
}
ReentrantLock
可重入锁演示
注:ReentrantLock
同时加几层锁都不影响运行,保证解锁数量与加锁数量对应即可。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class reentrantLock2 {
public static void main(String[] args) {
phone2 phone = new phone2();
Thread t1 = new Thread(phone, "t3");
Thread t2 = new Thread(phone, "t4");
t1.start();
t2.start();
}
}
class phone2 implements Runnable{
private Lock lock = new ReentrantLock();
@Override
public void run() {
getLock();
}
public void getLock() {
lock.lock();
// 同时加几层锁都不影响运行,保证解锁数量与加锁数量对应即可
// lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
}
}
public void setLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t set Lock");
} finally {
lock.unlock();
}
}
}
自旋锁 SpinLock
自旋锁 (SpinLock):指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环消耗CPU。
提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自旋锁验证程序:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo1 {
// 原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加自旋锁
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + " come in");
// 自旋锁,spinlock:通过cas比对
// 当修改成功时返回函数返回true,加!使其不进入循环
// 当修改失败时,函数返回false,加!进入循环等待
while(!atomicReference.compareAndSet(null, thread)) {
}
}
// 解自旋锁
public void myUnLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + " unlock");
}
public static void main(String[] args) {
SpinLockDemo1 spinLockDemo1 = new SpinLockDemo1();
new Thread(new Runnable() {
@Override
public void run() {
// use SpinClock
spinLockDemo1.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch(InterruptedException e) {
e.printStackTrace();
}
spinLockDemo1.myUnLock();
}
}, "Thread1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
spinLockDemo1.myLock();
spinLockDemo1.myUnLock();
}
}, "Thread2").start();
}
}
Thread1启动后满足条件没有进入到自旋锁中,进行atomicReference值的修改,Thread2不满足条件进入至自旋锁中不断循环。等待Thread1解锁恢复值,Thread2跳出循环,之后进行值得修改,后恢复值。
synchronized和Lock区别
不同:
1.synchronized
是Java关键字,属于JVM层面。底层通过monitor
对象来完成,wait/notify
等方法也都依赖于monitor
对象,只能在同步块或者方法中才能调用wait/notify
等方法。
Lock是具体类(java.util.concurrent.locks.Lock
),是api层面的锁。
2.使用方法:
a.synchronized
不需要用户手动释放锁,当synchronized
代码执行后,jvm会自动让线程释放对锁的占用。
b.ReentrantLock
需要用户手动释放锁,若没有主动释放锁,就可能出现死锁现象。需要lock(), unlock()
配合try catch finally
语句来完成,在finally
中必须释放锁。
3.等待是否中断
a.synchronized
不可中断,除非抛出异常或者正常运行完成。
b.ReentrantLock
可中断,lockInterruptible()
放在代码块中,调用interrupt()
可以中断。
可设置超时方法lock(long timeout, TimeUnit unit)
4.加锁是否公平;
a.synchronized
是非公平锁;
b.ReentrantLock
默认非公平锁,构造函数可以传递boolean fair
值,设定true
为公平锁,false
为非公平锁。
5.绑定多个条件Condition
a.synchronized
不可,要么随即唤醒线程,要么全部唤醒线程;
b.ReentrantLock
可以实现精准唤醒。
synchronized锁的作用范围
1.作用于成员变量和非静态方法时,锁住的是对象实例;
2.作用于静态方法时,锁住的是Class
实例,作用于此类的所有对象实例;
3.作用于代码块时,锁住的是代码块配置的对象;
Java基础
一个Java文件可以有多个类,但是public类只能有一个且要与.java文件同名
面向对象
子类对象实例化的全过程
- 从结果上来看(继承性):
子类继承父类以后,就获取了父类中声明的属性或方法。
创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。 - 从过程上来看:
当我们通过子类的构造器创建子类对象时,我们一定会直接或间接地调用其父类的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object
类中的空参构造器为止。正因为加载过所有父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。
明确:虽然创建子类对象时,调用了父类的构造器,但是自始自种就创建过一个对象,即为new的子类对象。
多态
- 对象的多态性:父类的引用指向子类对象(或子类的对象赋给父类的引用)
- 多态的使用,虚拟方法调用:当对象调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法,且对象只能调用父类拥有的方法,无法调用子类扩展的方法。(编译期能调用的方法看左边类,运行期实际执行的方法看右边类)
- 多态性的使用前提:类的继承关系、方法的重写;
- 对象的多态性,只适用于方法,不适用于属性;
虚拟方法:子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给他的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
Java构造器
构造器注意事项
- 构造器的名称必须和类名相同;
- 一个类中可以有多个构造器,构造器的参数必须不同(重载Overload);
- 如果没有手动定义构造器,编译器会提供一个默认的构造器给我们使用,一旦我们定义了构造器,编译器会把默认的构造器收回;
- 子类构造器一定要调用父类构造器,若父类有无参构造器,子类则会默认隐式调用
super()
;若父类有有参构造器,子类就不会再默认调用父类的无参构造器super()
, 需要显式的调用父类的有参构造器; - 构造器不能继承,只能进行调用;
构造器的定义和使用
class Father {
public Father() {
}
public Father(int i) {
...
}
}
public class Son extends Father {
public Demo(int i) {
//子类构造器只要调用了父类的构造器即可(隐式调用父类无参构造器)
//super()隐式调用
}
}
构造器的访问权限可以是以上四种权限中的任意一种:
1、采用 private:一般是不允许直接构造这个类的对象,再结合工厂方法(static方法),实现单例模式。注意:所有子类都不能继承它。
2、采用包访问控制:比较少用,这个类的对象只能在本包中使用,但是如果这个类有static 成员,那么这个类还是可以在外包使用;(也许可以用于该类的外包单例模式)。
注意:外包的类不能继承这个类;
3、采用 protected :就是为了能让所有子类继承这个类,但是外包的非子类不能访问这个类;
4、采用 public :对于内外包的所有类都是可访问的;
注意: 构造方法有点特殊,因为子类的构造器初始化时,都要调用父类的构造器,所以一旦父类构造器不能被访问,那么子类的构造器调用失败,意味子类继承父类失败!