线程间协作和通信

线程操作的定义

1、如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的,各自按照线程内语义执行即可,JMM对此不需要有额外的描述了。
2、线程间操作指:一个程序执行的操作可被其他线程感知或被其他线程直接影响。

线程间操作:

1、write 要写的变量以及要写的值。
2、read 要读的变量以及可见的写入值(由此,我们可以确定可见的值)。
3、lock 要锁定的管程(监视器monitor)。
4、unlock 要解锁的管程。
5、外部操作(socket等等…)
6、启动和终止

注意: 所有线程间操作,都存在可见性问题,JMM需要对其进行规范。

对于同步的规则定义

  1. 对于监视器 m 的解锁与所有后续操作对于 m 的加锁同步
  2. 对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步
  3. 对于每个属性写入默认值(0, false,null)与每个线程对其进行的操作同步
  4. 启动线程的操作与线程中的第一个操作同步
  5. 线程 T12的最后操作与线程 T1 发现线程 T2 已经结束同步。( isAlive ,join可以判断线程是否终结)
  6. 如果线程 T1 中断了 T2,那么线程 T1 的中断操作与其他所有线程发现 T2 被中断了同步,通过抛出 InterruptedException 异常,或者调用 Thread.interrupted 或 Thread.isInterrupted

Happens-before先行发生原则

happens-before 关系用于描述两个有冲突的动作之间的顺序,如果一个action happends before 另一个action,则第一个操作被第二个操作可见 。

具体的虚拟机实现,有必要确保以下原则的成立:

  1. 某个线程中的每个动作都 happens-before 该线程中该动作后面的动作。
  2. 某个管程上的 unlock 动作 happens-before 同一个管程上后续的 lock 动作。
  3. 对某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作。
  4. 在某个线程对象上调用 start()方法 happens-before 被启动线程中的任意动作。
  5. 如果在线程t1中成功执行了t2.join(),则t2中的所有操作对t1可见。
  6. 如果某个动作 a happens-before 动作 b,且 b happens-before 动作 c,则有 a happens-before c.

补充: 当程序包含两个没有被 happens-before 关系排序的冲突访问时,就称存在数据竞争。遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存!

final在JMM中的处理

1、final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。伪代码示例:f = new finalDemo(); 读取到的 f.x 一定最新,x为final字段。

public class Demo2Final {

    final int x;
    int y;

    static Demo2Final f;

    public Demo2Final(){
        x = 3;
        y = 4;
    }

    static void writer(){
        f = new Demo2Final();
    }

    static void reader(){
        if (f!=null){
            int i = f.x;        //一定读到正确构造版本
            int j = f.y;        //可能会读到 默认值0
            System.out.println("i=" + i + ", j=" +j);
        }
    }

}

在多线程中,调用reader方法,f.x一定能读到在构造函数中的正确赋值,但是f.y却不一定。

2、如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值;伪代码示例:public finalDemo(){ x = 1; y = x; }; y会等于1;

public class Demo3Final {
    final int x;
    int y;

    static Demo2Final f;

    public Demo3Final(){
        x = 3;
        //#### 重点 语句 #####
        y = x;      //因为x被final修饰了,所以可读到y的正确构造版本
    }

    static void writer(){
        f = new Demo2Final();
    }

    static void reader(){
        if (f!=null){
            int i = f.x;        //一定读到正确构造版本
            int j = f.y;        //也能读到正确构造版本
            System.out.println("i=" + i + ", j=" +j);
        }
    }
}

在多线程中,reader方法都能读到x和y的正确值,因为x被final修饰了,所以可读到y的正确构造版本。

3、读取该共享对象的final成员变量之前,先要读取共享对象。伪代码示例: r = new ReferenceObj(); k = r.f ; 这两个操作不能重排序。

4、通常static final是不可以修改的字段 。然而System.in,System.out和System.err是static final字段,遗留原因,必须允许通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段;

Word Tearing字节处理

Java虚拟机实现的一个考虑因素是,每个字段和数组元素被认为是不同的;对一个字段或元素的更新,不得与任何其他字段或元素的读取或更新交互。特别是,分别更新字段数组的相邻元素的两个线程不得干扰或交互。

有些处理器(尤其是早期的 Alphas 处理器)没有提供写单个字节的功能。在这样的处理器上更新 byte 数组,若只是简单地读取整个内容,更新对应的字节,然后将整个内容再写回内存,将是不合法的。

这个问题有时候被称为“字分裂(word tearing)”,更新单个字节有难度的处理器,就需要寻求其它方式来解决问题。

因此,编程人员需要注意,尽量不要对byte[]中的元素进行重新赋值,更不要在多线程程序中这样做。

volatile关键字

多个线程同时访问一个共享的变量的时候,每个线程的工作内存有这个变量的一个拷贝,变量本身还是保存在共享内存中。

Violate修饰的字段,对这个变量的访问必须要从共享内存刷新一次。最新的修改写回共享内存。可以保证字段的可见性,并且禁止对象指令重排。
注意:
Violate修饰的字段绝对不是线程安全的,没有操作的原子性。
适用场景:

  1. 一个线程写,多个线程读;
  2. volatile变量的变化很固定

示例代码:

public class VolatileThread implements Runnable {

    private volatile  int a= 0;

    @Override
    public void run() {
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);
    }
}
public class VolatileTest {
    public static void main(String[] args) {
        VolatileThread volatileThread = new VolatileThread();

        Thread t1 = new Thread(volatileThread);
        Thread t2 = new Thread(volatileThread);
        Thread t3 = new Thread(volatileThread);
        Thread t4 = new Thread(volatileThread);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

输出结果:

Thread-2----3
Thread-3----4
Thread-1----3
Thread-0----3
Thread-2----5
Thread-3----8
Thread-0----8
Thread-1----8

可以看到变量a虽然用volatile修饰,是线程共享的变量,但是并不是线程安全的,输出结果是不可预测的。

理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
在这里插入图片描述
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
在这里插入图片描述
volatile写的内存语义如下:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义如下:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

本质也是通过内存屏障来实现可见性 写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回 主存。读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队 列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。

禁止指令重排序: volatile是通过内存屏障来禁止指令重排序 JMM内存屏障的策略 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

内存屏障协议很多,比较有名的是MESI ,他的本质上是使用了cpu的一个叫做 高速缓存一致性协议
现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁

X86 CPU内存屏障

sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

JSR内存屏障

LoadLoad屏障:
对于这样的语句Load1; LoadLoad; Load2, 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:
对于这样的语句Store1; StoreStore; Store2, 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:
对于这样的语句Load1; LoadStore; Store2, 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:
对于这样的语句Store1; StoreLoad; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

synchronized关键字

可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
例如在上面的VolatileThread示例代码中,在run方法里面加上synchronized锁,那么就可以保证运行结果,修改后的代码为:

public class VolatileThread implements Runnable {

    private volatile  int a= 0;

    @Override
    public void run() {
        synchronized (this){
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);

        }
    }
}

VolatileTest运行结果:

Thread-0----1
Thread-0----2
Thread-3----3
Thread-3----4
Thread-2----5
Thread-2----6
Thread-1----7
Thread-1----8

所以可以知道synchronized锁住的代码,同一时间只能有一个线程运行。

Synchronized的类锁和对象锁,本质上是两把锁,Synchronized加在静态方法上就是类锁,加在非静态的上面就是对象锁,类锁实际锁的是每一个类的class对象。对象锁锁的是当前对象实例。

示例代码:

package com.dongnaoedu.syn;

import com.dongnaoedu.threadstate.SleepUtils;

/**
 * 动脑学院-Mark老师
 * 创建日期:2017/11/28
 * 创建时间: 20:45
 * 类锁和实例锁
 */
public class InstanceAndClass {

    //测试类锁
    private static class TestClassSyn extends Thread{
        @Override
        public void run() {
            System.out.println("TestClass is going...");
            synClass();
        }
    }

    //测试类锁
    private static class TestClassSyn2 extends Thread{
        @Override
        public void run() {
            System.out.println("TestClass2 is going...");
            synClass2();
        }
    }

    //测试对象锁
    private static class TestInstanceSyn extends Thread{
        private InstanceAndClass instanceAndClass;

        public TestInstanceSyn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }

        @Override
        public void run() {
            System.out.println("TestInstance is going..."+instanceAndClass);
            instanceAndClass.synInstance();
        }

    }

    //测试对象锁
    private static class TestInstance2Syn implements Runnable{
        private InstanceAndClass instanceAndClass;

        public TestInstance2Syn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is going..."+instanceAndClass);
            instanceAndClass.synInstance2();
        }
    }

    //锁对象的方法
    private synchronized void synInstance(){
        SleepUtils.second(3);
        System.out.println("synInstance is going...");
        SleepUtils.second(3);
        System.out.println("synInstance ended");
    }

    //锁对象的方法
    private synchronized void synInstance2(){
        SleepUtils.second(3);
        System.out.println("synInstance2 going...");
        SleepUtils.second(3);
        System.out.println("synInstance2 ended");
    }

    //锁类的方法
    private static synchronized void synClass(){
        SleepUtils.second(5);
        System.out.println("synClass going...");
        SleepUtils.second(5);
    }

    //锁类的方法
    private static synchronized void synClass2(){
        SleepUtils.second(1);
        System.out.println("synClass2 going...");
        SleepUtils.second(1);
    }

    public static void main(String[] args) {
        InstanceAndClass instanceAndClass = new InstanceAndClass();
        Thread t1 = new TestClassSyn();
        Thread t4 = new TestClassSyn2();
        Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass));
        Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass));
        t2.start();
        t3.start();
        SleepUtils.second(1);
        t1.start();
        t4.start();
    }

}

输出结果:

TestInstance is going...com.dongnaoedu.syn.InstanceAndClass@698b3761
TestInstance2 is going...com.dongnaoedu.syn.InstanceAndClass@698b3761
TestClass is going...
TestClass2 is going...
synInstance is going...
synInstance ended
synClass going...
synInstance2 going...
synInstance2 ended
synClass2 going...

从输出结果可以看出,类锁和对象锁是两个不同的锁,并且当一个对象在运行一个加了该对象锁的方法时,其他线程不能用该对象运行加了该对象锁的其他方法。如上例代码中,一个线程运行着synInstance方法,在synInstance运行结束之前,那么其他线程就不能通过instanceAndClass对象调用synInstance2或者synInstance方法。

补充: 即使是同一个方法,加了对象锁,如果不是同一个对象去调用,那么是不会互斥的,因为synchronized锁的不是同一个对象,如下面,把main方法改成如下,那么t2和t3是互不干扰的,不会互斥。
把TestInstance2Syn 在run方法中调用的方法改成synInstance,让两个线程都去调用synInstance方法。

private static class TestInstance2Syn implements Runnable{
        private InstanceAndClass instanceAndClass;

        public TestInstance2Syn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is going..."+instanceAndClass);
            instanceAndClass.synInstance();
        }
    }
    public static void main(String[] args) {
        InstanceAndClass instanceAndClass1 = new InstanceAndClass();
        InstanceAndClass instanceAndClass2 = new InstanceAndClass();
        Thread t1 = new TestClassSyn();
        Thread t4 = new TestClassSyn2();
        Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass1));
        Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass2));
        t2.start();
        t3.start();
        SleepUtils.second(1);
        t1.start();
        t4.start();
    }

运行结果:

TestInstance is going...com.dongnaoedu.syn.InstanceAndClass@64669643
TestInstance2 is going...com.dongnaoedu.syn.InstanceAndClass@7e437185
TestClass is going...
TestClass2 is going...
synInstance is going...
synInstance is going...
synClass going...
synInstance ended
synInstance ended
synClass2 going...

从结果上看,t2和t3是可以并发执行的。所以即使加了synchronized的对象方法,不同对象去调用是不会互斥的,是可以并发执行的。

等待和通知机制

等待方原则:
1、获取对象锁
2、如果条件不满足,调用对象的wait方法,被通知后依然要检查条件是否满足
3、条件满足以后,才能执行相关的业务逻辑
wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。
格式:

Synchronized(对象){
	While(条件不满足){
		对象.wait()
	}
	业务逻辑处理
}

通知方原则:
1、获得对象的锁;
2、改变条件;
3、通知所有等待在对象的线程
notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。
格式:

Synchronized(对象){
	业务逻辑处理,改变条件
	对象.notify/notifyAll
}

示例代码:

public class BlockingQueueWN<T> {

    private List queue = new LinkedList<>();
    private final int limit;

    public BlockingQueueWN(int limit) {
        this.limit = limit;
    }

    //入队
    public synchronized void enqueue(T item) throws InterruptedException {
        while(this.queue.size()==this.limit){
            wait();
        }
        //将数据入队,可以肯定有出队的线程正在等待
        if (this.queue.size()==0){
            notifyAll();
        }
        this.queue.add(item);
    }

    //出队
    public synchronized T dequeue() throws InterruptedException {
        while(this.queue.size()==0){
            wait();
        }
        if (this.queue.size()==this.limit){
            notifyAll();
        }
        return (T)this.queue.remove(0);
    }
}

public class BqTest {
    public static void main(String[] args) {
        BlockingQueueWN bq = new BlockingQueueWN(10);
        Thread threadA = new ThreadPush(bq);
        threadA.setName("Push");
        Thread threadB = new ThreadPop(bq);
        threadB.setName("Pop");
        threadB.start();
        threadA.start();
    }

    //推数据入队列
    private static class ThreadPush extends Thread{
        BlockingQueueWN<Integer> bq;

        public ThreadPush(BlockingQueueWN<Integer> bq) {
            this.bq = bq;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            int i = 20;
            while(i>0){
                try {
                    Thread.sleep(1000);
                    System.out.println(" i="+i+" will push");
                    bq.enqueue(i--);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }

            }
        }
    }

    //取数据出队列
    private static class ThreadPop extends Thread{
        BlockingQueueWN<Integer> bq;

        public ThreadPop(BlockingQueueWN<Integer> bq) {
            this.bq = bq;
        }
        @Override
        public void run() {
            while(true){
                try {
                    System.out.println(Thread.currentThread().getName()
                            +" will pop.....");
                    Integer i = bq.dequeue();
                    System.out.println(" i="+i.intValue()+" alread pop");
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }
            }

        }
    }
}

注意:
1、虽然会wait自动解锁,但是对顺序有要求, 如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。
2、这些方法只能由同一对象锁的持有者线程调用,也就是写在同步块里面,否则会抛出IllegalMonitorStateException异常。

join方法

线程A,执行了thread.join(),线程A等待thread线程终止了以后,A在join后面的语句才会继续执行.
示例代码:

public class JoinTest {

    static class CutInLine implements Runnable{

        private Thread thread;

        public CutInLine(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                //在被插队的线程里,调用一下插队线程的join方法
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" will work");
        }
    }

    public static void main(String[] args) {
        Thread previous = Thread.currentThread();
        for(int i=0;i<10;i++){
            Thread thread =
                    new Thread(new CutInLine(previous),String.valueOf(i));
            System.out.println(previous.getId()+" cut in the thread:"+thread.getName());
            thread.start();
            previous = thread;
        }

    }

}

运行结果:

1 cut in the thread:0
12 cut in the thread:1
13 cut in the thread:2
14 cut in the thread:3
15 cut in the thread:4
16 cut in the thread:5
17 cut in the thread:6
18 cut in the thread:7
19 cut in the thread:8
20 cut in the thread:9
0 will work
1 will work
2 will work
3 will work
4 will work
5 will work
6 will work
7 will work
8 will work
9 will work

这里在main方法启动的10个线程中,每个线程都是得在上一个线程执行完成(终止)之后,才会执行自己的输出。

park/unpark机制

线程调用park则等待“许可”,处于等待状态,unpark方法为指定线程提供“许可(permit)”,线程继续运行。
补充:调用unpark之后,再调用park,线程会直接运行。
提前调用的unpark不叠加,连续多次调用unpark后,第一次调用park后会拿到“许可”直接运行,后续调用会进入等待。
代码示例:

package com.dongnao.concurrent.period2;

import java.util.concurrent.locks.LockSupport;

public class Demo9_ParkUnpark {

    public static void main(String args[]) throws Exception {
        Demo9_ParkUnpark demo = new Demo9_ParkUnpark();
        demo.test1_normal();
        //demo.test2_DeadLock();
    }

    public static Object iceCream = null;

    /** 正常的park/unpark */
    public void test1_normal() throws Exception {
        //开启一个线程,代表小朋友
        Thread consumerThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (iceCream == null) {     // 若没有冰激凌
                    System.out.println("没有冰激凌,小朋友不开心,等待...");
                    LockSupport.park();
                }
                System.out.println("小朋友买到冰激凌,开心回家");
            }
        });
        consumerThread.start();

        Thread.sleep(3000L);    // 3秒之后
        iceCream = new Object();    //店员做了一个冰激凌

        LockSupport.unpark(consumerThread);     //通知小朋友

        System.out.println("通知小朋友");
    }

    /** 死锁的park/unpark */
    public void test2_DeadLock() throws Exception {
        //开启一个线程,代表小朋友
        Thread consumerThread = new Thread(new Runnable() {
            @Override
            public void run() {
                if (iceCream == null) {     // 若没有冰激凌
                    System.out.println("没有冰激凌,小朋友不开心,等待...");

                    synchronized (this) {   // 若拿到锁
                        LockSupport.park();     //执行park
                    }
                }
                System.out.println("小朋友买到冰激凌,开心回家");
            }
        });
        consumerThread.start();

        Thread.sleep(3000L);    // 3秒之后
        iceCream = new Object();    //店员做了一个冰激凌

        synchronized (this) {   // 争取到锁以后,才能恢复consumerThread
            LockSupport.unpark(consumerThread);
        }
        System.out.println("通知小朋友");
    }
}

伪唤醒

注意:
之前代码中用 if 语句来判断,是否进入等待状态,这样的做法是错误的!

官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

伪唤醒是指线程并非因为notify、notifyall、unpark等api调用而意外唤醒,是更底层原因导致的。

Disruptor

介绍

主页:http://lmax-exchange.github.io/disruptor/

源码:https://github.com/LMAX-Exchange/disruptor

GettingStarted: https://github.com/LMAX-Exchange/disruptor/wiki/Getting-Started

api: http://lmax-exchange.github.io/disruptor/docs/index.html

maven: https://mvnrepository.com/artifact/com.lmax/disruptor

Disruptor的特点

对比ConcurrentLinkedQueue : 链表实现

JDK中没有ConcurrentArrayQueue

Disruptor是数组实现的

无锁,高并发,使用环形Buffer,直接覆盖(不用清除)旧的数据,降低GC频率

实现了基于事件的生产者消费者模式(观察者模式)

RingBuffer

环形队列

RingBuffer的序号,指向下一个可用的元素

采用数组实现,没有首尾指针

对比ConcurrentLinkedQueue,用数组实现的速度更快

假如长度为8,当添加到第12个元素的时候在哪个序号上呢?用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值