> 文章列表 > 关于synchronized的介绍

关于synchronized的介绍

关于synchronized的介绍

文章目录

  • 前言
  • 一.synchronized的特性
  • 二.synchronized的使用
    • 2.1 同步方法
    • 2.2 同步代码
    • 2.3 静态同步方法
    • 2.4 同步代码块和volatile关键字
  • 三.synchronized的锁机制
    • 3.1 synchronized是什么锁机制
    • 3.2 synchronized的工作过程
    • 四. 其他的优化过程
      • 锁消除
      • 锁粗化

前言

我们先来看一下什么是synchronized,我做出了以下解释,大家可以看一下.
1.synchronized 是Java中的关键字,用于实现同步机制,确保在多线程环境下对共享资源的访问的安全性。
2.synchronized 可以被用来修饰方法或代码块,其作用是在同一时刻,只能有一个线程执行被 synchronized 修饰的代码,其他线程必须等待锁的释放才能继续执行。

3.在使用 synchronized 时,需要指定锁对象,锁对象可以是任意对象,只有持有同一把锁的线程才能访问被 synchronized 修饰的代码块或方法。

4.当一个线程进入被 synchronized 修饰的代码块或方法时,它会尝试获取锁对象的锁,如果锁没有被其他线程持有,那么该线程会获得锁并继续执行代码,当线程退出 synchronized 修饰的代码块或方法时,它会释放锁,以便其他线程可以获取锁并执行代码。如果锁被其他线程持有,则该线程将进入阻塞状态,等待获取锁。


一.synchronized的特性

我这里开始列出synchronized的特性如下:

原子性:synchronized能够保证被它修饰的代码块或方法的执行是原子性的,即在同一时刻只能有一个线程进入被synchronized修饰的代码块或方法。

可重入性:synchronized是可重入的,即在一个线程已经持有某个对象的锁时,它可以再次进入一个synchronized块或方法。

可见性:synchronized能够保证共享变量在多线程间的可见性,即一个线程修改了共享变量的值,另一个线程能够立即看到该变量的最新值。

互斥性:synchronized能够保证同一时刻只有一个线程能够进入被synchronized修饰的代码块或方法,从而保证线程之间的互斥性。

有序性:synchronized能够保证同一时刻只有一个线程能够执行被synchronized修饰的代码块或方法,从而保证线程之间的有序性。


二.synchronized的使用

我先给出一个综合的代码实例,来基础的展示一下synchronized的使用,代码如下:
这个代码我们就是用加锁操作,来保证俩个线程对count++的操作是原子性的.

public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}public static void main(String[] args) throws InterruptedException {SynchronizedExample example = new SynchronizedExample();// 创建两个线程来同时增加计数器的值Thread thread1 = new Thread(() -> {for (int i = 0; i < 100000; i++) {example.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 100000; i++) {example.increment();}});thread1.start();thread2.start();// 等待两个线程执行完毕thread1.join();thread2.join();System.out.println("Count: " + example.count);}
}

2.1 同步方法

public class Counter {private int count;public synchronized void increment() {count++;}public synchronized void decrement() {count--;}public synchronized int getCount() {return count;}
}

2.2 同步代码块

public class Example {private Object lock = new Object();private int count = 0;public void increment() {synchronized (lock) {count++;}}public void decrement() {synchronized (lock) {count--;}}public int getCount() {synchronized (lock) {return count;}}
}

2.3 静态同步方法

public class Example {private static int count;public static synchronized void increment() {count++;}public static synchronized void decrement() {count--;}public static synchronized int getCount() {return count;}
}

2.4 同步代码块和volatile关键字

public class Example {private volatile int count = 0;public void increment() {synchronized (this) {count++;}}public void decrement() {synchronized (this) {count--;}}public int getCount() {return count;}
}

这里我简单的介绍了synchronized的使用方法,但synchronized并不是一个单独的个体,可以根据不同场景,综合使用.


三.synchronized的锁机制

synchronized的锁机制,我先简单的阐述一下,什么是锁机制.
锁机制通过对共享资源的加锁来保证同一时刻只有一个线程可以访问该资源,其他线程必须等待锁的释放后才能访问。通过锁机制,可以有效地避免竞态条件的出现,从而保证程序的正确性和稳定性。

对于synchronized的来说,synchronized使用的是对象级别的锁机制,即每个对象都有一个与之关联的锁(也称为监视器)。在synchronized关键字加锁的代码块执行期间,该对象的锁被获取,其他线程无法访问该对象的synchronized代码块,只能等待当前线程执行完毕释放锁之后才能访问。

3.1 synchronized是什么锁机制

synchronized是Java中的一种锁机制,用于控制多个线程对共享资源的访问。synchronized可以用来修饰方法或代码块,在修饰方法时,锁住的是整个方法,而在修饰代码块时,锁住的是代码块中的对象。

synchronized锁机制的实现基于Java中的内置锁,也称为监视器锁。每个Java对象都有一个内置锁,可以用来同步访问该对象的代码。当一个线程获取了一个对象的内置锁后,其他线程必须等待该线程释放锁才能获取该对象的锁。

3.2 synchronized的工作过程

结合我们学习的锁策略,我们对synchronized进行以下总结:

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

加锁过程

关于synchronized的介绍

这上面解释了,synchronized的加锁过程,其中自旋锁和重量级锁,我们都知道是什么,但对偏向锁,好像没有太多的概念,我这里先解释一下什么是偏向锁.
这里还会来举一个生活中的小例子.女神和男神的故事
关于synchronized的介绍

偏向锁,只是先让线程针对锁,有个标记(做个标记很快的,非常轻量)
(偏向锁,只是先让线程针对锁,有个标记(做个标记很快的,非常轻量)

如果整个代码执行过程中,都没有遇到别的线程和我竞争这个锁,此时就不用真加锁了!!!
如果整个代码执行过程中,都没有遇到别的线程和我竞争这个锁~~此时就不用真加锁了!

但是一旦,要是有别的线程尝试来竞争这个锁!!!于是偏向锁就立即升级成真的锁(轻量级锁),此时别的线程只能等待
但是一旦,要是有别的线程尝试来竞争这个锁!于是偏向锁就立即升级成真的锁(轻量级锁),此时别的线程只能等待.
既保证了效率,又保证了线程安全!!
既保证了效率,又保证了线程安全!!

如果还没有明白,我这里再举一个实际的代码例子

public class MyObject {private int x;private int y;public synchronized void increment() {x++;y++;}
}

如果在多线程环境下,多个线程同时对 obj 进行操作,那么就可能会发生竞争条件,从而导致结果不可预测。此时,我们可以使用偏向锁来优化。

偏向锁的实现方式是,如果一个线程获取了该对象的锁,那么该对象就会被标记为偏向模式,并且该线程的 ID 会被记录在对象头中。接下来,如果该线程再次请求锁,就无需进行竞争,直接获取锁即可。这样就可以避免多线程竞争的开销。

例如,我们可以在一个单线程环境下进行如下操作:

MyObject obj = new MyObject();
obj.increment(); // 偏向锁对象
obj.increment(); // 无需竞争,直接获取锁

在第二次调用 increment() 方法时,由于只有一个线程在操作该对象,因此可以直接获取锁,无需进行竞争。这样就可以提高程序的性能

然后我们继续来说明synchronized工作过程的下一个阶段,就是进入偏向锁,之后,随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).
此处的轻量级锁就是通过 CAS 来实现.

通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
如果更新成功, 则认为加锁成功
如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

最后,如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex .
执行加锁操作, 先进入内核态.
在内核态判定当前锁是否已经被占用
如果该锁没有占用, 则加锁成功, 并切换回用户态.
如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒
这个线程, 尝试重新获取锁.

四. 其他的优化过程

锁消除

简单来说就是非必要不加锁
举一个简单的例子,我们经常使用的StringBuilder和StringBuffer
一般来说,我们说StirngBuffer是线程安全的,为什么呢?大家可以看一下源码
关于synchronized的介绍
这里实际上对他们都进行了加锁,但说实话,我们在使用他们的时候,不一定都是多线程的一个环境,如果是单线程使用,就不涉及线程安全的问题了,但这个但是这个synchronized是不是也写了?,但其实在编译阶段,没有被真正的去编译,这样的操作就是锁消除,可见编译器的优化,很重要,它还是比我们聪明的.
最后来总结一下:
锁消除是指在编译期间,由于分析程序的执行情况发现一些同步操作不会出现竞争,编译器会自动消除这些不必要的同步操作,从而提高程序的执行效率。

锁粗化

画一个小小的图,大家来感受一下:

关于synchronized的介绍
大家如果还是模模糊糊,我来一个实际的代码,大家来体验一下:
例如,下面的代码中,循环内部的加锁和解锁操作会重复执行多次


for (int i = 0; i < 1000; i++) {lock();// do somethingunlock();
}

如果将这些加锁解锁操作合并为一次加锁解锁操作,代码如下所示:

lock();
for (int i = 0; i < 1000; i++) {// do something
}
unlock();

这样做可以减少加锁解锁的次数,从而提高程序的执行效率。

最后来小小的总结一下:
锁粗化是一种优化技术,它的目的是将多个连续的加锁、解锁操作合并成一个更大的锁操作,从而减少加锁解锁的次数,减小锁的竞争。