java synchronized 小记

129 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第20天,点击查看活动详情

前言

之前简单的介绍过了java volatile关键字,现在再来介绍关键字synchronized。这个也是java里面非常重要的一个关键字。面试的重点啊!所以结合java的官方文档,一起来总结下这个关键字的知识点。

定义

synchronized关键字在执行时会申请获取互斥锁,然后执行代码块,再释放锁。当一个线程获取并持有这个锁的时候,其它线程无法获取到这个锁。

我们先了解下java中的同步机制,java中是通过监视器实现的,每一个java对象都会和一个见识器相关联,操作的线程可以对监视器进行加锁或解锁操作。而当一个现场获取了监视器的资源的锁后,别的线程如果也想获取这把锁就会被s阻塞进入等待的状态。

synchronized 关键字可以修饰多个地方,比如

同步方法(实例方法):锁的是当前调用方法的实例对象。进入代码块执行要获得当前实例的锁。

静态同步方法:锁的的是这个方法所在类的class对象。进入同步代码钱要获得当前类对象的锁。

同步方法块:锁是括号里面的对象,对给定的对象加锁,进入同步代码库钱要获得给定对象的锁

首先我们来看下synchronized修饰实例方法。我们可以先把这个关键字去掉然后运行程序。这个时候由于没有synchronized关键字来保证临界资源在同一时间只能由一个线程持有。所以会输出:

public class DemoTest extends Thread {
    //计数
    static int count = 0;
    
    public synchronized void increase() throws InterruptedException {
        sleep(1000);
        count++;
        System.out.println(Thread.currentThread().getName() + ": " + count);
    }

    @Override
    public void run() {
        try {
            increase();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DemoTest test = new DemoTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.setName("threadOne");
        t2.setName("threadTwo");
        t1.start();
        t2.start();
    }
}

只是没有synchronized 关键字修饰的,因此两个线程可能同时进入了这个方法,在之前的文章中有提到Java的内存模型,默认是会从当前的线程工作内存中读取变量。所以没有保证可见性。

threadOne: 2
threadTwo: 2

下面是加上了synchronized的,这样可以保证可见性、原子性。也就是线程需要先尝试获取这个方法的锁,如果获取到了,那么才可以进入方法继续执行。否则只能等待别的线程释放锁才可以再次尝试获取。

threadOne: 1
threadTwo: 2

如何验证是锁住的当前实例呢?首先我们实例化两个DemoTest,各自启动一个线程,然后线程体内的方法我们做个修改

public synchronized void increase() throws InterruptedException {
    // sleep(1000);
    for(int i = 0;i < 100000000;i++){
    count++;
    }
    System.out.println(Thread.currentThread().getName() + ": " + count);
}

如果是锁住不是当前实例的话,那么打印的值就应该分别是100000000和200000000。 而实际打印出来的是

threadTwo: 51668429
threadOne: 101667571

说明锁住的是当前实例,因为我们实例化的是两个对象,它们的线程可以分别获取到对应实例的锁,所以代码并没有同步执行。

由于我们创建了两个实例对象,所以如果还是要保证我们代码执行时候可以通过锁的机制来维持同步的话,那么就需要对当前的类对象加锁。

我们在之前的方法上做出修改

public static synchronized void increase() throws InterruptedException {
    // sleep(1000);
    for(int i = 0;i < 100000000;i++){
    count++;
    }
    System.out.println(Thread.currentThread().getName() + ": " + count);
}

将这个方法修改为静态方法,那么它锁定的就是这个类了。再次运行代码得到的结果

threadOne: 100000000
threadTwo: 200000000

最后一种就是synchronized 修饰代码块了。对于代码块而言,我们上锁锁的就是这个对象,我的认识是相当于把这个对象作为一个临界资源,哪个线程获取持有了这个资源那么哪个线程就可以进入这个代码块进行执行。而另外的线程就需要等待。

public static void increase() throws InterruptedException {
    System.out.println(Thread.currentThread().getName() + "获取到锁,其他线程在我执行完毕之前,不可进入。" );
    synchronized (objectLock) {
        // sleep(1000);
        for(int i = 0;i < 1000000;i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName() + ": " + count);
    }
}

例如修改成上面的代码同样可以实现同步执行代码的效果。

以上是一些对于synchronized浅显的认识,总结记录下可以在日后使用过程中对自己有所帮助。