美文网首页Android开发经验谈Android技术知识Android开发
Java 并发编程—线程间的共享和协作(一)

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

作者: 未见哥哥 | 来源:发表于2019-04-12 15:32 被阅读86次

    线程间的共享和协作

    线程间的共享和协作

    线程间的共享

    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日

    相关文章

      网友评论

        本文标题:Java 并发编程—线程间的共享和协作(一)

        本文链接:https://www.haomeiwen.com/subject/auecwqtx.html