文章目录
- 1. synchronized与ReentrantLock的区别
- 2. synchronized的作用
- 3. synchronized的使用
- 3.1 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
- 3.2 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
- 3.3 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁
- 4. 分析代码是否互斥
- 5. synchronized的可重入性
- 6. 发生异常synchronized会释放锁
- 7. synchronized的实现原理与应用(包含锁的升级过程)
1. synchronized与ReentrantLock的区别
区别点 | synchronized | ReentrantLock |
---|---|---|
是什么? | 关键字,是 JVM 层面通过监视器实现的 | 类,基于 AQS 实现的 |
公平锁与否? | 非公平锁 | 支持公平锁和非公平锁,默认非公平锁 |
获取当前线程是否上锁 | 无 | 可以(isHeldByCurrentThread()) |
条件变量 | 无 | 支持条件变量(newCondition()) |
异常处理 | 在 synchronized 块中发生异常,锁会自动释放 | 在 ReentrantLock 中没有在 finally 块中正确地调用 unlock() 方法,则可能会导致死锁 |
灵活性1 | 自动加锁和释放锁 | 手动加锁和释放锁 |
灵活性2 | 无 | 允许尝试去获取锁而不阻塞(如 tryLock 方法),并且可以指定获取锁等待的时间(如 tryLock(long time, TimeUnit unit))。 |
可中断性 | 不可中断,除非发生了异常 | 允许线程中断另一个持有锁的线程,这样持有锁的线程可以选择放弃锁并响应中断。1.tryLock(long timeout, TimeUnit unit);2.lockInterruptibly()和interrupt()配合使用 |
锁的内容 | 对象,锁信息保存在对象头中 | int类型的变量来标识锁的状态:private volatile int state; |
锁升级过程 | 无锁->偏向锁->轻量级锁->重量级锁 | 无 |
使用位置 | 普通方法、静态方法、代码块 | 代码块(方法里的代码,初始化块都是代码块) |
2. synchronized的作用
- 在Java中,使用synchronized关键字可以确保任何时刻只有一个线程可以执行特定的方法或者代码块。这有助于防止数据竞争条件(race conditions)和其他由于线程间共享资源而产生的问题。
- 当一个方法或代码块被声明为synchronized,它意味着在该方法或代码块执行期间,其他试图获得相同锁的线程将被阻塞,直到持有锁的线程释放该锁。这个锁通常是对象的一个监视器(monitor),对于静态方法来说是类的Class对象,对于实例方法则是拥有该方法的对象。
- synchronized可以限制对共享资源的访问,它锁定的并不是临界资源,而是某个对象,只有线程获取到这个对象的锁才能访问临界区,进而访问临界区中的资源。
- 保证线程安全。
-
当多个线程去访问同一个类(对象或方法)的时候,该类都能表现出正常的行为(与自己预想的结果一致),那我们就可以说这个类是线程安全的。
-
造成线程安全问题的主要诱因有两点
- 存在共享数据(也称临界资源)
- 存在多条线程共同操作共享数据
-
当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能)。
-
3. synchronized的使用
下面三种本质上都是锁对象
3.1 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
示例代码:
public class SynchronizedDemo2 {
int num = 0;
public synchronized void add() {
// public void add() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
public static class AddDemo extends Thread {
private SynchronizedDemo2 synchronizedDemo2;
public AddDemo(SynchronizedDemo2 synchronizedDemo2) {
this.synchronizedDemo2 = synchronizedDemo2;
}
@Override
public void run() {
this.synchronizedDemo2.add();
}
}
public static void main(String[] args) throws InterruptedException {
// 要想拿到临界资源,就必须先获得到这个对象的锁。
SynchronizedDemo2 synchronizedDemo2 = new SynchronizedDemo2();
AddDemo addDemo1 = new AddDemo(synchronizedDemo2);
AddDemo addDemo2 = new AddDemo(synchronizedDemo2);
AddDemo addDemo3 = new AddDemo(synchronizedDemo2);
addDemo1.start();
addDemo2.start();
addDemo3.start();
// 阻塞主线程
addDemo1.join();
addDemo2.join();
addDemo3.join();
// 打印结果
System.out.println(synchronizedDemo2.num);
}
}
打印:
期望结果:30000
无synchronized结果:23885
有synchronized结果:30000
synchronize作用于实例方法需要注意:
- 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的
- 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法
3.2 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
锁定静态方法需要通过类.class,或者直接在静态方法上加上关键字。但是,类.class不能使用this来代替。注:在同一个类加载器中,class是单例的,这也就能保证synchronized能够只让一个线程访问临界资源。
示例代码:
public class SynchronizedDemo1 {
static int num = 0;
// 加上synchronized保证线程安全
public static synchronized void add() {
// public static void add() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
// 同上
public static void add1() {
synchronized (SynchronizedDemo1.class) {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
public static class AddDemo extends Thread {
@Override
public void run() {
SynchronizedDemo1.add();
}
}
public static void main(String[] args) throws InterruptedException {
AddDemo addDemo1 = new AddDemo();
AddDemo addDemo2 = new AddDemo();
AddDemo addDemo3 = new AddDemo();
addDemo1.start();
addDemo2.start();
addDemo3.start();
// 阻塞主线程
addDemo1.join();
addDemo2.join();
addDemo3.join();
// 打印结果
System.out.println(SynchronizedDemo1.num);
}
}
打印:
期望结果:30000
无synchronized结果:14207
有synchronized结果:30000
3.3 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁
若是this,相当于修饰实例方法
示例代码:
public class SynchronizedDemo3 {
private static Object lockobj = new Object();
private static int num = 0;
public static void add() {
synchronized (lockobj) {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
public static class AddDemo extends Thread {
@Override
public void run() {
SynchronizedDemo3.add();
}
}
public static void main(String[] args) throws InterruptedException {
AddDemo addDemo1 = new AddDemo();
AddDemo addDemo2 = new AddDemo();
AddDemo addDemo3 = new AddDemo();
addDemo1.start();
addDemo2.start();
addDemo3.start();
// 阻塞主线程
addDemo1.join();
addDemo2.join();
addDemo3.join();
// 打印结果
System.out.println(SynchronizedDemo3.num);
}
}
打印:
期望结果:30000
无synchronized结果:28278
有synchronized结果:> 示例代码:
4. 分析代码是否互斥
分析代码是否互斥的方法,先找出synchronized作用的对象是谁,如果多个线程操作的方法中synchronized作用的锁对象一样,那么这些线程同时异步执行这些方法就是互斥的。
示例代码:
public class SynchronizedDemo4 {
// 作用于当前类的实例对象
public synchronized void m1() {
}
// 作用于当前类的实例对象
public synchronized void m2() {
}
// 作用于当前类的实例对象
public void m3() {
synchronized (this) {
}
}
// 作用于当前类Class对象
public static synchronized void m4() {
}
// 作用于当前类Class对象
public static void m5() {
synchronized (SynchronizedDemo4.class) {
}
}
public static class T extends Thread {
SynchronizedDemo4 demo;
public T(SynchronizedDemo4 demo) {
this.demo = demo;
}
@Override
public void run() {
super.run();
}
}
public static void main(String[] args) {
SynchronizedDemo4 d1 = new SynchronizedDemo4();
Thread t1 = new Thread(() -> {
d1.m1();
});
Thread t2 = new Thread(() -> {
d1.m2();
});
Thread t3 = new Thread(() -> {
d1.m3();
});
SynchronizedDemo4 d2 = new SynchronizedDemo4();
Thread t4 = new Thread(() -> {
d2.m2();
});
Thread t5 = new Thread(() -> {
SynchronizedDemo4.m4();
});
Thread t6 = new Thread(() -> {
SynchronizedDemo4.m5();
});
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
结论:
- 线程t1、t2、t3中调用的方法都需要获取d1的锁,所以他们是互斥的
- t1/t2/t3这3个线程和t4不互斥,他们可以同时运行,因为前面三个线程依赖于d1的锁,t4依赖于d2的锁
- t5、t6都作用于当前类的Class对象锁,所以这两个线程是互斥的,和其他几个线程不互斥
5. synchronized的可重入性
示例代码:
public class SynchronizedDemo5 {
synchronized void method1() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
method2();
System.out.println("method1 thread-" + Thread.currentThread().getName() + " end");
}
synchronized void method2() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("method2 thread-" + Thread.currentThread().getName() + " end");
}
public static void main(String[] args) {
SynchronizedDemo5 t5 = new SynchronizedDemo5();
new Thread(t5::method1, "1").start();
new Thread(t5::method1, "2").start();
new Thread(t5::method1, "3").start();
}
}
打印:
method2 thread-1 end
method1 thread-1 end
method2 thread-3 end
method1 thread-3 end
method2 thread-2 end
method1 thread-2 end
结论:
当线程启动的时候,已经获取了对象的锁,等method1调用method2方法的时候,同样是拿到了这个对象的锁。所以synchronized是可重入的。
6. 发生异常synchronized会释放锁
示例代码:
public class SynchronizedDemo6 {
int num = 0;
synchronized void add() {
System.out.println("thread" + Thread.currentThread().getName() + " start");
while (num <= 7) {
num++;
System.out.println("thread" + Thread.currentThread().getName() + ", num is " + num);
if (num == 3) {
throw new NullPointerException();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo6 synchronizedDemo6 = new SynchronizedDemo6();
new Thread(synchronizedDemo6::add, "1").start();
Thread.sleep(1000);
new Thread(synchronizedDemo6::add, "2").start();
}
}
打印:
thread1 start
thread1, num is 1
thread1, num is 2
thread1, num is 3
Exception in thread “1” java.lang.NullPointerException
at com.xin.demo.threaddemo.lockdemo.synchronizeddemo.SynchronizedDemo6.add(SynchronizedDemo6.java:14)
at java.lang.Thread.run(Thread.java:748)
thread2 start
thread2, num is 4
thread2, num is 5
thread2, num is 6
thread2, num is 7
thread2, num is 8
结论:
发生异常synchronized会释放锁
7. synchronized的实现原理与应用(包含锁的升级过程)
我的另一篇读书笔记:Java并发机制的底层实现原理 2.2节
锁的升级过程:无锁->偏向锁->轻量级锁->重量级锁,详细情况还是看上面这篇文章
- 无锁
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入
- 轻量级锁:当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞线程造成的CPU在用户态和内核态间转换的消耗。轻量级锁时,CPU是用户态。
- 重量级锁:两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗CPU,轻量级锁会升级成重量级锁。重量级锁时,CPU是内核态。
参考1:【多线程与高并发】- synchronized锁的认知
参考2:线程安全和synchronized
建议阅读文章1:【并发编程系列2】synchronized锁升级原理分析(偏向锁-轻量级锁-重量级锁)
建议阅读文章2:Java 对象、对象头mark word、锁升级、对象占内存大小