是什么

Java中的关键字,主要用来加锁

怎么用

不管怎么用,最终锁的都是对象

1
2
3
4
5
6
7
//加到方法上
synchronized static f()锁的是类的class对象
synchronized void f() 锁的是类的实例对象
//代码块
synchronized(){

}

管程

即monitor(管程or监视器),用来实现多个线程对统同一资源的互斥访问

在Java中,每个对象都有Monitor,Monitor伴随Java对象一身

在hotsopt虚拟机中,monitor的实现为ObjectMonitor。
ObjectMonitor是基于c++来实现的,它有几个重要属性:

1
2
3
4
5
6
7
8
9
_owner:指向持有 ObjectMonitor 对象的线程

_WaitSet:存放处于 wait 状态的线程队列

_EntryList:存放处于等待锁 block 状态的线程队列

_recursions:锁的重入次数(控制可重入次数的)

_count:用来记录该线程获取锁的次数 (辅助判断锁的整体活跃度、竞争情况)

对象头的mark word

和ObjectMonitor的关系?

如果一个Java对象被线程持有,那么这个Java对象的对象头中的mark word的Lock word就会指向
ObjectMonitor的起始地址

重要级锁

在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter和exit方法,这种锁被称为

重量级锁。主要实现是通过ObjectMonitor的关键属性来实现的,owner,EntryList,waitset,count

当多个线程同时访问同一段同步代码时,会先进入EntryList队列,当其中某个线程获取得到对象的Monitor后

进入owner区域并把Monitor中的_owner变量设置为当前线程,同时把Monitor中的计数器_count+1 即获得对

像锁。而其他线程这时候来获取锁,获取不到就处于变为阻塞状态。

当持有Monitor的线程调用了wait()方法后,那么他就会释放当前的Monitor,并将owner变量置为null,

count–,同时该线程进入waitset集合中等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值

,以便其他线程进入获取monitor,这时候会去唤醒EntryList中的其他线程

monitor依赖于操作系统的mutexLock

为什么说他重?

Java的线程模型默认是一个用户线程对应着操作系统的一个内核线程。那么在synchronized中涉及到大

线程的阻塞和唤醒,这些都是从用户态切换到内核态来实现的,而这个切换操作在早期CPU是很耗时的

偏向锁

在JDK1.6引入,在JDK13默认为关闭了,JDK 15 完全禁用,JDK 17彻底移除代码

正确地来说,它并不是一个真正的锁。当第一个线程抢到锁时,会在java锁对象中的对象头的markword里填

入自己的线程名字。下个线程还获取时就会进行一个判断(这个判断操作是很轻的),如果还是同一个的话,

就会直接获取。不是的话,就得进行一个锁升级了

匿名偏向?

还没有人去持有偏向锁,这时候锁就是匿名的

为什么要有这个锁?

因为在通过Java开发团队的大量统计,其实在开发中很多代码都是同一个线程在执行

为什么偏向级锁不要设置为一开始就启动,默认是JVM启动后4s才会开启

如果一开始就明确就是多线程环境,那么偏向锁还有什么意义?JVM启动涉及到10多个线程,本身就是

多线程环境

为什么JDK 15要废弃偏向锁?

早期很多集合都是一把synchronized梭哈,比如vector,HashTable。不可否认,偏向锁能保证这些老集

合在单线程使用的环境下的性能,但后来随着HashMap,ArrayLIst等集合的出现。偏向锁变得不是很

重要了

而且官方还说偏向锁的引入导致代码的复杂度升高了,不好维护

微博禁用偏向锁,性能直接飙升

偏向锁这种东西会让对象迟迟无法被回收,导致对象一直越来越多,STW时间变长

锁撤销

轻量级锁

它的出现是为了优化重量级锁在竞争少的场景下的开销大的问题

当另外一个线程获取非匿名偏向锁时,偏向锁就会被撤销,锁就会自动升级为轻量级锁

轻量级锁状态时,JVM为锁对象的对象头markword预留了一部分空间,用来存储指向线程栈中Lock record

的指针

当一个线程尝试获取轻量级锁时(即发现对象头里的锁标记位为 00),就在线程虚拟机栈中开辟一快空间,

即Lock record,里面有两个部分

然后尝试通过CAS操作将对象头的mark word更新为指向锁记录的指针

自适应自旋?

锁膨胀

发生在轻量级—> 重量级锁

锁升级

锁升级过程

synchronized 的锁能降级吗?

对于HotSpot虚拟机来说,是没办法的。即一旦锁升级为重量级锁后,你的锁状态就会一直维持重量级锁,直到释放。

不过还有一种特殊的“降级”情况,即重量级锁的monitor对象不再被任何线程持有时,被清理和回收的过程。

JDK6对synchronized的优化?

自旋锁

JDK1.4引入,JDK1.6默认开启

锁消除

JIT层面的优化,即在使用synchronized的时候,如果JIT经过逃逸分析后发现并无线程安全的问题的话,

就会做锁消除

锁粗化

如果在一个循环里,频繁地获取资源释放资源,这样带来的消耗会很大,锁粗化会扩大锁的范围,把加锁

逻辑放到外面。

ps:锁粗化和“平时我们在开放中要尽可能地减少锁的粒度”矛盾吗?

不矛盾

锁升级

JDK1.6引入了偏向锁,轻量级锁。当线程竞争不激烈时,可以减少性能开销

Synchronized的缺点

无法知道线程是否获取到了锁

锁只有阻塞状态

是什么?

线程本地变量,可以实现线程之间的隔离,线程内部共享。我们平时都是在类中作为私有静态变量来使用的,目的是为了让一些状态(用户ID,事务ID)与线程进行关联。

有什么用

实现每个线程内都有一份专属的本地变量副本。

实现线程之间隔离,线程内共享

共享这个更重要,有人就要问,线程内共享,难道直接传参就好了吗,但你想想这样不丑陋吗,如果你的方法调用链很长的花,例如Spring的事务机制,我们都知道Spring事务支持嵌套,如果你的事务嵌套10层,那你不是每个方法都要加一个 Connection 参数?那不丑陋死了

API

方法 作用
initialValue() 定义初始值,默认null,可以重写
get() 拿当前线程自己的值,没有就用initialValue()
set(value) 给当前线程自己设置一个新值
remove() 删除当前线程自己的值,防内存泄漏

原理

ThreadLocal,Thread和ThreadLocalMap的关系

当我们在往ThreadLocal里set值时,其实是往Thread的ThreadLocalMap里set值,key为ThreadLocal实例(弱引用),而value为我们set的值。为什么这就是线程安全的了?原因是ThreadLocalMap是Thread里维护的一个变量,可以说是线程私有的。

ThreadLocal可以认为是 ThreadLocalMap的封装,传递了变量值,而且它本身并不存储值,而是把自己作为一个key,来让线程从ThreadLocalMap获取得到值

1
2
3
4
5
6
class ServiceA{
private static ThreadLocal<xx> t=new ThreadLocal();
public void methodA(){
t.set("xx");
}
}

ThreadLocal内存泄漏问题

内存泄漏是什么? :

不再使用的内存不能被回收

为什么Entry的key要为弱引用,不用如何?

主要是为了能让ThreadLocal对象能够回收掉。若这个key为强引用,那么会导致key指向的ThreadLocal对象,以及value对象无法被GC。若为弱引用,key所指向的ThreadLocal就可以被回收且Entry指向的对象为null,减少内存泄漏概率

  但这单单不够,value为强引用,且引用链依然存在,仍然无法被GC。

其实ThreadLocal内存泄漏的说法只存在于线程池的情况,比如tomcat线程池,它会复用线程,导致你的线程一直存活。没有线程复用的化,线程执行后立马销毁了,那其实啥事都没有了

如何做?

手动remove,但其实但key为null后,我们再次去get,set也会自动清空

最后重中之重!!!

是否会内存泄露不是取决于是否remove,而是取决于怎么用。平时使用我们都是将ThreadLocal声明为一个

static对象,那这样的话它的生命周期就会和容器保持一样,那么它就不可能被回收掉,也就是这时候key是

个强引用,我们说的内存泄露主要是说我们无法把value置为null了,而这种情况下是不会出现的,因为key不

为null,我们还是可以通过key拿到我们的value的。那不正常情况,那就是一些傻子会把ThreadLocal当做局

部变量使用,这种情况就会出现随着线程结束,出现key也就是ThreadLocal被回收,因为它是局部变量么。

那么也就是key没了,但value还在,而这个kv(Entry)是存于ThreadLocalMap里的,而ThreadLocalMap是

和线程强相关的,而在tomcat线程里,有些核心线程是一直存在,ThreadLocalMap无法被回收掉。也就导

致了内存泄露

ThreadLocalMap如何解决哈希冲突?

开放寻址法(当发生Hash冲突时,则加1向后寻址,直到找到空位置或者被垃圾回收的位置),不同于HashMap采用的拉链法,也就是说ThreadLocalMap没有采用链表结构存储。而且在此期间还会清理掉key为null的Entry

Hash表的默认大小为 10,扩容阈值为 len*2/3 也就是10,大于10就扩容,扩容两倍

InheritableThreadLocal

可以解决父子线程间ThreadLocal无法共享的问题

它的原理是 子线程是在父线程中通过new Thread()创建的,在Thread构造方法中调用init,在init方法中父线程的数据会传递到子线程

但我们平常开发中异步都是配合线程池来使用的,这种问题还是无法解决

TransmitThreadLocal?

在ThreadLocal基础上进行了增强,增加了值拷贝和传递的功能

不同于 InheritableThreadLocal, TTL管它什么新线程/线程池,它自己控制拷贝和恢复

新一代ThreadLocal?ScopedValue

这东西鉴于ThreadLocal的缺点

线程创建的方式

  1. new Thread,重写run方法
  2. 实现Runnable接口,重写run方法
  3. 实现Callable接口,重写call方法
  4. 通过线程池方式创建

底层的来说就一种 即通过thread.start()创建并开启线程

线程状态

new:线程创建了但没start

反复调用线程的start是否可行?线程执行完毕再次start是否可行?都不行,因为start后threadStatus会改变,再次调用start方法会抛IllegalThreadStateException异常

runnable:线程正在执行代码中,也可能正在等待cpu调度

blocked:阻塞等待锁中

waiting:等待状态,有三种可能:

Object.wait():使线程变为等待中,直到其他线程唤醒它

Thread.jion():等待线程执行完毕,底层其实是Object.wait()

LockSupport.park():

timed_waiting:超时等待,时间到了自动觉醒,有以下可能

wait(time)

Thread.sleep(time)

jion(time)

parkNacos(time)

terminated:终止状态,表示线程已执行完毕

线程优先级

thread.setPriority() 设置线程优先级,分为1-10 默认5。哈哈哈,只是建议,低的不比高的后

多线程带来的问题

多线程之间,单个线程执行的流程可能不是原子性的,途中可能会有其他线程影响

多个线程之间,数据的可见性可能不是实时的,可能导致数据对不上

要解决以上问题,就得引入多线程 同步机制,例如加锁。但这又会引入新的问题,如死锁,锁开销,锁饥饿

多线程还会带来线程上下文切换的开销

线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。

如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

线程存活

isAlive() 可判断线程是否是存活

虚假唤醒

java 线程之间是如何通信的

Java中线程通信方式七种_java线程间通信的方式-CSDN博客

总的来说,线程之间通信有三种模型,一种是共享内存的形式,一种是消息传递的方式(也叫等待-通知),一种是

管道流。在java中一共有八种,volatile,synchronized,Interrupt,wait,notify,notifyAll,jion,管道通信

实现管道通信,我们可以使用jdk自带的PipedOutputStream和PipedInputStream,或者PipedWriter,PipedReader

看过八股的都知道volatile可以保证有序性和可见性,但其实他还保证了部分原子性

有序性

简单地说就是防止指令重排,保证代码按照程序员的意愿执行

它的实现是通过内存屏障来实现的。硬件层面有Load Barrier读屏障和Store Barrier写屏障 。而jvm层面有

LoadLoad(读读屏障),LoadStore(读写屏障),StoreStore(写写屏障),StoreLoad(读写屏障)。

内存屏障相当于一堵墙,墙两边的指令不能翻墙重排,而墙同侧的可以发生重排

eg:指令1 指令2 | 指令3 指令4

指令2和指令3不可以重排,即强制执行完指令2后才会执行指令3

指令1和指令2可以重排

内存屏障到底是什么?

本质上就是CPU指令

可见性

一个线程修改了volatile变量,这个修改对于其他线程立即可见

因为CPU运算速度要比内存块很多,所以会把主存的值缓存到高速缓存中。而这时线程的高速缓存可能就会

出现和主存不一致的情况。而volatile就可以解决这种情况

当写一个volatile变量时,写操作完成后会多出一条Lock为前缀的汇编指令,这指令在多核处理器下会做两件

1.将当前CPU缓存里的值写回主存

2.将其他CPU核的内存置为无效,读取必须去主存读

当读一个volatile变量时,会直接去主存里读

这样就可以保证其变量在多线程下的可见性了

部分原子性

关键字: volatile详解

它的原子性体现在赋值层面,在30位的操作系统,cpu只能一次性读写32位的数据,那么对于long类型的,也就只能分为两步,即高32位,低32位。如果在多线程中,一个线程只操作了前部分,而另外一个线程来操作这个变量,这样的话就发生了错误了么。那如果是被volatile修饰就不会发生这个问题。但自增自减就无法保证其原子性,因为这些操作都不是由单条字节码指令组成的

双重检验锁volatile真正的作用?

DCL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestSingleton implements Serializable {
private static TestSingleton INSTANCE;
/**
* 防止反射多个对象
*/
private TestSingleton(){
if (INSTANCE!=null) {
throw new RuntimeException("singleton");
}else{
INSTANCE=this;
}
}
public static TestSingleton getInstance(){
if (INSTANCE==null) {
synchronized(TestSingleton.class){
if (INSTANCE==null) {
INSTANCE=new TestSingleton();
}
}
}
return INSTANCE;
}
/**
* 防止序列化多个对象
* @return
*/
private Object readResolve(){
return INSTANCE;
}
}

网上有很多说法说对它是能防止new操作的指令重排序,然后并不是的,new操作分为三步(分配内存,对象初始化,指针指向),而volatile只能保证单个指令的可见性,有序性,原子性,i++这种也是无法保证的。它真正的作用是通过在new操作前加入了store_store屏障,在后加入store_load屏障。保证对INSTANCE的写是要优先于对它的读的,但其new的内部是无法保证有序性的,从而避免了有线程获得还没有初始化的实例

它是怎么实现不可变性的?

首先String类被final修饰,那就意味着它不能被继承。那么他里面的方法就不能被覆盖的

用final去修饰字符串内容的char,保证其地址不可变

无对外暴露字符串的setter方法,可以在一定程度上保证内容不可变,像substring,concat这种API,其

实是new 了一个String然后返回的。

ps:为什么说 一定程度呢,因为如果你通过反射或者unsafe类还是可以修改的

如果我们要实现一个可变的字符串,我们可以使用StringBuffer和StringBuilder

为什么要为不可变

我觉得是出于安全性的考量,String的用途很广泛。因为你不可变,也就意味着可以避免线程安全的问题

而且不可变的话,Hash值就定下来了,而且Hash值是存储在对象头里的,String不断Hash,不可变就可 以复用Hash值,提高效率(因为你Hashcode你根据字符串内容生成的)

Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?

byte数组占用内存空间更小吧。java9之前采用char[],由于java内部使用的编码是UTF16,这就导致即

有些字符可以用1个字节表示,但它还是会占两个字节。

因此java9对他进行了优化

通过byte[](一个byte 8位)和字段coder来控制字符的存储。如果字符串的编码没超过lan-1的范围 (纯英文)就用lan-1,每个字符就占1个字节(8位)而对于超过的话,那就统一采用UTF6,不管中英文来 进行一个存储,每个字符就两个字节(16位)。

String的长度有限制吗

编译期,受限制;运行期,受int最大长度限制

缓存(建议)

消息队列(不建议)

延迟消息(不建议)

排行榜(建议)

计数器(建议)

分布式ID(可以)

分布式锁(建议)

地理位置应用(建议)

分布式限流(可以)

分布式Session(建议)

布隆过滤器(建议)

bitmap状态统计(可以)

共同关注(建议)

推荐关注(可以)

客户端超时阻塞

引发网络阻塞

内存占用不均

持久化的影响

执行命令都是单线程的

redis2.6后会启动两个后台线程,负责关闭文件andAOF刷盘

redis4.0后引入了多线程,去执行一些比较缓慢的操作,如删除bigkey

redis6.0后引入了多线程网络模型,默认是关闭

2.4已经丢弃

它是指redis一开始为了解决物理内存限制的问题,会把一些不常用的数据刷到磁盘里,腾空间给常用的数据

但有性能问题,因为多了刷盘时产生的IO

DELETE 语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。

TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。

DROP语句将表所占用的空间全释放掉。

在速度上,一般来说,drop> truncate > delete。

如果想删除部分数据用 delete,注意带上 where 子句,回滚段要足够大;

如果想删除表,当然用 drop; 如果想保留表而将所有数据删除,如果和事务无关,用 truncate 即可;

如果和事务有关,或者想触发 trigger,还是用 delete;

如果是整理表内部的碎片,可以用 truncate 跟上 reuse stroage,再重新导入/插入数据

整理表内部的碎片

方法 实现原理 操作流程 / 示例 优缺点 适用场景
TRUNCATE + INSERT 删除表数据页,重建表结构;InnoDB 会重用原来分配的页 sql -- 备份表 CREATE TABLE t_backup AS SELECT * FROM t; -- 清空表 TRUNCATE TABLE t; -- 重新插入数据 INSERT INTO t SELECT * FROM t_backup; 快速、简单;但会隐式提交事务,删除数据风险大 小表 / 可停机维护
OPTIMIZE TABLE InnoDB 创建临时表,将原表数据复制过去 → 连续页存储 → 删除原表 → 重命名临时表 sql OPTIMIZE TABLE t; 无需手动备份,操作简单;大表占用临时空间多 中小表、定期维护
pt-online-schema-change 在线重建表 → 数据复制到新表 → 最后切换表名 bash pt-online-schema-change --alter "ENGINE=InnoDB" D=mydb,t=t --execute 不锁表,在线整理大表;需要额外工具 大表、在线环境