Java 并发编程—线程间的共享和协作(一)

线程间的共享和协作

线程间的共享和协作

线程间的共享

JVM 会为每一个线程独立分配虚拟机栈空间本地方法栈空间以及程序计数器,而对于共享内存中的变量,是对每一个线程而言是共享的,因此多线程并发访问共享内存中的变量时就会出现线程安全问题。具体可以参考JVM 内存模型这篇博客。

synchronized 内置锁

在前面提到共享资源在多个线程并发访问时会出现线程安全问题,而解决线程安全问题就是要解决以下两个问题,一是要保证共享资源的在多个线程之间是互斥访问的,二是要保证共享资源在多个线程之间的数据同步的。

我们用一张图来描述 synchronized 保证线程安全的本质原因:

Synchronized 保证线程安全的本质原因.png

从上图中,我们可以看出:

原子性:

互斥访问保证了共享变量同一时刻只有一个线程能够访问,体现了操作共享资源的原子性。

可见性:

数据同步在线程获取锁时从主存中读取共享变量的值到线程工作内存,在释放锁之前将工作内存的共享变量值刷新到主存中,这就体现了共享变量在多线程之间的可见性

对象锁

synchronized 作用于对象实例方法上,对象锁是当前 this 对象。

public class SyncTest {

    private int count;
  
    //作用于实例方法上,对象锁是当前 this 对象
    public synchronized void increase() {
        count++;
    }
}

synchronized 作用于对象实例方法内部的同步代码块上,对象锁是当前 this 对象/或者 monitor。

public class SyncTest {

    private int count;
    
    private Object monitor = new Object();

    public void increase() {
        // 对象锁是当前对象 this
        synchronized (this) {
            count++;
        }
        //对象锁是 monitor
        //synchronized (monitor) {
            //count++;
        //}
    }
}

类锁

其实类锁也是一个对象锁,为什么这样说呢?因为类锁使用的是一个类的 Class 对象作为锁, Class 是用来描述所有的类,因此使用 Class 对象也是一种对象锁,只是一般情况将其称为类锁而已。

//类锁:使用在类静态方法上
public synchronized static void change() {
    //do sth
}
//类锁:SyncTest.class对象作为对象实例方法代码块锁
public static void change2() {
    synchronized (SyncTest.class) {
        //do sth
    }
}

synchronized 注意点

synchronized 能够保证线程安全的前提是操作共享资源多个线程必须持有的是同一把锁

线程的协作

等待/通知机制

是指一个线程A调用了对象Owait()方法进入等待状态,而另一个线程B调用了对象Onotify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互。

JDK 提供实现等待/通知的 API

注意:以下方法不是 Thread 提供的,而是 Object 的。

  • wait()

如果正在执行的线程内部调用执行了该方法,那么线程将进入 WAITING 状态(线程状态可以参考(ps:劣实基础–Java 并发编程基础知识),等待其他线程通知或者线程被中断才会返回,注意: wait() 会释放当前对象锁和释放 CPU 执行权,具体可以看下面介绍的锁池等待池

  • wait(long)

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。指定时间到了不会抛出异常,而是继续往下执行。除非在 wait 期间发生了中断,那么 wait 将出异常。

  • wait (long,int)

对于超时时间更细粒度的控制,可以达到纳秒

  • notify()

通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。

  • notifyAll()

通知所有等待在该对象上的线程

等待和通知的标准范式

等待方遵循如下原则:

  • 获取对象锁。
  • 如果条件不符合,那么调用该对象的 wait()方法,被其他 notify() 之后仍要检查条件。
  • 条件满足则执行对应的逻辑。

伪代码如下:

synchronized(锁对象){
    while(条件不满足){
        锁对象.wait();
    }
    //满足条件处理对应的逻辑
}

通知方遵循如下原则:

  • 获取对象锁。
  • 改变条件。
  • 通知正在等待对象锁的线程。

伪代码如下:

synchronized(锁对象){
    改变条件
    锁对象.notifyAll();
}

一个对象拥有两个池:

  • 锁池

假设 A 线程持有对象 Object 的锁,此时其他线程想要执行该对象的某一个同步方法或者同步块,这些线程就会进入该对象的锁池中。

  • 等待池

假设 A 线程正在同步方法或者同步块中执行中调用了object.wait() ,那么线程 A 就会进入对象 object 的等待池中,等待其他线程调用该对象的 notify() 或者 notifyAll() 方法。如果其他线程调用的 object.notity() 方法,那么 CPU 会从等待池中随机取出一个线程放入锁池中,如果其他线程调用 object.notifyAll() 那么 CPU 会将等待池中所有的线程到放入到锁池中,准备争夺锁的持有权。

看了上面的等待池和锁池的作用后,这里有一个疑问:notify 和 notifyAll 应该用谁?

如果多个线程都调用了 对象锁.wait() 方法,那么如果只是调用 对象锁.notify() 方法,那么不一定会唤醒你想要的那个线程,CPU 只是随机地都等待池种去取出一个线程放入锁池中,所以说最好是使用 notifyAll();

下面举一个老王和老张买小米9手机的栗子:

等待/通知 范式的应用

public class XimaoShop implements Runnable {

    //锁
    private Object lock = new Object();

    private int xiaomi9Discount = 10;

    /*
    通知方:折扣改变的通知方法
     */
    public void depreciateXiaomi9(int discount) {

        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "收到总部通知,现在进行小米9打" + discount + "折活动,通知米粉们来买吧");
            xiaomi9Discount = discount;
            //通知客户:小米9打折了哦,赶紧去看看价格吧。
            //notify() 随机通知一个等待线程
//            lock.notify();
            //notifyAll() 通知所有等待的线程
            lock.notifyAll();
        }
    }

    /*
    等待方:查询小米9价格
     */
    public void getXiaomi9Price() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "正在查询小米9价格");
            //小米9的折扣还没低于8折,不要给我推销
            while (xiaomi9Discount > 8) {
                try {
                    System.out.println(Thread.currentThread().getName() + "发现小米9价格折扣为" + xiaomi9Discount + "太少,我要开始等待降价,老板,降价了,就通知我哦,开始等待...");
                    //等待:等待小米9降价
                    lock.wait();
                    System.out.println(Thread.currentThread().getName() + "收到通知:小米9搞活动,打折了哦,目前折扣为:" + xiaomi9Discount);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "剁手买顶配小米9:" + xiaomi9Discount + "折购入");
        }
    }
    
    @Override
    public void run() {
        getXiaomi9Price();
    }
    
    public static void main(String[] args) throws InterruptedException {
        
        XimaoShop shop = new XimaoShop();
        //老王想要买手机
        Thread getXiaomiPriceThread = new Thread(shop);
        //老张也要买手机
        Thread getXiaomiPriceThread2 = new Thread(shop);
        getXiaomiPriceThread.start();
        getXiaomiPriceThread2.start();
        Thread.sleep(1000);
        //降价了
        shop.depreciateXiaomi9(9);
        Thread.sleep(1000);
        //又降价了
        shop.depreciateXiaomi9(8);
    }
}
  • lock.notify()

根据输出结果可以看出,当降价到满足条件时,只有 Thread-1 收到通知。

等待/通知
  • lock.notifyAll() 的输出结果

根据输出结果可以看出,当降价到满足条件时,只有 Thread-1 和 Thread-2 都收到通知。

等待/通知

线程隔离ThreadLocal

ThreadLocal 即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构 ThreadLocal.ThreadLocalMap 被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个, ThreadLocal往往用来实现变量线程之间隔离

  • 定义一个 ThreadLocal,存储的是 String 类型,默认存储 subject 的值为"我是默认值"。
public class ThreadLocalTools {

    public static String subject = "我是默认值";

    public static ThreadLocal<String> sThreadLocal = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return subject;
        }
    };
}
  • 使用 ThreadLocal
package com.example.threadlocal;

public class ThreadLocalDemo {

    public static void main(String[] args) {

        Thread thread1 = new Thread("线程1") {


            @Override
            public void run() {
                super.run();
                ThreadLocalTools.sThreadLocal.set("Flutter");
                String result = ThreadLocalTools.sThreadLocal.get();
                System.out.println(Thread.currentThread().getName() + "-" + result);

                //线程执行完,要清除
                ThreadLocalTools.sThreadLocal.remove();
            }
        };

        Thread thread2 = new Thread("线程2") {

            @Override
            public void run() {
                super.run();
                ThreadLocalTools.sThreadLocal.set("Android");

                String result = ThreadLocalTools.sThreadLocal.get();
                System.out.println(Thread.currentThread().getName() + "-" + result);

                //线程执行完,要清除
                ThreadLocalTools.sThreadLocal.remove();
            }
        };

        thread1.start();
        thread2.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String result = ThreadLocalTools.sThreadLocal.get();
        System.out.println(Thread.currentThread().getName() + "-" + result);

        //线程执行完,要清除
        ThreadLocalTools.sThreadLocal.remove();
    }
}

运行结果:

线程2-Android
线程1-Flutter
main-我是默认值
我是默认值

从上面的运行结果可以看出,不同线程都拥有一个独有的 subject 的副本变量,不同线程对这个副本的修改都是针对当前线程的,对其他线程的 subject 副本变量不会造成影响。

记录于2019年4月12日

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

推荐阅读更多精彩内容