多线程(二)、内置锁 synchronized

2,126 阅读13分钟

前言

在上一篇 多线程(一)、基础概念及notify()和wait()的使用 文章中我们讲了多线程的一些基础概念还有等待通知机制,在讲线程之间共享资源的时候,提到会出现数据不同步问题,我们先通过一个示例来演示这个问题。

/**
 * @author : EvanZch
 *         description:
 **/

public class SynchronizedTest {

    // 赋count初始值为0
    public static int count = 0;
    // 进行累加操作
    public void add() {
        count++;
    }

    public static class TestThread extends Thread {
        private SynchronizedTest synchronizedTest;

        public TestThread(SynchronizedTest synchronizedTest) {
            this.synchronizedTest = synchronizedTest;
        }
        @Override
        public void run() {
            super.run();
            // 执行10000次累加
            for (int x = 0; x < 10000; x++) {
                synchronizedTest.add();
            }
        }
    }
    public int getCount() {
        return count;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        // 开启两个线程
        new TestThread(synchronizedTest).start();
        new TestThread(synchronizedTest).start();
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
}

可以看到,我程序中我们启动了两个线程,同时对 Count 变量进行累加操作,每个线程循环累加10000次,我们预想的结果,获取的count值应该会是20000,执行程序可以发现。

0?为什么结果会是0?因为我们在main里面开启线程执行,方法是顺序执行,当执行到 输出语句的时候,线程run方法还没有启动,所以这里打印的是count的初始值 0;

怎么获取到正确结果?

1、等待一会在获取结果

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new TestThread(synchronizedTest).start();
        new TestThread(synchronizedTest).start();
        // 等待一秒再回去结果
        Thread.sleep(1000);
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }

我们在获取结果之前,先等待一秒,结果如下:

结果不再为 0 ,但是结果也不是我们预想的 20000啊,难道是等待时间不够?我们增加等待时间,在运行,发现结果也不是20000,这么看,使用等待时间不严谨,因为没办法判断线程执行结束时间(其实线程执行很快的,远不需要几秒),那我们可以使用 join方法。

2、thread.join()

我们先看一下 thread 的 join方法

    /**
     * Waits for this thread to die.
     *
     * <p> An invocation of this method behaves in exactly the same
     * way as the invocation
     *
     * <blockquote>
     * {@linkplain #join(long) join}{@code (0)}
     * </blockquote>
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */

    public final void join() throws InterruptedException {
        join(0);
    }

注释大概意思是:当调用join方法后,会进行阻塞,直到该线程任务执行结束。

可以让线程顺序执行。

那我们可以简单修改代码,让两个线程执行结束后再打印结果

这里需要注意,我们是在 main 这个线程里面调用 join 方法, 则两个线程会在main 线程阻塞,但是两个子线程还是在并行处理,都执行结束后才会唤醒 main 线程执行后续操作。

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        TestThread testThread = new TestThread(synchronizedTest);
        TestThread testThread1 = new TestThread(synchronizedTest);
        testThread.start();
        testThread1.start();
        // 让程序顺序执行
        testThread.join();
        testThread1.join();
        // 当两个线程任务结束后再获取结果
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }

结果:

发现结果也不是我们预想的 20000,我们使用了 join() 方法,它会在调用线程进行阻塞(main),当testThreadtestThread1 都执行结束后再唤醒调用线程 , 能确保两个线程肯定是执行结束了的,可是结果跟预期不一致,多次打印,发现结果一直在 10000 ~ 20000 这个区间波动。

为什么会出现这种情况?

上一篇文章讲过,同一个进程的多个线程共享该进程的所有资源,当多个线程同时访问一个对象或者一个对象的成员变量,可能会导致数据不同步问题,比如 线程A数据a进行操作,需要从内存中进行读取然后进行相应的操作,操作完成后再写入内存中,但是如果数据还没有写入内存中的时候,线程B 也来对这个数据进行操作,取到的就是还未写入内存的数据,导致前后数据同步问题,我们也叫线程不安全操作

比如 线程 A 取到 count 的时候,其值为 100,加 1 后再放入内存中,如果在放入内存之前 线程B 也来拿 count 并对其进行累加操作,这个时候 **线程B **取到的 count 值 还是100,加 1 后放入内存,这个时候值为101, 这样 线程 A 进行累加的那步操作就没有被算上,这就是为啥,最后两个线程算出来的结果肯定是小于 20000。

怎么避免这种情况?

我们知道出现这种情况的原因是操作的时候,因为多个线程同时访问一个对象或者对象的成员变量,要处理这个问题,我们就引入了关键字 synchronized

正文

一、内置锁 synchronized

关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性排他性,又称为内置锁机制

锁又分为对象锁和类锁:

对象锁: 对一个对象实例进行锁操作,实例对象可以有多个,不同对象实例的对象锁互不干扰。

类锁:用于类的静态方法或者类的Class对象进行锁操作。我们知道每个类只有一个Class对象,也就只有一个类锁。

注意点:

类锁只是一个概念上的东西,它锁的也是对象,只不过这个对象是类的Class对象,其唯一存在。

类锁和对象锁之间互不干扰。

通过上面的案例,我们简单改改,我们在执行累加方法上加上 synchronized 关键字,然后再运行。

/**
 * @author : EvanZch
 *         description:
 **/

public class SynchronizedTest {

    public static int count = 0;

    // 我们对add方法添加关键字 synchronized
    public synchronized void add() {
        count++;
    }

    public static class TestThread extends Thread {
        private SynchronizedTest synchronizedTest;

        public TestThread(SynchronizedTest synchronizedTest) {
            this.synchronizedTest = synchronizedTest;
        }

        @Override
        public void run() {
            super.run();
            for (int x = 0; x < 10000; x++) {
                synchronizedTest.add();
            }
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        TestThread testThread = new TestThread(synchronizedTest);
        TestThread testThread1 = new TestThread(synchronizedTest);
        testThread.start();
        testThread1.start();

        // 让程序顺序执行
        testThread.join();
        testThread1.join();

        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
}

结果:

可以看到我们只加了一个关键字 synchronized ,结果就跟我们预期的 20000 一致,我们将 synchronized

添加到方法上,就确保了多个线程同一时刻只有一个线程对此方法进行操作,这样就确保了线程安全问题。

前面说了内置锁存在对象锁类锁 ,我们来看一下具体怎么实现和区别。

1.1、对象锁

对一个对象实例进行锁操作,实例对象可以有多个,不同对象实例的对象锁互不干扰。

我们在前面的示例上进行更改。

方法锁:

    // 非静态方法
    public synchronized void add() {
        count++;
    }

同步代码块锁:

    public void add(){
        synchronized (this){
            count ++;
        }
    }

或者:

    // 非静态变量
    public Object object = new Object();
    public void add(){
        synchronized (object){
            count ++;
        }
    }

我们可以看到对象锁都是对非静态方法和非静态变量进行加锁,以上三种从本质上来说没有区别,我们这个时候再改一下我们的示例代码,来验证一下 不同对象实例的对象锁互不干扰

    public static void main(String[] args) throws InterruptedException {

        SynchronizedTest synchronizedTest = new SynchronizedTest();
        // 我们再创建一个 SynchronizedTest 对象
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        // 传入 synchronizedTest 
        TestThread testThread = new TestThread(synchronizedTest);
        // 传入 synchronizedTest1
        TestThread testThread1 = new TestThread(synchronizedTest1);

        testThread.start();
        testThread1.start();
        // 让程序顺序执行
        testThread.join();
        testThread1.join();

        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }

我们开启两个线程,分别传入了不同的实例对象,这个时候再多次运行,查看运行结果。

结果:

我们多次运行获取结果,发现都获取不到我们期望的20000,可以我们明明也在add() 方法上添加了 synchronized 啊,唯一不同的就是,两个线程传入了不同的对象,所以通过结果,我们可以得出,不同对象的对象锁之间,是互不影响,各种运行。

1.2、类锁

用于类的静态方法或者类的Class对象进行锁操作。我们知道每个类只有一个Class对象,也就只有一个类锁。

类锁其实也是对象锁,只不过锁的对象比较特殊。

静态方法锁:

    // 静态方法
    public static synchronized void add() {
        count++;
    }

同步代码块锁:

    public void add(){
        // 传入Class对象
        synchronized (SynchronizedTest.class){
            count ++;
        }
    }

或者:

    // 静态成员变量
    public static Object object = new Object();
    public void add(){
        synchronized (object){
            count ++;
        }
    }

我们知道静态变量和类的Class对象在内存中只存在一个,所以我们对add方法通过类锁方式进行加锁,不管外界这个时候传的对象有多少个,它也是唯一的,我们再执行上面的main方法,打印结果:

可以看到结果和期望一致。

知识拓展 :static 关键字和 new 一个对象,做了什么操作?

static 关键字:

  • 静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且 JVM 也只会为它分配一次内存,同时类所有的实例都共享静态变量,即一处变、处处变,可以直接通过类名来访问它。
  • 但是实例变量则不同,它是伴随着new实例化的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。

new 一个对象,底层做了啥?
1、Jvm加载未加载的字节码,开辟空间
2、静态初始化(1静态代码块和2静态变量)
3、成员变量初始化(1普通代码块和2普通成员变量)
4、构造器初始化(构造函数)