总结synchronized

1.什么是synchronized

synchronized 是 Java 并发模块 非常重要的关键字,它是 Java 内建的一种同步机制,代表了某种内在锁定的概念,当一个线程对某个 共享资源 加锁后,其他想要获取共享资源的线程必须进行等待,synchronized 也具有互斥和排他的语义。主要应用于多线程环境下保证线程的安全性。

2.synchronized的特性

2.1 原子性

所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。但是像i++、i+=1等操作字符就不是原子性的,它们是分成读取、计算、赋值几步操作,原值在这些步骤还没完成时就可能已经被赋值了,那么最后赋值写入的数据就是脏数据,无法保证原子性。

被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。

2.2 可见性

可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。

synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。

而volatile的实现类似,被volatile修饰的变量,每当值需要修改时都会立即更新主存,主存是共享的,所有线程可见,所以确保了其他线程读取到的变量永远是最新值,保证可见性。

2.3 有序性

有序性值程序执行的顺序按照代码先后执行。

synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

2.4 可重入性

ynchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

3.synchronized的用法

synchronized的使用其实比较简单,可以用它来修饰实例方法和静态方法,也可以用来修饰代码块。我们需要注意的是synchronized是一个对象锁,也就是它锁的是一个对象。我们无论使用哪一种方法,synchronized都需要有一个锁对象

  1. 修饰实例方法
  2. 修饰静态方法
  3. 修饰代码块 

3.1.修饰实例方法

synchronized修饰实例方法, 在方法上加上synchronized关键字即可。

public class SynchronizedTest1 {
    public synchronized void test() {
        System.out.println("synchronized 修饰 方法");
    }
}

此时,synchronized加锁的对象就是这个方法所在实例的本身,作用于当前实例加锁,进入同步代码前要获得当前实例的锁 

3.2.修饰静态方法

synchronized修饰静态方法的使用与实例方法并无差别,在静态方法上加上synchronized关键字即可


class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public synchronized static void method() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}
 
public class Demo00{
	
	public static void main(String args[]){
		SyncThread syncThread1 = new SyncThread();
		SyncThread syncThread2 = new SyncThread();
		Thread t1 = new Thread(syncThread1, "SyncThread1");
		Thread t2 = new Thread(syncThread2, "SyncThread2");
		t1.start();
		t2.start();
	}
}

由于静态方法不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。给静态方法加synchronized锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前静态方法所在类的Class对象的锁

有一点我们需要知道:如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

3.3.修饰代码块

synchronized修饰代码块需要传入一个对象。

public class SynchronizedTest2 {
    public void test() {
        synchronized (this) {
            System.out.println("synchronized 修饰 代码块");
        }
    }
}

此时synchronized加锁对象即为传入的这个对象实例,指定加锁对象,进入同步代码库前要获得给定对象的锁 需要注意的是这里的this 

  1. synchronized(object) ,表示进入同步代码库前要获得 给定对象的锁
  2. synchronized(类.class) ,表示进入同步代码前要获得 给定 Class 的锁
  3. 最好不要使用 synchronized(String a) ,因为在 JVM 中,字符串常量池具有缓存功能,如果我们多次加锁,会加锁在同一个对象上

 4.synchronized的锁机制

4.1 锁机制

锁机制指的是锁所具有的特性,由于每种锁的特性各异,合理运用不同的锁能够显著提升效率。

锁机制的出现,主要是为了解决多线程环境下数据访问的并发问题,确保数据的一致性和完整性,避免出现错误的结果。

4.2 锁机制的类型

4.2.1. 乐观锁与悲观锁

乐观锁假定数据通常不会出现并发冲突,仅在数据提交更新时,才对是否产生并发冲突进行检测。若发现冲突,会向用户返回错误信息,由用户决定后续操作。

例如,在一个在线文档编辑系统中,如果多个用户同时编辑同一个文档的不同部分,乐观锁可以先让用户进行编辑,在保存时检查是否有冲突。

悲观锁则总是预设最坏情况,每次获取数据时都认为他人会修改,因此每次拿数据时都会上锁,导致其他想获取该数据的线程阻塞,直至获取到锁。

比如在银行系统中,处理账户资金操作时,通常会使用悲观锁来确保数据的绝对安全。

需要注意的是:Synchronized 初始使用乐观锁策略,当发现锁竞争频繁时,会自动切换为悲观锁策略。

4.2.2. 重量级锁与轻量级锁

锁的核心特性“原子性”源于 CPU 等硬件设备提供的支持。

(1)CPU 提供了“原子操作指令”。

(2)操作系统基于 CPU 的原子指令,实现了 mutex 互斥锁。

(3)JVM 基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类。

同时要知道,synchronized 并不仅仅是对 mutex 进行封装,其内部还进行了诸多其他工作。

重量级锁的加锁机制重度依赖 OS 提供的 mutex,会产生大量的内核态与用户态切换,容易引发线程调度。这两种操作成本较高,一旦涉及切换,情况就会有很大变化。

比如在一个高并发的数据库系统中,如果频繁使用重量级锁,可能会导致系统性能严重下降。

轻量级锁的加锁机制尽可能避免使用 mutex,尽量在用户态代码中完成,实在无法解决时才使用 mutex,因此只有少量的内核态与用户态切换,不太容易引发线程调度。

而且,synchronized 开始是一个轻量级锁,若锁冲突严重,则会转变为重量级锁。

4.2.3. 自旋锁(Spin Lock)

以往,线程抢锁失败后会进入阻塞状态,放弃 CPU,需等待很久才能再次被调度。但实际上,在多数情况下,尽管当前抢锁失败,不久后锁就会被释放,没必要放弃 CPU。此时便可使用自旋锁来解决这一问题。

若获取锁失败,立即再次尝试获取锁,不断循环,直至获取到锁为止。第一次获取锁失败,第二次尝试会很快进行。一旦锁被其他线程释放,就能第一时间获取到锁。

自旋锁是一种典型的轻量级锁实现方式,其优点在于没有放弃 CPU,不涉及线程阻塞和调度,锁一旦释放就能立刻获取。缺点是若锁被其他线程持有时间较长,会持续消耗 CPU 资源(而线程挂起等待时不消耗 CPU)。

例如,在一个短时间内频繁获取锁的场景中,自旋锁可以提高性能。

另外,synchronized 中的轻量级锁策略大概率是通过自旋锁的方式实现的。

4.2.4. 公平锁与非公平锁

公平锁遵循“先来后到”原则。例如 B 比 C 先来,当 A 释放锁后,B 能先于 C 获取到锁。

非公平锁则不遵循这一原则,B 和 C 都有获取到锁的可能。

需要注意的是,操作系统内部的线程调度可视为随机的。若不做额外限制,锁就是非公平锁。若要实现公平锁,需依赖额外的数据结构来记录线程的先后顺序。

公平锁和非公平锁并无绝对的好坏之分,关键取决于适用场景。

比如在一个对响应时间要求较高的系统中,可能更倾向于使用非公平锁。

同时,synchronized 是非公平锁。

4.2.5. 可重入锁和不可重入锁

可重入锁指的是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如在一个有锁操作的递归函数中,若递归过程中该锁不会阻塞自身,那么这个锁就是可重入锁(因此也叫递归锁)。

在 Java 中,只要是以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。但 Linux 系统提供的 mutex 是不可重入锁。

此外,synchronized 是可重入锁。

例如,在一个递归计算的函数中,如果使用的锁是不可重入的,就会导致死锁。

4.2.6. 读写锁

在多线程环境中,数据的读取方之间不会产生线程安全问题,但数据的写入方相互之间以及和读取方之间需要进行互斥。若在两种场景下都使用同一把锁,会造成极大的性能损耗,读写锁因此应运而生。

读写锁(readers-writer lock),从英文名可看出,执行加锁操作时需额外表明读写意图,多个读取者之间不互斥,而写者则要求与任何人互斥。

一个线程对于数据的访问,主要存在读数据和写数据两种操作:两个线程都只是读数据时,不存在线程安全问题,可直接并发读取;两个线程都要写数据,存在线程安全问题;一个线程读,另一个线程写,也存在线程安全问题。

读写锁将读操作和写操作区别对待。Java 标准库提供了 ReentrantReadWriteLock 类来实现读写锁。ReentrantReadWriteLock.ReadLock 类表示读锁,提供了 lock/unlock 方法进行加锁解锁;ReentrantReadWriteLock.WriteLock 类表示写锁,同样提供了 lock/unlock 方法进行加锁解锁。

其中,读加锁和读加锁之间不互斥,写加锁和写加锁之间互斥,读加锁和写加锁之间互斥。

读写锁特别适用于“频繁读,不频繁写”的场景,此类场景十分常见。并且,synchronized 不是读写锁。

例如,在一个高并发的缓存系统中,如果读操作远远多于写操作,使用读写锁可以显著提高性能。

5.结语


synchronized作为 Java 中重要的同步机制,通过其丰富的特性和多样的用法,以及不同类型的锁机制,为开发者在多线程编程中提供了有效的工具,帮助解决并发问题,保障程序的正确性和性能。但在实际应用中,需要根据具体的业务场景和性能需求,合理选择和运用,以实现高效、稳定的多线程程序。希望大家在今后的编程实践中,能够熟练掌握并灵活运用synchronized,编写出更加优秀的多线程应用程序。

相关推荐

  1. synchronized使用

    2024-07-15 20:22:02       48 阅读
  2. 聊一聊synchronized

    2024-07-15 20:22:02       56 阅读
  3. AtCoder D - Synchronized Players

    2024-07-15 20:22:02       52 阅读
  4. synchronized的使用方式

    2024-07-15 20:22:02       30 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-15 20:22:02       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-15 20:22:02       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-15 20:22:02       57 阅读
  4. Python语言-面向对象

    2024-07-15 20:22:02       68 阅读

热门阅读

  1. JVM堆内存的结构,YGC,FGC的原理

    2024-07-15 20:22:02       21 阅读
  2. Spring boot 2.0 升级到 3.3.1 的相关问题 (二)

    2024-07-15 20:22:02       21 阅读
  3. LeetCode题练习与总结:寻找峰值--162

    2024-07-15 20:22:02       17 阅读
  4. Mysql数据库(一)

    2024-07-15 20:22:02       25 阅读
  5. (leetcode学习)16. 最接近的三数之和

    2024-07-15 20:22:02       19 阅读
  6. /EtherCATInfo/Descriptions/Devices/Device/SubDevice/@Hideable

    2024-07-15 20:22:02       16 阅读
  7. 零基础自学爬虫技术该从哪里开始入手?

    2024-07-15 20:22:02       19 阅读
  8. FeignClient详解

    2024-07-15 20:22:02       21 阅读
  9. 【经验】LiveData使用常见问题

    2024-07-15 20:22:02       21 阅读