【多线程】Java多线程基础(6)- synchronized用法详解

303 阅读6分钟

synchronized详解

之前的文章只讲了在一般方法里的使用,并不全面。

什么时候使用synchronized

当多个线程共用一个变量时,可能会发生问题。比如, 两个线程同时读取某一个变量并为其加一, 最后的结果为只加了一。

这时,就需要对同步进行限制,使其一次只能为一个线程使用。

我们知道synchronized的作用为: 为方法或代码块加锁,使其一次只能一个线程使用。

synchronized用法

第一种用法: 【一文通关】Java多线程基础(4)- 线程同步 - 掘金 (juejin.cn) ,文章里是对方法上锁。

第二种用法: synchronized (具体类.class) 是一种类锁的写法。类锁是指对类的锁定,不是对类的某个实例的锁定。在Java中,每个类都有一个唯一的Class对象,可以使用它来锁定整个类。
因此常用于静态代码块或静态代码块

比如: synchronized (Bank.class) 的作用是获取Bank类的锁,这样可以确保在同一时刻只有一个线程可以访问Bank类的同步方法或同步块。具体来说,当某个线程执行到synchronized (Bank.class)块时,它会尝试获取Bank类的锁,如果锁已经被其他线程获取,则该线程会被阻塞,直到锁被释放。

synchronized (Bank.class){
    //执行代码
}

这种类锁的写法可以用于以下场景:

  1. 控制类中所有同步方法的访问:如果一个类中有多个同步方法,可以使用类锁来控制它们的访问,以避免多个线程同时访问同步方法而产生的竞争问题。
  2. 控制类静态变量的访问:如果一个类中有静态变量,可以使用类锁来控制它们的访问,以避免多个线程同时访问静态变量而产生的竞争问题。

需要注意的是,类锁会对整个类进行锁定,而不是对某个实例进行锁定,因此它的作用范围比实例锁更广。同时,类锁的获取和释放也比较耗费系统资源,因此在使用类锁时需要谨慎考虑,避免过度使用,以提高系统的性能。

synchronized放在方法还是代码块比较好

synchronized关键字可以用在方法级别和代码块级别上,具体使用哪种方式取决于具体的应用场景。

如果要保证整个方法的原子性,那么将synchronized放在方法上是比较合适的。在这种情况下,整个方法都被同步,任何时刻只能有一个线程进入该方法执行。

如果只需要保证部分代码的原子性,那么将synchronized放在代码块上是比较合适的。在这种情况下,只有代码块中的内容被同步,其他线程可以在代码块以外的地方继续执行。

需要注意的是,将synchronized放在方法上可能会导致锁的粒度过大,从而影响程序的并发性能。因此,在使用synchronized关键字时,需要根据具体情况来决定同步的粒度和范围。如果只需要保证部分代码的原子性,那么将synchronized放在代码块上可以提高程序的并发性能。

下面分别举例说明在哪些情况下应该将synchronized放在方法上或代码块上。

  1. synchronized放在方法上

假设有一个银行账户类Account,其中有一个方法withdraw()用于取款操作,该方法需要保证原子性,即在同一时刻只能有一个线程执行该方法。在这种情况下,应该将synchronized放在方法上,代码示例如下:

public class Account {
    private int balance;

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }
}

在这个示例代码中,withdraw()方法被声明为synchronized,这意味着任何时刻只能有一个线程执行该方法。这样可以保证取款操作的原子性,避免多个线程同时对账户进行取款操作而导致数据不一致。

  1. synchronized放在代码块上

假设有一个线程安全的单例类Singleton,其中有一个方法getInstance()用于获取类的唯一实例,该方法需要保证原子性,即在同一时刻只能有一个线程获取实例。在这种情况下,应该将synchronized放在代码块上,代码示例如下:

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在这个示例代码中,getInstance()方法中的synchronized关键字被放在了代码块上,只有在instancenull的情况下才会执行synchronized块中的代码。这样可以避免在instance已经被初始化的情况下也执行synchronized块,从而提高程序的并发性能。

当锁放在代码块时,可以使用什么来当作锁对象

synchronized关键字后面的括号中,可以放置任何对象作为锁对象。通常情况下,我们使用synchronized关键字来实现线程同步,需要使用一个锁对象来保证同步。Java中的任何对象都可以作为锁对象,包括类、实例对象、数组等。

synchronized关键字后面的括号中,放置的对象会被用作锁对象。如果synchronized后面的括号中放置的是类对象,则该锁对象是类级别的;如果放置的是实例对象,则该锁对象是实例级别的。在多线程环境下,同一个锁对象只能被一个线程持有,其他线程需要等待锁的释放才能进入同步块。

除了类和实例对象,还可以使用其他对象作为锁对象,例如:

  1. 字符串对象:
public class MyClass {
    public void myMethod() {
        synchronized ("myLock") {
            // ...
        }
    }
}

在这个示例代码中,使用字符串对象"myLock"作为锁对象,任何拥有该字符串对象的线程都可以获得该锁。

  1. 数组对象:
public class MyClass {
    public void myMethod() {
        Object[] lockArray = new Object[10];
        synchronized (lockArray) {
            // ...
        }
    }
}

在这个示例代码中,使用一个对象数组lockArray作为锁对象,任何拥有该对象数组的线程都可以获得该锁。

需要注意的是,为了避免锁竞争,应该尽量使用私有的锁对象,而不是公共的对象。例如,不要使用全局变量或静态变量作为锁对象,因为这样会增加锁竞争的可能性。

synchronized的作用范围

  • 当它放在普通方法上, 它的作用范围为当前类实例。
  • 当它放在静态方法上,它的作用范围为当前类。
  • 当它锁住类Class,作用范围为类
  • 当它锁住类的静态变量, 作用范围为当前类
  • 当它锁住实例的变量, 作用范围为实例