目录
基础部分
Java语言有什么特点
JDK与JRE的区别
Java的基本数据类型
装箱和拆箱过程
Java访问修饰符
构造方法,成员变量和静态成员变量的初始化顺序
面向对象的三大特征
重载和重写的区别
接口类和抽象类的相同点和不同点
内部类及其作用
static关键字的作用
为什么将String设计为不可变
String/StringBuffer/StringBulider之间的关系
==与equals的区别
Object中常用的方法
Java中的异常
finally代码块是否一定执行
简述final,finally和finalize的区别
简述泛型
Java的反射机制
Java中的List
Java中的Set
Java中的HashMap
Java中的TreeMap
ArrayList,Vactor,linkedList的相同点和不同点
HashMap和HashTable的区别
HashMap和TreeMap的选择
hashCode和equals的关系
Collection和Collections的区别
并发编程
JMM模型
什么是原子性
什么是内存可见性
什么是有序性
Java线程的创建方式
线程的状态
多线程运行产生线程安全问题的原因分析
Volatile关键字的作用
Synchronized关键字的作用
如何线程安全的使用顺序表
如何线程安全的使用队列(阻塞队列)
HashTable与concurrentHashMap的区别
线程池
简述线程池
线程池常用类型
线程池的状态
线程池参数
发放一个任务,线程池的几种工作场景
多种锁策略
偏向锁
乐观锁与悲观锁
读写锁
重量型锁和轻量型锁
自旋锁和自适应自旋锁
可重入锁和不可重入锁
死锁
CAS
ABA问题
CountLatch
CyclicBarrier
Semaphore
锁升级
锁粗化
锁清除
有了Synchronized为什么还需要ReentrantLock
基础部分
Java语言有什么特点
1.java是纯面向对象的语言,能够直接反映生活当中的对象。
2.java是具有平台无关性的解释性语言,java利用JVM来运行字节码,javac将.java文件编译为与平台无关的.class中间文件,在经由JVM编译为机器能够识别的文件,能够很好的进行移植。
3.java具有健壮性和安全性,提供了异常处理和垃圾回收机制,并移除了c语言中的难以理解的指针。
JDK与JRE的区别
jdk:java开发工具包,包含有jre,为java提供了开发环境和运行环境
jre:java运行环境,为java提供运行环境。
Java的基本数据类型
byte:占有1个字节
short:占有2个字节
int:占有4个字节
long:占有8个字节
float:占有4个字节
double:占有8个字节
boolean:根据虚拟机的不同
char:占有2个字节
装箱和拆箱过程
装箱:将基本的数据类型转换为包装类
拆箱:将包装类转换为基本数据类型
Java访问修饰符
private:同一个类中可见,不可修饰类
default:同一个包内可见
protected:同一个包下的类和所有子类可见,不能修饰类
public:对所有类都可见
构造方法,成员变量和静态成员变量的初始化顺序
父类静态成员变量,父类静态代码块,子类静态成员变量,子类静态代码块,父类非静态成员变量,父类非静态代码块,父类构造器,子类非静态成员变量,子类非静态代码块,子类构造器。
面向对象的三大特征
封装:利用private访问修饰符限制其他的类对内部变量的访问,可以提供public修饰的set/get方法来进行数据操作与访问,降低数据的访问代价。
继承:
1.从父类中派生出新的类称为子类,子类可以从他的父类继承方法和实例变量,实现代码的复用,也可针对自己的需求进行代码的重写。
2.java中是不允许多继承的,接口除外(因为接口只有方法定义,没有方法实现,只有实现接口的类对方法进行实现才能够使用),假如A继承B,C,当A调用B和C都拥有的方法时就会发生二义性。
多态:
1.允许不同的类的对象对同一信息作出反应,即使调用的方法和参数相同,最终的表现形式也是不同的。
2.java为多态提供了重载和重写两种机制:重载是在同一个类中,对相同的方法名提供不同的参数列表,在编译时就能决定使用哪个方法。重写是发生在父类和子类之间的,使用父类指向子类的实例对象,或者接口类指向实现类的实例对象。
3.多态分为编译时多态和运行时多态,编译时多态主要是指方法的重载,即根据不同的参数列表调用不同的方法。而运行时多态指的是父类的继承和接口的实现,父类的引用可以指向子类对象。
重载和重写的区别
重载发生在同一个类中,而重写发生在父类子类之间
重载是多个方法之间的关系,而重写是一对方法的关系
重载的参数列表必须不同,而重写的参数列表必须相同
调用方法时,重载之间根据参数列表确定调用的方法,编译时即可确定;重写之间根据对象的类型来确定调用的方法,运行时才能确定。
重载可以改变返回值而重写不能
接口类和抽象类的相同点和不同点
相同:
都不可以被实例化,只有通过抽象类的子类和接口类的实现类对方法进行重写才可以实例化。
不同:
接口只能有方法的定义,而抽象类可以有方法的定义和实现。
一个类可以实现多个接口,但是只能继承一个抽象类
当子类和父类有层次上的关系时,推荐是由抽象类,抽象类以便于方法功能的积累;当功能差别比较大,推荐使用接口类,能够降低软件的耦合度,方便日后的删除和修改。
内部类及其作用
成员内部类:作为成员对象内部的类,可以访问private以及以上的外部类的属性和方法,外部类想要访问内部类的属性和方法时需要先创建一个内部类对象,通过对象去访问内部类的属性和方法,外部类也可以访问内部类的private属性,不可以存在static修饰的方法和属性。
局部内部类:存在与方法中的内部类,不允许使用任何访问修饰符和static,除了创建该内部类的方法,其他均不可访问;只能访问外部类的final变量。
匿名内部类:只能使用一次,只能访问外部类的final变量。
静态内部类:不需要依赖外部类,可以直接创建;不可以使用外部任何非静态的方法和变量。
static关键字的作用
1.static能够为某种数据类型或者对象划分与对象个数无关的单一的存储空间
2.使得某个方法或者属性与类关联起来而不是对象,即在不创建对象的情况下可以直接使用方法或者使用类的属性。
3.修饰成员变量,用static修饰的变量在内存中只会存储一份副本,在加载当前类时就会给静态成员变量划分空间,可以通过“类.静态变量”,“对象.静态变量”进行使用。
4.修饰成员方法, static修饰的成员方法无需创建对象就可直接使用,static方法中不可使用this和super,static方法只可使用所属类的静态成员变量和静态方法。
5.修饰代码块,JVM加载类时就会调用static代码块,static代码块只会被执行一次,通常用来初始化变量。
6.修饰内部类,静态内部类可以不依赖与外部类实例对象而被实例化,只可访问外部类的静态变量与方法。
为什么将String设计为不可变
1.节省空间:String存储于字符串常量池当中,可以被多个用户读取
2.提高效率:因为String的不可变,属于线程安全,在多线程的操作下可以不进行同步操作
3.安全问题:String常常被用来存储用户名密码,由于String的不可变,可以避免黑客的恶意篡改。
String/StringBuffer/StringBulider之间的关系
String采用fianl来修饰,是不可变得,对字符串进行操作只能新建对象。
StringBuilder不采用final,可以进行字符串的拼接,但通过分析源码可知,是线程不安全的。
StringBuffer不采用final,通过分析源码可见被synchronized修饰,是线程安全的。
==与equals的区别
==比较的是引用,而equals比较的是内容。
当==比较的是基本数据类型则是判断内容是否相等,如果是比较引用类型,则判断引用是否指向同一块内存空间。
equals属于Object中的方法,因此每个类都具有这个方法,Object中的equals直接采用==来进行比较,通过重写可以实现比较内容的功能。
Object中常用的方法
hashCode:通过对象计算出散列值,用于map型和equals方法,需要保证同一个对象多次调用该方法返回相同的值。
equals:判断两个对象是否一致,需保证equals方法相同,对应的对象hashCode也相同。
toString:通过字符串输出该对象
clone:深度拷贝一个对象
Java中的异常
异常分为Error(程序无法处理的错误)和Execption(程序可以处理的异常),均继承与Throwable。
Error常见的有StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)
Execption分为编译时异常和运行时异常,运行时异常可以通过try/catch处理,编译时异常必须处理,否则无法进行编译。
throw出现在方法体内部,有程序员自定义程序发生异常时抛出的类型
throws出现在方法声明上面,代表该方法可能出现的异常
finally代码块是否一定执行
当进入try代码块之前发生异常,或者在try代码块中通过System.exit(0)来强制退出就不会执行finally代码块。
正常执行时,先执行try代码块,发生异常后执行catch代码块之后再执行finally代码块,若try当中有return,则跳过return,先执行finally在执行return,若finally中有return则会覆盖掉try中的return。
简述final,finally和finalize的区别
final可以用于修饰变量,方法和类,分别表示变量不可修改,方法不可重写和类不可继承
finally用于try/catch中表示一定被执行的部分,通常用于释放内存
finalize是Object的一个方法,在垃圾回收器准备好回收对象资源时,就会调用该对象的finalize方法,并且在下次垃圾回收器进行动作时真正的释放内存
简述泛型
泛型用于不确定传入参数的类型的问题,Java编译生成的字节码是不包含泛型信息的,在编译期间被擦除,称为泛型擦除。
Java的反射机制
java的反射机制值得是可以再程序运行期间构造任意一个类的对象,获取任意类的成员变量和成员方法,获取任意一个对象的类信息,调用任意一个对象的属性和方法。java的反射机制使得可以动态获取对象信息和动态调用对象方法的能力。
Class类:可获得类属性方法
Field类:可获得类的成员变量
Method类:可获得类的方法信息
Construct类:可获得类的构造方法信息
Java中的List
List是一个有序队列,在java中有两种实现方式:
ArrayList基于数组实现,属于线程不安全结构,随机访问元素比较快但增删元素较慢,对ArrayList扩容时需要新建一个数组,将原有元素放入其中。
linkedList基于双向链表实现,属于线程不安全结构,增删元素很快但随机访问比较慢。
Java中的Set
Set即集合,该结构中不允许有重复的元素并且元素无序,java中有三种方法实现Set:
1.HashSet基于HashMap实现,HashMap中的key值就是Set的元素值,而Value则被系统定义为PRESENT的Object变量,比较是否相同时先通过hashCode比较,相同后在通过equals比较。
2.linkedHashSet,继承自HashSet,基于linkedHashMap实现,通过双向链表维护元素插入其中的顺序。
3.TreeSet基于TreeMap实现的,底层是红黑树,通过一定的规则比较将元素插入其中使其集合依然有序。
Java中的HashMap
HashMap在JDK7以前基于数组+链表实现,JDK8之后基于数组+链表/红黑树实现,主要成员变量头table数组用于存储数据,size元素数量以及加载因子loadFactor,数据是以键值对的形式存储的,key对应的hash值用来计算数组下标,如果hash值相同就会发生哈希冲突,存放到同一个链表当中。
table数组存放的是一个链表,hash值相同的元素都会存放在同一个链表当中,Node/Entry节点当中存放有四个数据:key,value,next指针和hash值。JDK8之后若链表超过8则会转换为红黑树。
当前数据量/总容积>负载因子就需要对HashMap进行扩容,默认初始大小为16,每次扩容为2的幂次方。
HashMap是线程不安全的,因为JDK7以前采用的是头插法,因此在并发编程状态下容易形成环路,进而死循环。JDK8值都采用尾插发改善了这一状况,但是并发下的put操作会使前一个key值被后一个key值覆盖。HashMap也存在扩容机制,可能会导致线程一进行扩容之后可能导致线程二的get操作失败。
Java中的TreeMap
TreeMap是基于红黑树实现的Map结构,底层是一颗平衡的排序二叉树,他的插入删除和遍历所需要的时间复杂度为O(longn)因此效率低于哈希表,但是哈希表是无序的,而TreeMap可以实现数据的有序输出。
ArrayList,Vactor,linkedList的相同点和不同点
1.ArrayList,Vactor,linkedList都可以动态改变长度
2.ArrauList和Vactor都是基于数组来实现的,在内存当中开辟一块连续的空间存储,对于数据的增加,删除的功能比较低效,当超过最大长度时均可进行扩容。linkedList是基于双向链表的结构,访问元素效率很低,但是增删元素比较高效。
3.ArrayList和linkedList是线程不安全的,而Vactor是线程安全的,其大部分方法是直接或者间接同步的
HashMap和HashTable的区别
HashMap是HashTable的轻量级实现,HashMap中允许key,value为null,但是HashTable不允许
HashMap是线程不安全的,在多线程中使用需要额外的同步机制,HashTable是线程安全的。
HashMap通过Iterator进行遍历,HashTable通过Enumeration进行遍历。
HashMap和TreeMap的选择
如果经常进行数据的增删改查推荐使用HashMap,但如果需要对数据进行一个有序的遍历推荐使用TreeMap。
hashCode和equals的关系
hashCode和equals都是从Object中继承过来的方法,equals比较的是引用指向同一块内存空间,而hashCode则是将对象的内存地址通过哈希规则转换为一个哈希码。
HashSet中首先通过比较hashCode的值,如果不相同则对象不同,若相同则继续通过equals进行比较,若相同则为同一对象否则不同。
Collection和Collections的区别
Collection是集合框架的父接口,他提供了很多集合对象进行基本操作的通用接口方法,所有的集合都是他的子类
Collections是一个包装类,它包含有很多集合类的静态多态方法,不能被实例化,类似于一个工具类服务于Collection
并发编程
JMM模型
所有的变量都存储在主存当中,每个线程都有自己的工作内存;
工作内存中存储了被该线程操作的变量的主存副本,所有的操作只能在自己的工作内存中进行,不能对主存数据直接进行读写操作;
操作完毕后通过缓存一致性协议将数据写进主存中
什么是原子性
一系列的操作要么全部都执行并且执行过程中不被任何因素打断,要么就都不执行。
什么是内存可见性
当一个线程修改完共享数据之后,所有的线程都能够得知,通过Volatile,synchronized,final关键字可以保护内存可见性
什么是有序性
多线程存在并发和指令重排序等操作,但在本线程内观察指令依然是有序的。
Java线程的创建方式
1.继承Thread:建立一个类继承自Thread,重写run方法,在run函数中写入执行流在主函数中创建实例并通过start函数执行 为了方便观察引入sleep睡眠函数,但线程可能会提前苏醒,通过异常包裹。睡眠时间过后线程将重新抢占CPU时间片,若没抢占到则延后执行直至抢占到,所以等待线程的执行可能多于睡眠时间。
public class Text {
public static void main(String[] args) {
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("新线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
MyThread myThread = new MyThread();
myThread.start();
while(true){
System.out.println("主线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.实现Runnable接口:
public class ThreadDemo2 {
public static void main(String[] args) {
class MyTask implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Runnable MyTask = new MyTask();
Thread thread = new Thread(MyTask);
thread.start();
}
}
3.lambda表达式:
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread(()-> System.out.println("lambda表达式建立线程"));
thread.start();
}
}
4.匿名实现的两种方法
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello");
}
}
});
thread.start();
}
}
线程的状态
NEW:此时线程被创建出来,但是还未通过start()启动
RUNNABLE:线程处于运行状态,但是可能没有在运行,处于排队等待CPU时间片的过程中
BLOCKED:线程未能获取到锁,等待有锁的释放,进入阻塞状态
WAITTING:等待状态,线程执行Object.wait()/Thread.join()方法后进入的状态
TIMED_WAITTING:进入一段限期时间的等待,线程执行完Object.wait(long)/Thread.join(long)/Thread.sleep(long)后进入的状态
TERMINATED:结束状态,线程执行完run()方法后进入的状态
多线程运行产生线程安全问题的原因分析
1.线程之间是抢占式的执行(系统调度问题)
2.多个线程操作一个数据(需求决定)
3.不是原子操作,一个线程读取数据之后到将数据存储到主存之前,有另一个线程读取了还未操作的数据,因此出现了bug
4.内存的可见性问题,因为从主存中读取数据比较耗时间,JVM自己进行了优化,将多次读取执行存储操作合并为一次读取多次执行一次存储这样的结构,导致了一个线程操作数据之后其他线程未能及时得知此次操作而导致的bug
5.指令的重排序问题,也是JVM擅自进行优化导致的bug,单线程模式下并未改变执行的逻辑,但多线程的情况下可能将逻辑改变了。
Volatile关键字的作用
保证了内存可见性,禁止了指令重排序。
Synchronized关键字的作用
保证了原子性和内存可见性,禁止了指令重排序
如何线程安全的使用顺序表
1.自己手动加锁
2.Collections.synchronizedList,相当于在ArrayList等集合类加了一层壳,壳里面试用synchronized来加锁。
3.CopyOnWriteArrayList,让不同的线程使用不同的变量,并没有加锁。属于写时拷贝,多个线程来读取一份数据,某个线程进行了修改,立刻就给这个线程拷贝一份新的数据。
如何线程安全的使用队列(阻塞队列)
Java提供了多种阻塞队列:
1.ArrayBlockingQueue:底层是由数组实现的有界阻塞队列
2.linkedBlockingQueue:底层是由链表实现的有界阻塞队列
3.PriorityBlockingQueue:优先级阻塞队列
4.TransferQueue:只包含一个元素的阻塞队列,生产一个元素加入队列时会一直阻塞直到其中的元素被消费
5.DelayQueue:创建元素时可以指定多久之后才能从队列中获取到当前元素
6.SynchronizedQueue:不存储元素,每一个存储必须等待一个取出操作
HashTable与concurrentHashMap的区别
concurrentHashCode:
1.并不是针对整个对象进行加锁,而是分成很多把锁,每个链表/红黑树进行加锁,只有当多个线程修改到同一个链表/红黑树才会发生锁竞争。
2.针对读操作直接不加锁,虽说是一个十分大胆的操作不过还好,大部分的场景状态下对读操作的线程安全并没有太高的要求,如果有则更加推荐读写锁的使用。
3.内部采用大量的CAS操作,提高效率
4.针对扩容进行了优化,HashTable的扩容特别麻烦,需要将整个表进行一次拷贝,如果轮到了哪个倒霉线程去执行,就需要负责整个扩容的过程,相对比来说,ConcurrentHashMap将扩容的任务分散开了,一次只扩容一点,能够更加平滑的过度。
HashTable:并不推荐使用,单纯的一个synchronized对整个哈希表进了加锁,坏处就是锁冲突会特别容易发生。
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
线程池
简述线程池
没有线程池的情况下,对于现成的创建和销毁将耗费大量的资源,如果能过对线程进行复用就能节省大量的资源,降低开销;线程池创建线程时会将线程封装为工作线程worker,worker执行完任务后会循环继续从工作队列中获取任务执行。
线程池常用类型
newCachedThreadPool:可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟销毁
newFixedThreadPool:可指定工作线程的数量的线程池
线程池的状态
RUNNING:能够接受新的任务,并且也能处理阻塞队列中的任务
SHUTDOWN:不能够接受新的任务,但是能继续处理阻塞队列中的任务
STOP:不接受新任务也不处理阻塞队列中的任务,处理中的任务将被中断
TIDYING:所有任务都已终止,工作线程数为0
TERMINATED:执行terminated()方法后进入的状态
线程池参数
corePoolSize:核心线程数
maximumPoolSize:最大线程数(核心线程数+临时线程数)
keepAliveTime:允许临时线程空闲的时间(不累计)
unit:空闲的时间的基本单位
workQueue:线程池的任务队列,可以由程序员进行指定,指定的意义在于明确队列的长度以及是否需要带有优先级
handler:拒绝策略,如果线程池满了之后如何处理:
AbortPolicy:直接异常
CallerRunsPolicy:重新尝试提交任务
DiscardOldestPolicy:丢弃最老的任务
DiscardPolicy:丢弃当前的任务
如何选用则是具体情况具体分析。
threadFactor:线程工厂,创建线程用的辅助类,用于生产一组相同的任务
发放一个任务,线程池的几种工作场景
先检查线程池是否为RUNNING状态,不是的话拒绝任务
如果工作线程小于核心线程数则创建线程并执行新提交的任务
如果工作线程大于等于核心线程数并且阻塞队列没有满,则将任务放入阻塞队列中等待执行
如果工作线程大于等于核心线程数并且小于最大线程数,切阻塞队列满了,则创建新的线程执行该任务
如果工作线程数大于最大线程数并且阻塞队列已经满了,则执行拒绝策略处理该任务
多种锁策略
偏向锁
大多数情况下,锁的竞争状态并不存在,而加锁过程有浪费了大量的资源;每次进行加锁时就会判断偏向锁是否偏向自己,如果是则进入同步状态。
偏向锁的实现:
1.首先通过对象的Mark Word判断是不是偏向状态,如果不是则进入轻量型锁的判断。
2.判断请求锁的线程ID是否与偏向锁记录的ID是否一致,一致的话判断是否需要重偏向,如果不需要则直接获得偏向锁
3.利用CAS算法将记录的ID替换为当前线程的ID,如果成功则重偏向成功,获得偏向锁;如果失败则说明有多个线程竞争,升级为轻量型锁
乐观锁与悲观锁
乐观锁:这个锁认为出现锁竞争大概率比较小(当前场景中,线程数目比较少,不太涉及锁竞争),工作量比较小,付出的代价也比较少。读操作前不上锁,执行写操作才会进行判断,如果数据发生改变则返回false,如果数据没有改变则进行操作并返回true;通常乐观锁是基于CAS实现的。
悲观锁:这个锁认为出现锁竞争大概率比较大(当前场景中,线程数目比较多,非常可能涉及锁竞争),工作量比较大,付出的代价也比较多。
操作系统中的Mutex就是一个典型的悲观锁。Java中的synchronize既属于乐观锁也属于悲观锁,会根据当前锁冲突的状况来切换模式。
读写锁
这是一种特殊的锁,将读操作和写操作分别加锁,能够进一步减少锁冲突。一般具有以下三种情况:
读加锁和读加锁之间不发生互斥。
读加锁和写加锁之间发生互斥。
写加锁和写加锁之间发生互斥。
根据这三种情况不难判断出该锁更加适用于少写多读的场景当中。
重量型锁和轻量型锁
这两种锁非常类似于乐观锁和悲观锁,重量型锁大概率也是悲观锁,轻量型锁大概率也是乐观锁。乐观锁和悲观锁根据锁冲突来划分的,而重量型锁和轻量型锁根据工作量来划分的。
轻量型锁的实现:
1.如果同步对象没有被锁定,则在虚拟机在当前线程的栈帧中划分一个记录锁空间,存储锁对象目前Mark Word的拷贝。
2.虚拟机尝试使用CAS将Mark Word更新为指向锁的记录指针。
3.如果更新成功则表示这该线程拥有锁,锁标记为00;如果更新失败,就会检查Mark Word是否指向当前线程的栈帧,如果是则说明当前线程已经获取到锁,直接进入同步状态,如果不是则说明锁被另一个线程锁获取到了,这时就不再是轻量型锁了,而是膨胀为重量型锁。
自旋锁和自适应自旋锁
自旋锁:线程在竞争失败之后并未放弃CPU,而是进行一段时间的自旋,在自旋的过程中不断的尝试重新获取到锁
自适应自旋锁:自旋的时间不再是由人为设定的了,而是通过上一任锁拥有者的自旋时间和自旋状态来决定的了。
可重入锁和不可重入锁
一个线程对同一把锁进行两次加锁,若没有问题则成为可重入锁,若存在问题则成为不可重入锁。synchronized就属于一种可重入锁,通过对synchronized我们来了解一下可重入锁的机制。
在synchronized当中持有哪个线程获取到锁并且将会维护一个计数器,当该线程再次遇到加锁状况时并没有真正的加锁,而是计数器自增,同理可的当遇到解锁时并不会真正的解锁,而是计数器自减,直到计数器为0时才会真正地解锁。
死锁
死锁的出现往往伴随着线程挂掉等严重的Bug的出现:
1.一个线程一把锁之间通过可重入锁可完美的解决这个问题。
2.两个线程两把锁,线程一获取到锁A,在获取锁B,线程二获取到锁B在获取锁A,那么这个时候,线程一想要获取锁B则需要线程二解开锁B,而线程二想要获取锁A则需要线程一解开锁A,这样就形成了环路等待。
3.N个线程M把锁,与2情况相同原因,形成了环路等待。
解决方法:
1.一般的在锁当中不要轻易的加锁,不过说起来容易做起来难,很大的概率根据需求分析不可避免的锁上加锁。
2.约定一个固定的加锁顺序,例如要先加锁1,再加锁2,再加锁3。这样不会形成环路等待。
CAS
1.该算法认为竞争比较少
2.该算法的核心在于比较线程读取的值和内存中的值是否一致,如果一致则说明中间过程数据没有被操作,将该变量替换为新的值;如果不一致说明该数据被其他线程更改,则不进行任何操作。
ABA问题
即在CAS判断的过程中,一个线程读取值后,另一个线程将数据A操作为B,再将B操作为A,那么CAS很容易误认为该数据未进行其他操作。我们通过转账来分析一下:
JUC提供了一个AtomicStampedReference,即在原先的版本下加入一个版本戳,解决ABA问题
CountLatch
是指一个线程或多个线程等待其他线程完成任务之后才能执行。通过一个计数器来实现的,计数器的初始值为工作线程的个数,每有一个线程完成工作,就会调用countDown()方法来使计数器-1,如果计数器不为0,其他线程调用await()方法时就会进入阻塞状态,直到计数器为0才会执行其他等待的线程。只能使用一次,不能reset。
CyclicBarrier
与ConutLatch功能类似,也是通过计数器使一个或多个线程一组现成的完成。但是可以重复使用
Semaphore
semaphore信号量,是并发编程的一个重要概念,表示可用资源的数量。信号量涉及的核心操作:P操作:申请一个资源(可用资源-1),V操作:释放一个资源(可用资源+1)与此同时,可用资源变化的操作均属于原子操作。
semaphore持有acquire()方法和release()方法,分别对应P操作和V操作。当可用资源为0时执行acquire方法时线程就会进入阻塞状态,执行release方法时就会释放资源使可用资源+1,这时被阻塞的线程将会竞争这份资源,获取到之后就会继续执行。
锁升级
我们以synchronized来说一下什么是锁升级:
当一个线程进行操作时首先需要进行加锁,但此时并不是真的加锁,而是在对象头里面通过一个标志位进行标记。此时synchronized是偏向锁,在赌没有其他锁来竞争,往往赌还能赌成功。
此时又有一个线程来进行操作时,第二个线程就会尝试加锁,与此同时第一个线程将会立即获取锁,而后面的线程也将尝试真正的加锁,此时涉及到锁竞争,synchronized此时为轻量型锁。
越来越多的线程参与进锁竞争,竞争越来越激烈,自旋锁也慢慢的不容易获取到锁,并且还占用大量的CPU资源,此时synchronized就会继续膨胀为重量型锁,线程争取不到锁之后就会进入阻塞状态,争取到的就会继续工作。
总而言之,synchronized的锁膨胀/锁升级过程是为了更好地适应不同的环境,提高执行的效率。
锁粗化
锁的粒度代表synchronized所影响的范围。思想就是增加锁的粒度,扩大synchronized的影响范围,避免重复加锁解锁
锁清除
初学程序员写代码过程中可能会多次无意义的加锁而导致程序效率变低,JVM如果将该程序判定为不涉及线程安全问题,就会自动将锁去掉。例如单线程变成的情况下,使用StringBuffer将会涉及到无意义的加锁,编译器判定在一个线程内完成,就不会再加锁,而是从编译生成的字节码当中直接去除加锁过程。
有了Synchronized为什么还需要ReentrantLock
ReentrantLock是JUC(java.util.concurrent)的一个组件,并且也是一个可重入锁。ReentrantLock有两个方法,一个是lock()用于加锁,一个是unlock()用于解锁,这样的做法有优点也有缺点:
缺点:程序员容易忘记解锁从而导致Bug的产生。
优点:更加的灵活,可以将lock()放在一个方法里,unlock()放进另一个方法里。
ReentrantLock还提供了另一个方法tryLock(),对于synchronized来说,如果锁被占用了,将会进入阻塞状态,直到对方释放锁,不管这个时间多久都会一直等待;而对于tryLock()来说,此处不留爷自有留爷处,tryLock()将会立即返回或者等待一段时间后返回。这样就会导致节省了很多的资源以及更多的操作空间。
区别:
synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放



