Synchronized的使用

前言

同一个进程中多条线程的内存是共享的,如果不进行同步处理,会产生不可预知的结果。如何解决同步问题呢? 我们要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等待该线程操作完毕后再进行操作,实现这种方式我们要在线程上加锁,使用关键字synchronized就是其中一种加锁方式,它可以让每个线程依次排队操作共享数据。

原子性,可见性和有序性

什么是原子性,可见性和有序性?

  1. 原子性:原子是构成物质的基本单位,所以原子的意思是------"不可分",原子性的操作拒绝线程调度器中断。
  2. 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到,称为可见性。
  3. 有序性:有序性是指程序按照代码的先后顺序执行。编译器为了优化性能,有时会改变程序中语句的执行顺序,但是不会影响最终的结果。有序性的经典案例就是利用DCL双重检查创建单例对象。

synchronied可以保证原子性,可见性和有序性。

synchronized的应用方式

synchronized有以下3种应用方式:

  1. 用在普通方法上,加锁的对象是当前实例,进入方法前需要获取当前实例的锁;
  2. 用在静态方法上,加锁的对象是当前类对象,进入方法前需要获取当前类对象的锁;
  3. 用来修饰代码块,这种方式需要自己指定加锁对象,进入代码块前要获得指定对象的锁;

synchronized作用于实例方法

看如下代码:

java 复制代码
public class AccountingSync implements Runnable{
    // 共享资源(临界资源)
    static int i=0;

    // synchronized修饰普通方法
    public synchronized void increase(){
        i++;
    }
    
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join(); // join:当前线程要等待调用join()的线程执行完毕才能继续往下执行
        t2.join();
        System.out.println(i);
    }
}

在上述代码中,我们开启两个线程操作同一个共享资源------变量i,由于i++操作不具备原子性,该操作是先读取旧值,然后在旧值的基础上加1,最后写回新值,总共分3步完成。如果第2个线程在第1个线程读取旧值和写回新值期间读取i的值,那么第2个线程与第1个线程读取的是就是同一个值,并执行相同值的加1操作,最后写回的也就是同一个值。这样就造成了线程安全问题,显然不是我们想要的结果。

为了解决这个问题,我们使用synchronized修饰普通方法increase(),在种情况下,当前锁的对象便是instance实例。运行代码,最终输出结果是2000000,从执行结果来看确实是正确的,倘若没有使用synchronized关键字,其最终输出结果会小于2000000,这便是synchronized关键字的作用。

但这种情况下如果给2个线程传入不同的实例,线程安全就无法保证了,看如下代码:

java 复制代码
public class AccountingSyncBad implements Runnable{

    static int i=0;
    
    public synchronized void increase(){
        i++;
    }
    
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        // 传入新实例
        Thread t1=new Thread(new AccountingSyncBad());
        // 传入新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述方法我们使用synchronized修饰了increase()方法,但是new了两个不同的实例,这也就意味着锁的对象是2个不同的实例,也就是说t1和t2线程中锁的对象不同,因此线程安全是无法保证的。

解决这个问题的方法是将increase()方法改成静态方法,这样锁的对象就是当前类对象,无论创建多少个实例对象,类对象只有一个,这样2个线程锁的对象是相同的。下面我们看看如何将synchronized作用于静态方法。

synchronized作用于静态方法

当synchronized作用于静态方法时,锁的对象是当前类对象。静态方法是类成员,比如静态的increase()方法可以直接通过AccountingSyncClass.increse()调用,不管你new多少个实例,类对象只有一个。因此把上面的increase()方法改成static修饰,就可以拿到正确的打印结果。代码如下:

java 复制代码
public class AccountingSyncBad implements Runnable{

    static int i=0;
    
    // 静态synchronized方法,锁的是当前类对象
    public static synchronized void increase(){
        i++;
    }
    
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        // 传入新实例
        Thread t1=new Thread(new AccountingSyncBad());
        // 传入新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

需要注意的是如果线程A调用实例的非静态synchronized方法,而线程B调用新实例的静态synchronized方法,这种不会发生互斥现象,因为两个线程锁的对象不一样,看如下代码:

java 复制代码
public class AccountingSync implements Runnable{

    private boolean flag;

    public AccountingSync(boolean flag){
        this.flag = flag;
    }

    static int i=0;

    // 静态synchronized方法,锁的对象是当前类对象
    public static synchronized void increase(){
        i++;
    }

    // 非静态synchronized方法,锁的对象是当前实例
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        if(flag) {
            for(int j=0;j<1000000;j++) {
                increase();
            }
        }else {
            for(int j=0;j<1000000;j++) {
                increase4Obj();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        // new新实例
        Thread t1=new Thread(new AccountingSync(true));
        // new新实例
        Thread t2=new Thread(new AccountingSync(false));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

其打印结果会小于2000000。

synchronized作用于同步代码块

除了可以使用synchronized关键字修饰实例方法和静态方法外,还可以用来修饰同步代码块,在某些情况下,我们编写的方法体可能比较大,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,同步代码块的使用示例如下:

java 复制代码
public class AccountingSync implements Runnable{

    private Object object = new Object();

    static int i;

    static int m;

    @Override
    public void run() {
        for(int k=0; k < 1000000; k++){
            m++;
        }
        
        synchronized(object){
            for(int j=0; j < 1000000; j++){
                    i++;
              }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(m);
        System.out.println(i);
    }
}

上述代码运行后,m的打印值小于2000000,而i的打印值是2000000。

这里synchronized锁的object对象没有用static修饰,锁的对象是当前实例。如果把object改成static修饰,锁的对象就是类对象了,这种情况与synchronized作用于静态方法类似。由此可知,synchronized作用于同步代码块,锁的对象与synchronized修饰的对象有关。

另外,使用sychronied修饰普通方法,其实也等价于synchronized修饰同步代码块并包裹整个方法的synchronized(this){},意思是下面2段代码是等价的:

java 复制代码
public synchronized void increase(){
    i++;
}
java 复制代码
public void increase(){
    synchronized(this){
        i++;
    }    
}

使用sychronied修饰静态方法,其实也等价于synchronized作用于同步代码块并包裹整个方法的synchronized(class){},下面2段代码也是等价的:

java 复制代码
public static synchronized void increase(){
    i++;
}
java 复制代码
public void increase(){
    synchronized(AccountingSync.class){
        i++;
    }    
}
相关推荐
郭二哈19 分钟前
C++——模板进阶、继承
java·服务器·c++
A尘埃24 分钟前
SpringBoot的数据访问
java·spring boot·后端
yang-230725 分钟前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
沉登c26 分钟前
幂等性接口实现
java·rpc
代码之光_198037 分钟前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
科技资讯早知道1 小时前
java计算机毕设课设—坦克大战游戏
java·开发语言·游戏·毕业设计·课程设计·毕设
小比卡丘2 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
xmh-sxh-13142 小时前
java 数据存储方式
java
liu_chunhai3 小时前
设计模式(3)builder
java·开发语言·设计模式
ya888g3 小时前
GESP C++四级样题卷
java·c++·算法