前言
在java
知识体系中,多线程之间的数据同步机制在面试中经常被问到,因为在工作中用的也不频繁,很容易当时理解了,后面不用又忘记了,特别是各种锁的特点,很容易搞乱,这里做一个系统的梳理和总结,以便加深记忆。
Synchronized
Synchronized
即同步锁,可以修饰方法和代码块,根据情况不同可以分为方法锁,对象锁和类锁。
方法锁
synchronized
修饰方法时,称之为方法锁,如果此方法为非静态方法,也可称为对象锁,因为在同一对象实例下,两个线程同时访问此方法会共用一个锁。示例代码如下:
arduino
public class CLock{
//非静态方法锁,使用的是对象锁。相同的CLock实例访问同一方法共用一个锁。不同实例的话无论是相同方法还是不同方法用的都不是一个锁。
public void synchronized test1(){
}
//此方法也是非静态方法锁,在同一对象实例下,和test1共用一个对象,也就共用一把锁
public void synchronized test2(){
}
}
对象锁
对象锁不是说synchronized
修饰在对象之上,而是指synchronized
修饰的非静态方法或者代码块上,当不同线程访问的时候,共用的也是一把锁,称之为对象锁。示例代码如下:
typescript
public class CLock{
//这里是非静态方法锁,也称之为对象锁
public void synchronized test1(){}
public void synchronized test2(){}
public void test3(){
//非静态方法种的代码块,如果用this来加锁,也是对象锁。和上面的非静态方法使用同一个锁。
synchronized(this){}
}
}
类锁
类锁不是指synchronized
修饰在类之上,而是在概念上理解锁住一个类,synchronized
修饰在静态方法上或者代码块中如果用.class
类来加锁,则代表此锁是类锁,也就是这个类无论建多少对象实例,在此类下访问用synchronized
修饰的静态方法时用的都是同一个锁。
这里可以看出和对象锁的区别,对象锁是在同一对象下共用锁,在多个对象中是各自有独立的锁,这样理解就可以和类锁很好区分了。代码示例如下:
typescript
public class CLock{
//静态方法锁,使用的是类锁。不同实例下,多个线程访问这两个方法也是共用一个锁
public static synchronized void test1(){ }
public static synchronized void test2(){}
public void test3(){
//非静态方法种的代码块,如果用.class类来加锁,也是类锁。和上面的静态方法使用同一个锁。
synchronized(CLock.class){}
}
}
ReentrantLock
ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。示例代码如下:
scss
public class A {
public static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
testSync();
}, "t1").start();
new Thread(() -> {
testSync();
}, "t2").start();
}
public static void testSync(){
//加锁更加灵活,可以在指定位置加锁
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
ReentrantLock与Synchronized对比
Synchronized
是独占锁,加锁和解锁过程自己进行,易于操作,但不够灵活,ReentrantLock
也是独占锁,加锁和解锁需手动进行,不易操作,但非常灵活Synchronized
可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。Synchronized
不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。Synchronized
不能实现公平锁,ReentrantLock
可以实现公平锁。
volatile
-
volatile属于一种轻量级的同步机制,保证了可见性和有序性(不保证原子性),如果一个共享变量被volatile关键字修饰,那么线程A要修改此变量,修改结果会立即刷新到主存中.线程B的共享变量缓存就会失效,需要重新从主内存重新读取最新值。
-
volatile通过内存屏障禁止指令重排序优化,保证了有序性。
-
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令
单例模式的双重锁为什么要加volatile
typescript
public class TestInstance{
private volatile static TestInstance instance;
public static TestInstance getInstance(){ //1
if(instance == null){ //2
synchronized(TestInstance.class){ //3
if(instance == null){ //4
instance = new TestInstance(); //5
}
}
}
return instance; //6
}
}
需要volatile
关键字的原因是;在并发情况下;如果没有volatile
关键字;在第5行会出现问题。instance = new TestInstance();
可以分解为3行伪代码
scss
a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时;如果没有使用volatile
修饰可能会出现重排序从a-b-c
排序为a-c-b
。在多线程的情况下会出现以下问题。当线程A
在执行第5行代码时;B
线程进来执行到第2行代码。假设此时A
执行的过程中发生了指令重排序;即先执行了a
和c
;没有执行b
。那么由于A
线程执行了c
导致instance
指向了一段地址;所以B线程判断instance
不为null
;会直接跳到第6行并返回一个未初始化的对象。
知识拓展
公平锁
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁
JVM
随机就近原则分配锁的机制则称为非公平锁,非公平锁实际执行的效率要远超公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护等待队列 Java
中的 synchronized
是非公平锁,ReentrantLock
默认的 lock()
方法采用的是非公平锁,也可以在创建ReentrantLock
对象时指定构造函数参数来设置非公平锁
可重入锁(递归锁)
指的是以线程为单位,当一个线程获取对象锁后,这个线程可以再次获取对象上的锁,而其他线程是不可以的 , ReentrantLock 和 synchronized 都是 可重入锁
死锁
死锁是指两个或多个线程在执行过程中,因为争夺资源而相互等待,导致它们都进入停滞状态的现象,在Java中,这通常发生在多个线程尝试以不同的顺序获取相同的锁时。
避免死锁的策略
- 锁顺序:最基本的一条规则是:总是以固定的顺序获取锁。
- 锁超时:另一个策略是使用锁超时。这意味着线程在尝试获取锁时不会无限等待。
- 使用并发工具类:最后,Java并发API提供了一些高级工具,比如
java.util.concurrent
包中的类,可以帮助咱们更好地管理锁和避免死锁。例如,Semaphore
可以用来控制对资源的并发访问数,而CountDownLatch
和CyclicBarrier
可以用于线程间的同步。