多线程(二)、内置锁synchronized

前言

在上一篇 多线程(一)、基础概念及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,执行程序可以发现。

image

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);
    }

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

image

结果不再为 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);
    }

结果:

image

发现结果也不是我们预想的 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);
    }
}

结果:

image

可以看到我们只加了一个关键字 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);
    }

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

结果:

image

我们多次运行获取结果,发现都获取不到我们期望的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方法,打印结果:

image

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

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

static 关键字:

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352