多线程-synchronized锁关键字(五)

tech2025-08-22  3

1.作用简述

同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象多个线程可见,则对该对象的所有读取或写入都是通过同步方法完成的。总结:能够保证在同一时刻最多只有一个线程执行该段代码,以到达保证并发安全的效果

2.synchronized的两个用法
对象锁: 包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)类锁: 指synchronized修饰静态的方法或指定锁为class对象(类对象)

对象锁

synchronized修饰的普通方法,锁对象默认是this public class SyncMethod implements Runnable{ @Override public void run() { try { method(); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method() throws InterruptedException { System.out.println("我是【对象锁的方法修饰形式】,线程名为"+Thread.currentThread().getName()); Thread.sleep(3000); System.out.println(Thread.currentThread().getName()+"运行结束"); } public static void main(String[] args) throws InterruptedException { SyncMethod syncMethod = new SyncMethod(); Thread t1 = new Thread(syncMethod); t1.setName("线程1"); Thread t2 = new Thread(syncMethod); t2.setName("线程2"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("运行结束"); } } 结果: 我是【对象锁的方法修饰形式】,线程名为线程1 线程1运行结束 我是【对象锁的方法修饰形式】,线程名为线程2 线程2运行结束 线程运行结束

从上方可以看到被synchronized修饰的方法被两个线程同时执行的时候是顺序执行,他们的锁对象是this

代码块锁(手动选择锁对象) public class SyncCodeBlock implements Runnable{ Object lock1 = new Object(); Object lock2 = new Object(); public static void main(String[] args) throws InterruptedException { SyncCodeBlock syncCodeBlock = new SyncCodeBlock(); Thread t1 = new Thread(syncCodeBlock); Thread t2 = new Thread(syncCodeBlock); t1.setName("线程1");t2.setName("线程2"); t1.start();t2.start(); t1.join();t2.join(); System.out.println("执行完成"); } @Override public void run() { synchronized (lock1){ System.out.println("我的锁对象是【lock1】,我是【"+Thread.currentThread().getName()+"】"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【"+Thread.currentThread().getName()+"】,【lock1】部分运行结束"); } synchronized(lock2){ System.out.println("我的锁对象是【lock2】,我是【"+Thread.currentThread().getName()+"】"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【"+Thread.currentThread().getName()+"】,【lock2】部分运行结束"); } } } 结果: 我的锁对象是【lock1】,我是【线程1】 【线程1,【lock1】部分运行结束 我的锁对象是【lock2】,我是【线程1】 我的锁对象是【lock1】,我是【线程2】 【线程1,【lock2】部分运行结束 【线程2,【lock1】部分运行结束 我的锁对象是【lock2】,我是【线程2】 【线程2,【lock2】部分运行结束 执行完成

从上面可以看出来,lock1锁住上面的代码的时候,另一个线程是无法执行这段代码的。当第一个线程释放锁的时候,第二个线程才可以拿到这个锁,并且无法获取下一把锁。这样就实现 了不同的锁来锁住不同的代码块

类锁的简介

java可能会有很多个对象实例,但是class对象(类对象)只有一个,所谓类锁,即以class对象为锁的形式,因为class对象同时只能被一个对象所持有,具有唯一性

类锁的两种形式

synchronized加载在static方法上 public class SyncStatic implements Runnable{ @Override public void run() { try { method(); } catch (InterruptedException e) { e.printStackTrace(); } } public static synchronized void method() throws InterruptedException { System.out.println("类锁的【static形式】,我是"+Thread.currentThread().getName()); Thread.sleep(3000); System.out.println(Thread.currentThread().getName()+"===运行结束"); } public static void main(String[] args) throws InterruptedException { SyncStatic syncStatic1 = new SyncStatic(); SyncStatic syncStatic2 = new SyncStatic(); Thread t1 = new Thread(syncStatic1); Thread t2 = new Thread(syncStatic2); t1.setName("线程1");t2.setName("线程2"); t1.start();t2.start(); t1.join();t2.join(); System.out.println("==运行结束=="); } } 结果: 类锁的【static形式】,我是线程1 线程1===运行结束 类锁的【static形式】,我是线程2 线程2===运行结束 ==运行结束==

从上面可以看出来,两个线程的参数不同并同时调用了method()方法,因为存在着被synchronized修饰的这个静态方法,所以依然可以实现线程同步。如果将synchronized 这个关键字拿掉的话,则线程的安全无法实现

类对象作为锁的形式 public class SyncClass implements Runnable{ @Override public void run() { synchronized (SyncStatic.class){ System.out.println("我是【"+Thread.currentThread().getName()+"】"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"结束"); } } public static void main(String[] args) throws InterruptedException { SyncClass syncClass1 = new SyncClass(); SyncClass syncClass2 = new SyncClass(); Thread t1 = new Thread(syncClass1); Thread t2 = new Thread(syncClass2); t1.setName("线程1");t2.setName("线程2"); t1.start();t2.start(); t1.join();t2.join(); System.out.println("finish"); } } 结果: 我是【线程1】 线程1结束 我是【线程2】 线程2结束 finish

这里可以看到我们的锁对象是SyncStatic.class,所以虽然我们在Thread线程传入两个不同实例的Runnable实现类也是能够实现串行同步执行的 如果我们将SyncStatic.class换成this就会发现线程是并行执行的,原因是this代表的是通的实例对象,我们在Thread传入的对象是不一样的所以锁对象也就是不同的,这时候只要传入相同的实例就能保证线程能够串行执行

3.多线程同步访问的7中情况
1.两个线程同时访问同一个锁对象的同步方法

线程1先执行,待线程1释放锁后,线程2再执行

2.两个线程同时访问两个不同锁对象的同步方法

线程1和线程2同时执行,锁对象不同,执行时互不干扰

3.两个线程访问synchronized修饰的static方法

两个线程会依次执行,因为其默认的锁为当对象的类对象(类锁)

4.两个线程同时访问同步方法和非同步方法

会同时运行,synchronized只会作用于被修饰的方法,并且执行过程中不需要征用锁,所以他们是并行执行的

5.访问同一个对象的不同的普通同步方法 public class SameClassDiffNorMethod implements Runnable{ @Override public void run() { if (Thread.currentThread().getName().equals("线程1")){ method1(); }else{ method2(); } } public synchronized void method1() { System.out.println("我是线程1开始执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程1执行结束"); } public synchronized void method2() { System.out.println("我是线程2开始执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程2执行结束"); } public static void main(String[] args) { SameClassDiffNorMethod sameClassDiffNorMethod = new SameClassDiffNorMethod(); Thread t1 = new Thread(sameClassDiffNorMethod); Thread t2 = new Thread(sameClassDiffNorMethod); t1.setName("线程1");t2.setName("线程2"); t1.start();t2.start(); while (t1.isAlive()||t2.isAlive()){}; System.out.println("finish"); } } 结果: 我是线程1开始执行 线程1执行结束 我是线程2开始执行 线程2执行结束 finish

可以看到虽然我们执行的是不同的方法,但是它还是串行执行的,主要是他们在Thread中传入的是同一个对象而使用synchronized修饰的不同方法默认的锁对象就是this它指向的就是我们传入的这个对象,也就是说他们使用的是同一把锁

6.同时访问静态synchronized和非静态synchronized方法

我们大体可以预见,他们是并行执行的因为他们并没有使用同一锁对象,静态synchronized方法的锁对象是类对象,非静态synchronized方法的锁对象是this,他们不相同,也就无法保证串行执行

7.方法抛出异常会释放锁,但是使用lock来进行上锁的操作是不会进行释放锁的

public class SameClassDiffNorMethod implements Runnable{ @Override public void run() { if (Thread.currentThread().getName().equals("线程1")){ method1(); }else{ method2(); } } public synchronized void method1() { System.out.println("我是线程1开始执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } throw new RuntimeException(); //System.out.println("线程1执行结束"); } public synchronized void method2() { System.out.println("我是线程2开始执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程2执行结束"); } public static void main(String[] args) { SameClassDiffNorMethod sameClassDiffNorMethod = new SameClassDiffNorMethod(); Thread t1 = new Thread(sameClassDiffNorMethod); Thread t2 = new Thread(sameClassDiffNorMethod); t1.setName("线程1");t2.setName("线程2"); t1.start();t2.start(); while (t1.isAlive()||t2.isAlive()){}; System.out.println("finish"); } } 结果: 我是线程1开始执行 Exception in thread "线程1" java.lang.RuntimeException at thread.synckeyword.SameClassDiffNorMethod.method1(SameClassDiffNorMethod.java:21) at thread.synckeyword.SameClassDiffNorMethod.run(SameClassDiffNorMethod.java:8) at java.lang.Thread.run(Thread.java:748) 我是线程2开始执行 线程2执行结束 finish 测试不释放锁的情况可将synchronized替换为该对象,调用lock和unlock方式来进行操作 ReentrantLock myLock = new ReentrantLock(); myLock.lock(); myLock.unlock();

被synchronized修饰的方法在发生异常能够释放锁原因是jvm帮我们释放的,所以线程2能够很快的获得锁

总结

一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况);每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是 *.class 以及synchronized 修饰的是 static 方法的时候,所有的对象共用同一把锁(对应2、3、4、6种情况)无论是方法正常执行或者方法抛出异常,都会释放锁(对应第7种情况)

思考

加入一个线程进入到一个被synchronized修饰的方法,再这个方法种又调用了一个没有被synchronized修饰的方法,此时还是线程安全的吗
4.syncronized的性质
可重入性

含义: 指的是同一线程外层函数获得锁后,内层函数可直接再次获取该锁 一个线程拿到一把锁下次需要用锁的时候,可以直接拿到,就是可重入的,也称之为递归锁;如果拿到锁之后,想再次使用,此时不可直接再次拿到,必须和其他线程一起竞争,这就是不可重入的; 好处: 避免死锁,假设synchronized不具有可重入性质,现在线程1去访问一个被synchronized修饰的方法1的时候在方法内部调用了另一个被synchronized修饰的方法2,这个时候因为方法1还未释放锁,导致方法2获取不到锁,但是方法1又要调用方法2,这个时候就会陷入一种死锁状态

粒度:可重入的粒度是线程

证明同一个方法是可重入的 public class DeathLock implements Runnable{ int num = 0 ; @Override public void run() { try { method1(); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method1() throws InterruptedException { System.out.println("拿到锁了,开始执行程序"+num); Thread.sleep(2000); if (num==0){ num++; method1(); } } public static void main(String[] args) { Thread thread = new Thread(new DeathLock()); thread.start(); } } 结果: 拿到锁了,开始执行程序0 拿到锁了,开始执行程序1 证明可重入不要求是同一个方法 public class DeathLock implements Runnable{ @Override public void run() { try { method1(); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method1() throws InterruptedException { System.out.println("拿到锁了,开始执行程序"); Thread.sleep(2000); method2(); System.out.println("运行结束"); } public synchronized void method2(){ System.out.println("我是方法2"); } public static void main(String[] args) { Thread thread = new Thread(new DeathLock()); thread.start(); } } 结果: 拿到锁了,开始执行程序 我是方法2 运行结束 证明可重入锁不要求是同一个类 public class DiffClassLock { public synchronized void mySelfMethod(){ System.out.println("我是父类"); } } class OtherClass extends DiffClassLock{ @Override public synchronized void mySelfMethod() { System.out.println("我是子类"); super.mySelfMethod(); } public static void main(String[] args) { OtherClass otherClass = new OtherClass(); otherClass.mySelfMethod(); } } 结果: 我是子类 我是父类

通过上面三种情况我们可以看出可重入的粒度是线程,解释一下就是当一个线程在执行方法1时获得一把锁,这个时候他去调用另一个带锁方法2,如果这个方法2需要的锁就是方法1所持有的锁,那么synchronized可重入的性质就会被激发,就无需显性的去释放和获取锁

不可中断性

一旦这个锁被别人获得了,如果我还想要获得,就只能选择等待或者阻塞,直到别的线程释放这个锁,如果别人永远不释放锁,那么我只能永远地等待下去

tips:与syschronized相比lock是具有这种中断能力的,第一点,如果我觉得等待时间太长了有权中断已经获取锁的线程的执行,第二点:如果我觉得等待时间太长了不想再等了,可以退出;

5.锁的原理
加锁的原理

每个对象都会存在一个内置锁也可以被叫做监视器锁(monitor lock),多线程执行过程中争用的其实就是这个锁,所有我们在synchronized方法里只需要设置对象就行,线程就会争用指定对象的内置锁,至于线程是如何获取对象的内置锁和怎样去释放的其实都是jvm帮我们实现的,那么jvm到底是怎么实现的呢?我们可以反编译一下看一下底层是怎么实现的

//创建一个用于反编译的对象 public class MonitorLock { private final Object object = new Object(); public void inster(Thread thread){ synchronized(object){ } } } //查找到MonitorLock.java所在的目录执行 javac命令编译成字节码文件 javac MonitorLock.java //使用javap反编译class文件 javap -verbose MonitorLock.class //-verbose表示将所有的信息都打印 //这里是编译出来的信息的部分,我们找到inster方法 public void inster(java.lang.Thread); descriptor: (Ljava/lang/Thread;)V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=2 0: aload_0 1: getfield #3 // Field object:Ljava/lang/Object; 4: dup 5: astore_2 6: monitorenter 7: aload_2 8: monitorexit 9: goto 17 12: astore_3 13: aload_2 14: monitorexit 15: aload_3 16: athrow 17: return Exception table:

分析: 可以看到monitorenter(编号6)和monitorexit(编号8,14)这两个函数,其实它就对应这我们java执行过程中加锁和释放锁的过程

monitorenter和monitorexit指令 monitorenter

其实每一个对象都和一个monitor相关联,而一个monitor锁同一时刻只能被一个线程获得,一个线程在尝试获取monitor锁时只会有三种情况

线程未获得monitor锁且monitor锁计数器为0,表示这把锁还未被占用,此时线程会立即获得该锁的使用权,并将monitor锁计时器+1,计时器锁大于0就表示此锁已被占用其他锁要获取使用权就要等待锁释放线程已经获得了monitor锁,但是要调用其他使用同一个锁的方法,由于synchronized的可重入性此时会将monitor锁计时器继续+1表示进入到新的需要此锁的方法,这样锁计时器就变成了2,当该方法执行完就会将monitor锁计时器-1表示回到原有方法中线程未获得monitor锁且monitor锁计时器大于0,表示已被其他线程获取,这个时候只能等待锁的释放才能继续去竞争该monitor锁的使用权 monitorexit

monitorexit指令就相当于释放锁,当然这里必须是拥有锁才能释放锁。释放的过程比较简单,就是将monitor的计数器减1,如果减完之后,如果不是0,说明刚刚是可重入进来的,线程继续拥有monitor所有权;如果变成0,就说明当前线程已经释放锁,不再拥有monitor的所有权,这也意味着刚刚被阻塞的线程会再次尝试获取锁

可重入原理

可重入是利用加锁计数器实现的,每个对象都有一把monitor锁,JVM负责追踪monitor被加锁的次数

递增 线程第一次获得锁时,计数器会从0变成1,当相同的线程再次获取该对象的锁时计数器会递增

递减 当任务离开时,计数递减,当计数为0时,锁被完全释放。

可见性原理

(1)java内存的简单模型

可以看到线程A和线程B都有自己的本地内存,他们将主内存的共享变量复制一份到自己的本地内存中,实现内存共享,那么线程之间是怎么进行同行的呢可以按下面的步骤进行

线程A需要将自己本地内存的共享变量副本(修改后的)写入主内存,主内存是线程通信的桥梁主内存中数据更新后,线程B再从主内存中读取,此时线程B就能读取到A更新后的变量数据

(2)可见性原理的核心

一旦一个方法或代码块被synchronized修饰,在进入到方法或代码块时,被锁定的对象的数据是从主内存中读取;那么它在执行完毕之后被锁住的对象做的任何修改都要在锁释放之前,从线程内存写回到主内存中,所以每一次synchronized执行的数据都是最新的,每一次执行都是可靠的,它保证了可见性

6.synchronized的缺点
效率低 锁的释放情况少 synchronized锁的释放情况只有两种:正常执行完毕以及抛出异常。而如果锁不释放,其他处于阻塞的线程只能干巴巴的等着。当在执行IO或sleep等比较费时间的操作时,其他线程只能等很久,这是非常不好的,不过lock可以避免这种情况。试图获得锁时,不能设定超时不能中断一个正在试图获取锁的线程 不够灵活(读写锁更灵活) 加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的

3.synchronized与lock对比,lock能够克服这些缺点吗

public interface Lock { //加锁 void lock(); //加锁,能够响应中断 void lockInterruptibly() throws InterruptedException; //尝试获取锁,返回boolean boolean tryLock(); //尝试获取锁,设置超时时间,返回boolean boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //释放锁 void unlock(); //与条件相关的,处理线程的接口,这里不做展开 Condition newCondition(); }

可以看出Lock锁对象是提供这样的一些操作接口的,关于lockInterruptibly(),它比较特殊,当通过这个方法去获取锁时,如果其他线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

7.面试中的问题
synchronized使用要注意的点 锁对象不可为空:作为锁的对象必须实例过的,或者通过其他方法创建的,不能是一个空对象。因为锁的信息是保存在对象的对象头中的,如果没有对象就更没有对象头了,那么这把锁是不能工作的。作用域不宜过大:可以并行的代码,要尽量并行,我们加锁也只是为了保证线程的安全,不过将本可以并行的代码串行,这就大大降低了程序运行效率,也有违高并发编程的初衷。要注意避免死锁 如何选择Lock和synchronized 如果可以,尽量不要使用这两个,可以使用java.util.concurrent包中各种各样的工具类,例如CountDownLatch等,使用这些类不需要我们自己去做同步工作,更方便快捷如果synchronized适用我们的程序,可以优先选择它,因为它可以减少我们的代码量,也就减少了出错的机率如果我们特别需要用到Lock的独有的特性的时候,我们才用它总结:避免出错,优先选择现有的工具类,其次选择synchronized,最后需要使用Lock特性才使用Lock 多线程访问同步方法的各种情况 上面的笔记有具体的7种情况,看懂熟记,明白原理 思考
8.总结

一句话介绍synchronized: JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个i小鹌鹑可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质

synchronized的作用、地位、不控制并发的后果

两种用法:对象锁和类锁

多线程访问同步方法的7种情况:是否是static、Synchronized方法等

Synchronized的性质:可重入、不可中断

原理:加解锁原理、可重入原理、可见性原理

Synchronized的缺陷:效率低、不够灵活、无法预判是否获取到锁

最新回复(0)