为对象分配内存:如果内存规整,虚拟机使用碰撞指针法(指针向空闲区前移对象大小的距离);如果不规整则使用空闲列表法。并发安全:虚拟机维护一个列表记录哪些内存块可用,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表内容。
初始化分配的空间:所有属性初始化为零,保证对象实例字段在不赋值的时候可以直接用
设置对象头信息
执行构造方法初始化
逃逸:方法体内创建的对象,方法体外被其他变量引用过。这样在方法执行完毕之后,该方法中创建的对象不能被GC回收。开启逃逸分析之后,如果对象的作用域仅在方法内,那对象可以创建在虚拟机栈上,随方法入栈创建,出栈销毁,减少GC回收压力。
对象的内存布局:包含三部分:对象头,实例数据和对齐填充。
对象头:运行时数据和类型指针。标记字段包含hashcode
、GC分代年龄
、锁状态标志
、线程持有锁等信息
;类元数据的指针
:可以知道这个对象是哪个类的实例。
实例数据:存储对象真正的数据,也包含父类的数据。
对齐填充:保证对象大小是8字节的整数倍。
在jdk1.2之前,Java对引用的定义很传统:如果reference类型的数据中存储的数值是另一块内存的起始地址,就称这块内存代表一个引用。
- 强引用:Java中默认声明的引用为强引用,只要强引用存在,垃圾回收器永远不会回收被引用的对象,哪怕内存不足,JVM也只会抛出OOM错误,不会去回收。
Object obj = new Object();
- 软引用:用于描述一些非必需但仍有用的对象。内存足够的时候,软引用对象不会被回收,只有在内存不足的时候,系统会回收软引用对象,如果内存还是不够才会抛出OOM异常。这种特性使他往往用于实现缓存技术。在
JDK1.2 之后,用java.lang.ref.SoftReference
类来表示软引用。
- 弱引用:弱引用的强度比软引用更弱。无论内存是否足够,只要JVM开始垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2
之后,用java.lang.ref.WeakReference
来表示弱引用。
- 虚引用:最弱的引用关系。与其他几种引用不同,虚引用不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,任何时期都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,且必须与引用队列联合使用。当垃圾回收器准备回收一个对象的时候,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
-
内存泄漏:一个不再被线程所使用的对象或变量还在内存中占用空间。
-
内存溢出:程序无法申请到足够的内存。
内存泄漏的原因
1.长生命周期的对象持有短生命周期对象的引用。
2.连接未正常关闭。
3.变量作用域设置过大
避免内存泄漏
1.避免在循环中创建对象
2.没有用的对象尽早释放
3.慎用静态变量
4.字符串的拼接使用Stringbuffer/StringBuilder
5.增大xmx和xms的值
内存溢出的原因
1.加载数据过大
2.死循环或过多循环
3.启动参数中内存值设定过小
栈溢出
原因:递归深度过大、局部变量过大
解决:递归不要太深,局部变量改为静态变量
如果排查内存问题
1.JConsole:能看到内存用量的趋势,确定是否有问题
2.GC日志:能看到年轻代和老年代等区域配置是否合理
3.代码中打印内存使用量
4.分析dump文件:针对性的看到发生OOM时候的内存使用量和线程情况
List:有序的对象
(1)ArrayList
:数组
(2)Vector
:数组
(3)LinkedList
:双向链表
Set:不允许重复的集合
(1)HashSet
(无序且唯一):基于HashMap
(2)LinkedHashSet
:基于HashMap
(3)TreeSet
(有序且唯一):基于红黑树
Map:使用键值对存储
(1)HashMap
:Jdk1.8之前HashMap
由数组+链表组成,之后再链表长度大于阈值(默认8)时将链表转换为红黑树以减少搜索时间。
(2)LinkedHashMap
:继承自 HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。
(3)HashTable
:数组+链表组成,数组是HashMap
的主体,链表为了解决哈希冲突
(4)TreeMap
:红黑树
ArrayList、LinkedList、Vector
的区别
-
存储结构:
ArrayList
和Vector
是基于数组实现的,而LinkedList
是基于双向链表实现的。 -
线程安全性:
ArrayList
不具有线程安全性(ArrayList
添加元素的操作不是原子操作,可能会出现一个线程的值覆盖另一个线程添加的值的问题),在单线程的环境中,LinkedList
也是不安全的。Vector实现了线程安全,它大部分的关键字都包含synchronized
,但效率低。 -
扩容机制:
ArrayList
和Vector
都是用数组来存储,容量不足的时候可以扩容,ArrayList
扩容后的容量是之前的1.5倍,Vector默认是2倍。Vector
可以设置扩容增量capacityIncrement
。可变长度数组的原理是当元素个数超过数组长度时,产生一个新的数组,将原数组的数据复制到新数组,再将新元素添加到新数组中。 -
增删改查效率:
ArrayList
和Vector
中,从指定的位置检索一个对象,或在末尾插入删除一个元素时间复杂度都是O(1),但是在其他位置增加和删除对象的时间是O(n);LinkedList
,插入删除任何位置的时间都是O(1),但是检索一个元素的时间是O(n)。
继承Thread
类,重写run方法,继承Thread
类的线程类不能再继承其他父类。
实现Runnable
接口,重写run方法
通过Callable
接口和Future
接口创建线程,执行call方法,有返回值可以抛异常
线程池。前三种的线程如果创建关闭频繁的话会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取。
BIO:传统的网络通讯模型,同步阻塞IO。服务器实现是一个连接一个线程,客户端有连接请求的时候,服务端就要启动一个线程去处理。线程数量可能会爆炸导致崩溃。适用于连接数目小且固定的架构。
NIO:同步非阻塞。服务器实现是一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,复用器轮询到连接有IO请求才启动线程。适用于连接数目多且连接比较短的架构,比如聊天服务器。
AIO:异步非阻塞。用户进程只需要发起一个IO操作然后立即返回,等IO操作真正完成之后,应用程序会得到IO操作完成的通知。适用于连接数目多且连接长的架构。
重写(Override
):重写是子类对父类允许访问的方法实现过程进行重新编写,返回值和形参都不能改变。重写的好处是子类可以根据特定需要,定义特定行为。异常范围可以减少,但是不能抛出新的或更广的异常。
class Animal{
public void move(){
System.out.println(“动物可以移动”);
}
}
//加入Java开发交流君样:756584822一起吹水聊天
class Dog extends Animal{
public void move(){
System.out.println(“狗可以跑和走”);
}
}
public class TestDog{
public static void main(String args[]){
Animal a = new Animal(); // Animal 对象
Animal b = new Dog(); // Dog 对象
a.move();// 执行 Animal 类的方法
b.move();//执行 Dog 类的方法
}
}
虽然b属于Animal类型,但是它运行的是Dog
类的move
方法。因为在编译阶段,只是检查参数的引用类型,运行时JVM指定对象的类型并运行该对象的方法。
方法重写规则
-
(1)参数列表和被重写方法的参数列表必须完全相同。
-
(2)访问权限不能比父类中被重写的方法访问权限更低。
-
(3)父类的成员方法只能被它的子类重写。
-
(4)声明为final的方法不能被重写;声明为
static
的方法不能被重写,但是能被再次声明。 -
(5)构造方法不能被重写。
-
(6)子类和父类在同一个包中,那么子类可以重写父类中没有声明为private和final的方法;如果不在同一个包中,子类只能重写父类声明为
public
和protected
的非final
方法。
当需要在子类中调用父类的被重写方法时,使用super关键字。
重载(Overload):是在一个类里面,方法名字相同,参数不同的两个方法。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)必须有一个独一无二的参数类型列表。常用于构造器重载。
重载规则
(1)被重载的方法必须改变参数列表。
(2)被重载的方法可以改变返回类型,可以改变访问修饰符,可以声明新的或更广的异常检查。
(3)方法能够在同一个类中或者在一个子类中被重载。
public class Overloading {
public int test(){
System.out.println(“test1”);
return 1;
}
public void test(int a){
System.out.println(“test2”);
}
//以下两个参数类型顺序不同
public String test(int a,String s){
System.out.println(“test3”);
return “returntest3”;
}
public String test(String s,int a){
System.out.println(“test4”);
return “returntest4”;
}
public static void main(String[] args){
Overloading o = new Overloading();
System.out.println(o.test());
o.test(1);
System.out.println(o.test(1,“test3”));
System.out.println(o.test(“test4”,1));
}
}
方法重载和方法重写是java多态的不同表现。
17.final/finally/finalize与static
-
final
:java中的关键字,修饰符。如果一个类被声明为final
,就意味着它不能再派生出新的子类,不能作为父类被继承。一个类不能被同时声明final和abstract
抽象类。如果变量或方法被声明为final
,就能保证它们在使用中不被改变,变量必须在声明时赋值,以后的引用中只读,被声明final的方法只能使用,不能重载。 -
finally
:java的一种异常处理机制。java异常处理模型的最佳补充,finally
结构使代码总会执行,而不管有无异常发生。使用finally可以维护对象的内部状态,清理非内存资源。在关闭数据库连接时,如果把数据库连接的close()
方法放到finally中,就会减少出错的可能。 -
finalize
:Java中的一个方法名,该方法是在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器确定这个对象没被引用的时候调用的。它在Object
类中定义,因此所有类都继承了它。子类可以覆盖该方法来整理资源和清理。 -
static
:static修饰的属性在编译器初始化,初始化之后能改变,final修饰的属性可以在编译器也可以在运行期初始化,但是不能被改变;static
不能修饰局部变量,但是fina
l可以。
18.String、StringBuffer和StringBuilder的区别
String是java编程中广泛使用的,但它的底层实现实际是一个final
类型的字符数组,其中的值不可变,每次对String进行操作就会生成一个新对象,造成内存浪费。
private final char value[];
StringBuffer/StringBuilder
:它们的底层是可变的字符数组,都继承AbstractStringBuilder
抽象类,所以在进行频繁的字符串操作的时候,尽量使用这两个类,它们的区别是:StringBuilder
是线程不安全的,但执行速度较快;StringBuffer
线程安全,但执行速度慢。StringBuffer
使用synchronized关键字进行同步锁。
另外,String类型的比较,“==”是比较两个内存地址是否一样,而“equals
”是比较两个字符串的值是不是一样的。
引用计数算法:为对象增加一个引用计数器,当对象增加一个引用的时候+1,引用失效-1,引用计数为0的对象可以被回收。但是当两个对象循环引用的情况下,计数器永远不为0,因此JVM不使用引用计数算法。
可达性分析算法:以GC Roots为起点开始搜索,可达的对象都是存活的,不可达的对象可以被回收,JVM使用该算法进行判断。GC Roots中包含:虚拟机栈中引用的对象、本地方法栈中引用的对象,方法区中静态成员或常量引用的对象。
标记-清除算法(Mark-Sweep)
标记阶段:标记的过程实际上就是可达性分析算法过程,遍历GC Roots
对象,可达的对象都做好标记,在对象的header
中将其记录为可达。
清除阶段:对堆进行遍历,如果发现有某个对象没有可达对象标记,则回收。
缺点:两次遍历,效率低;GC运行时需要停止整个程序;产生大量的碎片,需要维护一个空闲列表。
复制算法(Copying)
对象在Survivor
区每经历一次Minor GC
,就将对象年龄+1,当对象年龄达到某个值时,对象复制到老年代,默认为15。JVM中Eden
和Survivor
区的默认比例为8:1:1,保证内存利用率为90%,如果每次回收有多于10%的对象存活,Survivor
空间可能就不够用了,此时借用老年代空间。
缺点:复制收集算法在对象存活率高的时候需要进行很多的复制操作,效率会变低,老年代一般不会用该算法。
标记-整理算法
第一阶段和标记-清楚算法一样,第二阶段将所有存活的对象压缩到内存的另一端,按顺序排放。之后,清理边界外所有的空间。
缺点:效率不高,不仅要标记存活对象,还要整理所有存活对象的引用地址;移动过程中,要全程暂停用户应用程序。
分代收集算法
新生代
:使用复制算法,因为大量对象需要回收。
老年代
:回收的对象很少,所以采用标记清除或者标记整理算法。
21.Double与Float
java语言支持两种基本的浮点类型:float
和double
。32位浮点数float用1位表示符号,8位表示指数,用23位表示尾数;64位浮点数double
用一位表示符号,11位表示指数,52位表示尾数。在表示超过23位的时候,float就会自动四舍五入,这就是float
的精度限制,所以会出现double
可以表示而float会不精确的情况,如果要将这两个浮点数进行转型,java提供了Float.doubleValue()
和Double.floatValue()
方法。使用这个方法在单精度转双精度的时候,会出现偏差。
浮点运算很少是精确的,只要超过精度表示范围就会产生误差。
解决方法:可以通过String
结合BigDecimal
或者通过使用long类型来转换。
查看默认垃圾收集器:-XX:+PrintCommandLineFlags
- Serial串行收集器:单线程收集器,只使用一个线程回收垃圾,需要停掉其他所有线程,Client模式下默认新生代垃圾收集器,新生代使用复制算法,老年代使用标记整理算法,Serial
Old也作为CMS收集器的后备垃圾收集方案。JVM参数:-XX:+UseSerialGC
- ParNew收集器:Serial的多线程版本,对应的JVM参数:-XX:+UseParNewGC。开启参数之后,会使用ParNew(新生代)复制算法+Serial
Old(老年代)标记整理算法的组合,Java8之后不再推荐使用这种组合。
-
Parallel scavenge收集器:新生代和老年代都使用并行,Parallel scavenge收集器可以使用自适应调节策略,把基本的内存数据设置好,然后设定是更关注最大停顿时间或者更关注吞吐量,给虚拟机设立一个优化目标。JVM参数是:-XX:+UseParallelGC。新生代使用复制算法,老年代使用标记-整理算法。
-
CMS收集器:一种以获取最短回收停顿时间为目标的收集器。JVM参数:-XX:+UseConcMarkSweepGC。使用ParNew(新生代)+CMS(老年代)+Serial
Old(后备)的收集器组合。优点是并发收集,停顿少。缺点是并发会造成CPU的压力,而且标记清除算法会产生大量空间碎片。
(1)初始标记:标记GC Roots能直接关联到的对象,速度很快,需要停顿。
(2)并发标记:进行GC Roots Trancing的过程,不需要停顿。
(3)重新标记:修正并发标记期间因为用户程序继续运作而导致变动的那一部分对象重新进行标记,需要停顿。
(4)并发清除:不需要停顿。
G1垃圾收集器:它使得Eden、Survivor和Tenured等内存区域不再连续,而变成一个个大小一样的region,每个region从1M到32M不等。它不再采用CMS的标记清理算法,G1整体上使用标记整理算法,局部上看是基于复制算法。JVM参数:-XX:+UseG1GC。
降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片内。是因为G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的region。
另:JVM设置参数的方法(win10):环境变量中新建变量JAVA_OPTS,在里面设置。
我们使用线程的时候去创建一个线程,这种方法非常简便,但是会导致一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁的创建线程会大大降低系统效率。
Java中引入了线程池来使得线程可以复用,执行完一个任务不会被立刻销毁,而是可以继续执行其他任务。
ThreadPoolExecutor类是线程池技术最核心的类:
其构造器中的参数意义
-
corePoolSize:
核心池大小。在创建线程池之后,默认线程池中是没有线程的,除非调用prestartAllCoreThreads()
或者prestartCoreThread()
方法来预创建线程,就是没有任务到来之前先创建corePoolSize
个线程。当线程池中的线程数目到达corePoolSize
个之后,就会把到达的任务放到缓存序列中。 -
maximumPoolSize
:非常重要的参数,表示线程池中最多能创建多少个线程。 -
keepAliveTime:
表示线程没有任务执行时最多保持多久会终止。 -
unit
:参数keepAliveTime
的时间单位。 -
workQueue:
阻塞队列,用来存储等待执行的任务,会对线程池的运行过程产生重大影响。有三个选择:ArrayBlockingQueue
、LinkedBlockingQueue
和SynchronousQueue
,一般使用后两者。 -
threadFactory
:线程工厂,主要用来创建线程。 -
handler
:表示拒绝处理任务的策略,有四种取值:
(1)ThreadPoolExecutor.AbortPolicy
:丢弃任务抛出RejectedExecutionException
异常;
(2)ThreadPoolExecutor.DiscardPolicy:
丢弃任务,不抛异常
(3)ThreadPoolExecutor.DiscardOldestPolicy
:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)
(4)ThreadPoolExecutor.CallRunsPolicy
:由调用线程处理该任务
ThreadPoolExecutor类的方法
execute()
和submit()
:都是提交任务,execute
方法用于提交不需要返回值的任务,无法判断任务是不是被线程池执行成功;submit
提交需要返回值的任务,线程池返回future
类型的对象以判断是否执行成功,future
对象具有的get()方法可以获取返回值。`
shutdown()
和shutdownNow()
:都是关闭线程池,他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt
方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow
首先将线程池的状态设置成STOP,然后尝试停止所有正在执行或者暂停的线程,并返回等待执行任务的列表;shutdown
只是将线程池的状态设置为SHUTDOWN
,然后中断所有没有执行任务的线程。
如何合理分配线程池的大小:CPU密集型任务,一般公式为:最大线程数 = CPU核数+1;IO密集型的最大线程数 = CPU核数 * 2;
实现一个线程池:
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println(“线程池中线程数目:”+executor.getPoolSize()+“,队列中等待执行的任务数目:”+
executor.getQueue().size()+“,已执行完别的任务数目:”+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
线程池不允许使用Executors的静态方法创建,必须通过ThreadPoolExecutor。
线程池的处理流程
当线程池提交一个任务的时候:
(1)线程池判断核心线程池中的线程是不是都在执行任务,如果不是则创建一个新的工作线程执行任务,否则进入流程(2)
(2)线程池判断工作队列是否已满,如果没有满则将新提交的任务存储在这个任务队列中,如果工作队列满了,则进入流程(3)
(3)线程池判断池中的线程是否都处在工作状态,如果没有则创建一个新的工作线程来执行任务,如果已经满了就交给拒绝策略(handler)来处理任务。
四种线程池:
(1)newCachedThreadPool 创建一个可以缓存的线程池。
(2)newFixedThreadPool 创建一个定长线程池,可以控制线程最大并发数。
(3)newScheduledThreadPool 创建一个定长线程池,支持定时和周期性任务执行。
(4)newSingleThreadExecutor 创建一个单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
//可以缓存的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); //需要指定长度
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
线程同步的五种方式:synchronized
的关键字修饰方法、静态资源或者代码块;Lock
(必须放在try-catch-finally
中执行,finally
释放锁以防止死锁);wait
和notify
,必须在synchronized
范围内,被synchronized
锁住的对象就是wait和notify
的调用对象;CAS;信号量(Semaphore
)。
线程通讯的方式:
-
(1)
wait()、notify()、nofityAll()
:等待/通知机制。线程A调用了对象O的wait
方法进入等待状态,另一个线程B调用了对象O的notify
或notifyAll
方法,线程A收到通知之后,从对象O的wait
方法中返回执行后续操作。调用对象的wait方法会导致线程阻塞,释放该对象的锁;调用对象的notify方法会随机解除该对象阻塞的线程,该线程重新尝试获取该对象的锁;从wait方法返回的前提是获得了调用对象的锁;必须在synchronized
块或方法中使用。 -
(2)
condition
:Condition
用await(),signal
,singalAll
方法代替wait和notify
。notify
只能随机唤醒一个线程,但是用condition
可以唤醒指定线程。 -
(3)管道
-
(4)volatile
-
(5)
Thread.join
:如果一个线程执行了Thread.join()
,意味着当前线程A等待thread线程中止之后才从thread.join()
返回。
调用一个线程的interrupt()
方法来中断线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出InterruptedException
,从而提前结束该线程。
如果线程的run()执行一个死循环,并且没有执行sleep()等会抛出InterruptedException
的操作,那么调用interrupt()
方法无法使线程提前结束。但是调用interrupt
方法会设置线程的中断标记,此时调用Thread.interrupted()
或Thread.currentThread().isInterrupted()
方法会返回true。因此可以在循环体中使用interrupted()方法判断线程是否处于中断状态,从而提前结束线程。
线程安全是Java并发编程中的重点,造成线程安全问题主要有两个原因:一是存在共享数据,二是存在多条线程共同操作共享数据。因此,当存在多个线程操作共享数据的时候,需要保证同一时刻有且只有线程在操作共享数据,其他线程必须等到该线程处理完才能进行,这种方式叫做互斥锁。Java中,关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,同时它还可以保证一个线程(共享数据)的变化被其他线程所看到(可见性保证,完全可以替代Volatile功能)
synchronized是Java的关键字,是一种同步锁。
Java的内置锁(synchronized):每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,退出同步代码块的时候会释放该锁。获得内置锁的唯一途径就是进入锁保护的同步代码块/方法。
Java的对象锁和类锁:在锁的概念上与内置锁一致,但对象锁是用于对象实例方法或对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。
Java中每个对象都有一把锁和两个队列,一个队列用于挂起未获得锁的线程,一个队列用于挂起条件不满足而等待的线程。synchronized实际上是一个加锁和释放锁的集成。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,计数归零。线程第一次给对象加锁的时候,计数变成1。每当这个相同的线程在此对象上获得锁的时候,计数就会递增。每当任务离开一个synchronized方法,计数就会递减,为0的时候锁被完全释放。
Synchronized有三种应用方式:
修饰一个实例方法:被修饰的方法称为实例同步方法,其作用范围是整个方法,锁定的事该方法所属的对象(调用该方法的对象)。所有需要获得该对象锁的操作都会对该对象加锁。
public synchronized void method(){}
//等同于
public void method(){
synchronized(this){
}
}
如果一个对象有多个synchronized
方法,只要一个线程访问了其中的一个synchronized
方法,其他线程不能同时访问这个对象中任何一个synchronized
方法。
当一个对象O1在不同的线程中执行这个同步方法的时候,会形成互斥。但是O1对象所属类的另一对象O2是可以调用这个被加了synchronized
关键字的方法的。其他线程调用O2中的相同方法时不会造成同步阻塞。程序可能在这种情况下摆脱同步机制的控制,造成数据混乱。注意:
-
(1)
synchronized
关键字不会被继承:子类覆盖父类带synchronized
方法的时候,必须也要给子类的这个方法显式的增加synchronized关键字。 -
(2)定义接口的时候不能使用synchronized关键字。
-
(3)构造方法不能使用synchronized关键字,但可以使用synchronized代码块完成同步。
修饰一个静态方法:被修饰的方法被称为静态同步方法,其作用域是整个静态方法,锁是静态方法所属的类。
public synchronized static void method(){}
修饰代码块:被修饰的代码块被称为同步语句块。synchronized的括号中必须传入一个对象作为锁,作用范围是大括号中的代码,锁是synchronized括号中的内容,可以分为类锁和对象锁
//锁对象为实例对象
public void method(Object o){
synchronized(o){
…
}
}//加入Java开发交流君样:756584822一起吹水聊天
//锁对象为类的Class对象
public class Demo{
public static void method(){
synchronized(Demo.class){
…
}
}
}
实际上是通过monitor
(监视器)。Java中的同步代码块是使用monitorenter
和monitorexit
指令实现的,其中monitorenter
指令插入到同步代码块的开始位置,monitorexit
指令插入同步代码块的结束位置。
JVM保证这两个指令成对出现。
当执行monitorenter
指令的时候,线程试图获取锁也就是获取monitor对象
的所有权,当计数器为0的时候就可以成功获取,获取后将计数器加一。在执行monitorexit
指令之后,将锁计数器减一,表明锁被释放。
synchronized
修饰方法的时候,没有monitorenter
和monitorexit
指令,取而代之的是ACC_SYNCHRONIZED
标识,这个标识指明这个方法是一个同步方法。
无锁–>偏向锁–>轻量级锁–>重量级锁(过程不可逆)
偏向锁:大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得;如果一个线程获得了锁,锁进入偏向模式,此时对象头的Mark Word
结构也变为偏向锁结构。
对象头在第十章节中提到过,另外这篇文章讲的更详细。
当该线程再次请求锁的时候,只需要检查Mark Word
锁标记为是否为偏向锁,以及当前线程ID是不是等于Mark Word
的Thread Id即可,省去了大量有关锁申请的操作。
偏向锁只适用于只有一个线程访问同步块的场景。
轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。适用于追求响应时间,同步快执行速度非常快的情况。
代码在进入同步块的时候,如果同步对象锁状态是无锁,虚拟机首先在当前线程的栈帧中创建锁记录(Lock Record)
空间,拷贝对象头的Mark Word
复制到锁记录中。
之后虚拟机使用CAS操作尝试将对象的Mark Word
更新为指向Lock Record
的指针,并将Lock Record
的owner指针指向对象的Mark Word。如果这个动作成功了,那么这个线程就有了该对象的锁,对象的锁标记为设置为“00”,说明处于轻量级锁定状态。
如果这个动作失败了,JVM检查对象的Mark Word是否指向当前线程的栈帧,是则说明当前线程已经拥有了这个对象的锁,否则说明多个线程竞争锁。
如果有两个以上的线程竞争同一个锁,轻量级锁不再有效,膨胀为重量级锁。
重量级锁:多线程情况,线程阻塞响应时间缓慢,频繁的释放获取锁会带来巨大的性能损耗。适用于追求吞吐量,同步快执行速度较长的情景。
29.Synchronized与重入锁ReentrantLock的区别
相对与ReentrantLock而言,synchronized锁是重量级的,而且是内置锁,意味着JVM可以对synchronized锁做优化。
在synchronized锁上阻塞的线程是不可中断的,而ReentrantLock锁实现了可中断的阻塞。
synchronized锁释放是自动的,而ReentrantLock需要显式释放(在try-finally块中释放)\
线程在竞争synchronized锁的时候是非公平的:如果synchronized锁被线程A占有,线程B请求失败,被放入队列中,线程C此时来请求锁,恰好A在此时释放了,线程C会跳过队列中等待的线程B直接获得这个锁。但是ReentrantLock可以实现锁的公平性。
synchronized锁是读写和读读都互斥,ReentrankWriteLock分为读锁和写锁,读锁可以同时被多个线程持有,适合于读多写少的并发场景。
ReentrantLock只能锁代码块,但是synchronized可以锁方法和类。ReentrantLock可以知道线程有没有拿到锁,但是synchronized不行。
在28章节中,我们提到过重量级锁,在重量级锁中,JVM会阻塞未获取到锁的线程,在锁被释放的时候唤醒这些线程,阻塞和唤醒依赖于操作系统,需要从用户态切换到内核态,开销很大。monitor调用了OS底层的互斥量(mutex),切换成本很高。因此JVM引入了自旋的概念。
自旋锁与自适应自旋锁,CAS实现:
-
自旋锁:很多情况下,共享数据的锁定状态持续时间短,切换线程不值得;通过让线程执行忙循环等待锁的释放,不让出CPU,缺点是如果锁被其他线程长时间占用,带来很多开销。
-
自适应自旋锁:自旋的次数不固定,由前一次在同一个锁上的自旋时间和锁的拥有者状态来决定。
-
优点:自旋锁不会使线程状态发生改变,一直处于用户态,不会使线程阻塞,执行速度快。
-
CAS(Compare And Swap) 乐观锁与悲观锁:synchronized操作就是悲观锁,这种情况线程一旦得到锁,其他需要锁的线程就挂起的情况是悲观锁;CAS操作实际上是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果失败了就重试,直到成功为止。悲观在认为程序中的并发情况严重,乐观在于并发情况不那么严重,可以多次尝试。
-
锁消除:虚拟机在即时编译器运行时,对一些代码上要求同步而被检测到实际不可能存在共享数据竞争的锁进行消除。依据是:JVM会判断一段程序中的同步明显不会逃逸出去从而被其他线程访问,JVM就把它们当作栈上的数据对待,认为这些数据是线程独有的。
-
锁粗化:在加同步锁的时候,我们尽量的把同步块的作用范围限制到尽量小的范围。但是如果存在一连串的操作都对同一个对象反复加锁解锁,甚至加锁出现在循环体内,即使没有线程竞争,频繁的进行互斥同步也会导致消耗。
public static String test04(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
上述连续的append操作就属于这类情况,jvm检测到一连串操作都是对同一个对象加锁,就会把锁同步范围扩展(粗化)到整个一系列操作的外部,使得一连串append操作只需要加一次锁就可以了。
设计模式是一套被反复使用,多数人知晓的,经过分类编目的,代码设计经验的总结。使用设计模式是为了可重用代码,让代码更容易被他人理解。实际上就是在某些场景下,针对某类问题的某种通用的解决方案。
设计模式分为三类:
-
(1)创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。包括单例模式、简单工厂、抽象工厂等。
-
(2)结构型模式:把类和对象结合在一起形成一个更大的结构。包括适配器模式、组合模式、装饰模式等。
-
(3)行为型模式:类和对象如何交互、及划分责任和算法。包括模板模式、解释器模式、观察者模式等。
单例模式:属于创建型模式,主要有三种写法:懒汉式、饿汉式和登记式。
单例模式的特点:
-
(1)单例类只能有一个实例
-
(2)单例类必须自己创建自己的唯一实例
-
(3)单例类必须给所有其他对象提供这一实例
懒汉式:在第一次调用的时候就实例化自己。
public class Singleton{
private Singleton(){}
private static Singleton single = null;
//静态工厂方法
private static Singleton getInstance(){
if(single == null) single = new Singleton();
}
return single;
}
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
等。
-
(2)结构型模式:把类和对象结合在一起形成一个更大的结构。包括适配器模式、组合模式、装饰模式等。
-
(3)行为型模式:类和对象如何交互、及划分责任和算法。包括模板模式、解释器模式、观察者模式等。
单例模式:属于创建型模式,主要有三种写法:懒汉式、饿汉式和登记式。
单例模式的特点:
-
(1)单例类只能有一个实例
-
(2)单例类必须自己创建自己的唯一实例
-
(3)单例类必须给所有其他对象提供这一实例
懒汉式:在第一次调用的时候就实例化自己。
public class Singleton{
private Singleton(){}
private static Singleton single = null;
//静态工厂方法
private static Singleton getInstance(){
if(single == null) single = new Singleton();
}
return single;
}
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-GaLIfTdW-1715739762200)]
[外链图片转存中…(img-gRb6js1L-1715739762200)]
[外链图片转存中…(img-V9OzqPLC-1715739762200)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!