java之Synchronized同步

2019-11-05 12:38:13 浏览数 (1)

java多线程下如何保证共享数据安全,如何保证数据的顺序访问问题,这就需要我们在书写程序代码时需要关注的一个点,在单体java服务中使用Synchronized关键字保证数据同步也是很常用的一件事情了。今天我们来分享的是Synchronized关键字的使用。

其实这个技术点的使用一点也不难,需要保证方法同步就在方法上加上Synchronized关键字就行,为什么今天自己还要单独抽取一点时间去写这篇呢?其实在这说下,每次写点内容之前,自己总是会要构思一个点来写的,所以这个点在自己心里有一两天了,所以今天就来分享一下。

好了,上面说出了自己要分享的原因了,我们还是按照原有的文章风格继续看这个点的示例程序了。

代码语言:javascript复制
package com.wpw.asyncthreadpool;

public class SynchronziedTest implements Runnable {
    /**
     * 共享数据
     */
    private static int i = 0;

    /**
     * 此方法的主要功能是对于共享数据的自增操作
     */
    private synchronized void increase() {
        i  ;
    }

    /**
     * 启动一个jvm进程
     *
     * @param args 入参参数数组
     */
    public static void main(String[] args) throws InterruptedException {
        /**首先我们先启动两个线程*/
        Thread thread = new Thread(new SynchronziedTest());
        thread.setName("线程一");
        Thread thread2 = new Thread(new SynchronziedTest());
        thread2.setName("线程二");
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println(i);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("当前JVM实例退出")));
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000; j  ) {
            increase();
        }
    }
}

我们先分析一下上面的示例程序,在分析上面的程序之前我们还是先简单介绍一下创建线程都有哪种方式吧。

首先这里你要知道如何创建线程,一般创建线程主要有三种方式,其一是手动new一个线程实现里面的run()方法,其二是实现Runnable接口,实现run()方法,其三就是实现Callable接口,实现里面的call()方法。

好了,我们继续,上面的程序示例,是自己在一个JVM进程里面new了两个线程示例,分别对其进行执行start操作,当我们使用synchronized关键字在方法上进行标识时,这就表示了这个方法是一个同步方法。看到我们执行的输出结果就是2000,其实你可以暂时将方法上的synchronized关键字去掉,多运行几次,你会发现运行的结果不是2000,而是另外一个数字,这是你就会明白,synchronized关键字是如何保证线程同步的了。

我们继续分析synchronized关键字的其它用法了。

代码语言:javascript复制
package com.wpw.asyncthreadpool;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class SynchronizedTest2 {
    /**
     * 测试同步方法
     *
     * @throws InterruptedException 抛出的异常信息类型
     */
    public synchronized void test1() throws InterruptedException {
        log.info("执行test1()方法开始");
        TimeUnit.SECONDS.sleep(3);
        log.info("执行test1()方法结束");
    }

    /**
     * 测试同步方法
     *
     * @throws InterruptedException 抛出的异常信息类型
     */
    public synchronized void test2() throws InterruptedException {
        log.info("执行test2()方法开始");
        TimeUnit.SECONDS.sleep(2);
        log.info("执行test2()方法结束");
    }

    public static void main(String[] args) {
        final SynchronizedTest2 synchronizedTest2 = new SynchronizedTest2();
        new Thread(() -> {
            try {
                synchronizedTest2.test1();
            } catch (InterruptedException e) {
                log.error("线程中断,message{}", e);
            }
        }).start();
        new Thread(() -> {
            try {
                synchronizedTest2.test2();
            } catch (InterruptedException e) {
                log.error("线程中断异常信息:{}", e);
            }
        }).start();
    }
}

上面的示例程序模拟的就是两个线程拿到同一个对象实例,分别调用不同的用synchronized关键字修饰的方法时,我们还是看下后台日志信息做下说明吧,这里以我自己跑的示例来说明咯。

代码语言:javascript复制
09:24:14.079 [Thread-0] INFO com.wpw.asyncthreadpool.SynchronizedTest2 - 执行test1()方法结束
09:24:14.079 [Thread-1] INFO com.wpw.asyncthreadpool.SynchronizedTest2 - 执行test2()方法开始
09:24:16.079 [Thread-1] INFO com.wpw.asyncthreadpool.SynchronizedTest2 - 执行test2()方法结束

其实,上面打印的日志信息有可能是先执行test2()方法,然后执行test1()方法,这里我只列举了一种打印日志信息。在上面的日志信息我们看到都是成对出现的,说明了synchronized关键字是保证同步的,因为每一个对象实例都可以是一把锁。

我们继续看下两个线程分别调用由synchronized关键字修饰的同步方法和普通方法的调用示例程序。

代码语言:javascript复制
package com.wpw.asyncthreadpool;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class SynchronizedTest3 {
    public synchronized void test1() {
        log.info("执行同步方法test1()开始");
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            log.error("线程中断异常信息:{}", e);
        }
        log.info("执行同步方法test1()结束");
    }

    public void test2() {
        log.info("执行普通方法test2()开始");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            log.error("线程中断异常信息:{}", e);
        }
        log.info("执行普通方法test2()结束");
    }

    public static void main(String[] args) {
        final SynchronizedTest3 synchronizedTest3 = new SynchronizedTest3();
        new Thread(() -> synchronizedTest3.test1()).start();
        new Thread(() -> synchronizedTest3.test2()).start();
    }
}

上面的示例程序就是分别起两个线程分别调用同步方法和普通方法进行日志信息的输出。

我们看下日志信息发现在执行同步方法的时候,普通方法也在执行,所以同一个实例对象在调用同步方法的时候不影响普通方法的调用执行过程。

代码语言:javascript复制
10:20:28.234 [Thread-1] INFO com.wpw.asyncthreadpool.SynchronizedTest3 - 执行普通方法test2()开始
10:20:28.238 [Thread-0] INFO com.wpw.asyncthreadpool.SynchronizedTest3 - 执行同步方法test1()开始
10:20:30.241 [Thread-1] INFO com.wpw.asyncthreadpool.SynchronizedTest3 - 执行普通方法test2()结束
10:20:32.242 [Thread-0] INFO com.wpw.asyncthreadpool.SynchronizedTest3 - 执行同步方法test1()结束

在这里说明一下,上面我们说的都是基于普通方法的修饰,其实synchronized关键字可以修饰类方法,那什么是类方法呢,就是由static关键字修饰的方法就是类方法。

这种模拟我们在这里就不介绍了,其实你明白类和对象之间的关系,其实很容易写出这种模拟效果的,接下来我们还是看下synchronized关键字是如何保证在同步代码块和同步方法保证同步的了。

代码语言:javascript复制
package com.wpw.asyncthreadpool;

public class SynchronizedTest0001 {
    private final Object object=new Object();
    public void test(){
        synchronized(object){
            System.out.println("同步代码块");
        }
    }
    public synchronized void test2(){
        System.out.println("同步方法");
    }

    public static void main(String[] args) {
        final SynchronizedTest0001 synchronizedTest0001=new SynchronizedTest0001();
        new Thread(()->synchronizedTest0001.test()).start();
        new Thread(()->synchronizedTest0001.test2()).start();
    }
}

在上面的我们示例程序代码中我们使用synchronized关键字进行了代码块和方法的修饰。接下来我们看下这个类的反编译字节码内容,看下synchronized关键字是有谁来保证的。

就是这个monitor监视器,我们看到test()方法在执行synchronized关键字修饰的代码块分别使用了monitorenter指令和monitorexit指令,这里仅仅提到了监视器,需要更多了解的可以看下官方说明。因为每个人理解不一样。

我们看下反编译之后synchonized关键字修饰的test2()方法的标识。

一般是synchronzied关键字修饰的方法,都是有这个flag标识ACC_SYNCHRONIZED进行限定。

0 人点赞