JAVA多线程和并发编程(三)- JAVA多线程信息共享

        我们通常希望多个线程之间有信息的通信,而不是每个线程各自run方法执行完就结束了。那么多个线程间如何通信呢?

        Java中多线程通常通过共享变量进行信息共享。

        1)使用static变量共享信息,该方法适用于通过继承Thread类创建线程的方式。

        2)通过同一个Runnable实例的成员变量来共享信息,该方法适用于通过实现Runnable接口创建线程的方式。

        JDK原生库不支持线程间点对点发送消息(类似C/C++的MPI并行库, MPI是一个信息传递应用程序接口,至今仍然是高性能计算的主要模型。MPI支持线程0向线程1发送一条消息,或者线程0向所有线程群发一条消息)。

一、使用static变量共享信息

public class ThreadMsgShareTest {
    public static void main(String[] args) {
        new Thread1().start();
        new Thread1().start();
        new Thread1().start();
        new Thread1().start();
    }
}

class Thread1 extends Thread {
    private static int tickets = 100; // 所有线程一起卖100张票
    public void run() {
        while(tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " is " +
                    "selling ticket : " + tickets);
            tickets --;
        }
    }
}

        如上代码中, Thread1定义了一个静态变量,则所有Thread1实例共享同一个tickets变量。在main函数中,创建了4个不同的Thread1对象,启动四个线程,这些线程共同拥有100张票。

        程序运行结果如下:

Thread-0 is selling ticket : 100
Thread-0 is selling ticket : 99
Thread-0 is selling ticket : 98
Thread-0 is selling ticket : 97
Thread-0 is selling ticket : 96
Thread-3 is selling ticket : 100
Thread-3 is selling ticket : 94
Thread-3 is selling ticket : 93

... 省略,4个线程共卖了103张票,说明出现了信息同步问题

        可以看到线程0和线程3都卖了第100张票,说明存在信息同步问题。

        如果将Thread1的静态变量改成普通的成员变量,则tickes将成为每个线程对象各自的成员变量,即每个线程都拥有100张票,如下代码所示:

public class ThreadMsgShareTest {
    public static void main(String[] args) {
        new Thread1().start();
        new Thread1().start();
        new Thread1().start();
        new Thread1().start();
    }
}

class Thread1 extends Thread {
//    private static int tickets = 100;
    private int tickets = 100; // 每个线程卖100张票
    public void run() {
        while(tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " is " +
                    "selling ticket : " + tickets);
            tickets --;
        }
    }
}

        该代码运行后,每个线程独立售卖100张票,四个线程一共卖了400张票。

二、使用同一个Runnable实例的成员变量共享信息

public class RunnableMsgShareTest {
    public static void main(String[] args) {
        Runnable1 runnable1 = new Runnable1();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
    }
}

class Runnable1 implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while(tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " is " +
                    "selling ticket : " + tickets);
            tickets --;
        }
    }
}

        如上代码中,虽然Runnable1类中的tickets是普通成员变量,但同一个Runnable实例可以在多个线程对象间共享,如上main函数中,虽然创建了4个线程对象,但它们的target都是同一个Runnable1对象,所以四个线程操作的tickets都是同一个实例的变量,从而达到信息共享的效果。

        运行结果如下:

Thread-1 is selling ticket : 100
Thread-3 is selling ticket : 100
Thread-2 is selling ticket : 100
Thread-2 is selling ticket : 97
Thread-0 is selling ticket : 100
Thread-2 is selling ticket : 96
Thread-3 is selling ticket : 98
Thread-3 is selling ticket : 93

... 省略,四个线程共卖了103张票,说明出现了信息同步问题

        如上运行结果说明,四个线程确实共享了同一个tickets变量,由于没有进行共享资源的同步控制,因此出现了信息共享问题。

        如上代码中,如果将main函数的代码改成每个线程对象都拥有不同的runnable实例,则每个线程都独立售卖100张票,不存在tickets信息的共享。

通过如上两种方式,虽然实现了信息共享,但却带来了多线程操作同一资源出现的信息不一致问题。下面介绍如何解决信息不一致的问题。

三、信息不同步的原因 - 工作缓存副本

        JVM中每个线程都有一个工作缓存,线程会从主存中加载操作数到工作缓存中生成一个副本,CPU对副本进行运算操作后,将结果写入工作缓存,最后数据才从工作缓存中刷新到主存中。

        线程的运行都是依赖工作缓存的,线程1对工作缓存中副本的修改,对线程2和线程3是不可见的,它们的工作缓存中还是原来的值,这就出现了数据不一致的问题。

四、实现多线程信息同步的方案

方案一:volatile 关键字,实现内存可见性

        解决工作缓存副本问题,用于保证多线程对共享变量操作时的可见性。用volatile关键字修饰的变量,如果在工作缓存中被修改,会立即刷新到主存,且同步失效其他线程工作缓存中该变量的值。即volatile关键字修饰的变量如有改变,会及时通知给所有线程

        注意:volatile关键字修饰的变量只能进行原子操作,适用于修饰flag=true/false这种标记型字段,volatile对复合操作无效。

volatile底层原理

        如果将使用volatile修饰的代码和未使用volatile修饰的代码都编译成汇编语言,会发现,使用volatile修饰的代码会多出一个lock前缀指令。

        lock前缀指令相当于一个内存屏障,内存屏障的作用有以下三点:

        ①重排序时,不能把内存屏障后面的指令排序到内存屏障前

        ②使得本CPU的cache写入内存

        ③写入动作会引起其他CPU缓存或内核的数据无效,相当于修改对其他线程可见。

        上文卖票的示例中,由于tickets--是复合操作,因此对tickets变量增加volatile修饰,仍然解决不了多线程同步问题。

public class RunnableMsgShareTest {
    public static void main(String[] args) {
        Runnable1 runnable1 = new Runnable1();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
    }
}

class Runnable1 implements Runnable {
    // volatile对多线程中有复合操作的变量无效
    private volatile int tickets = 100;

    @Override
    public void run() {
        while(tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " is " +
                    "selling ticket : " + tickets);
            tickets--;
        }
    }
}

        运行结果:

Thread-0 is selling ticket : 100
Thread-0 is selling ticket : 99
Thread-0 is selling ticket : 98
Thread-0 is selling ticket : 97
Thread-0 is selling ticket : 96
Thread-3 is selling ticket : 100
Thread-2 is selling ticket : 100

... 省略,多个线程售卖第100张票,仍然有多线程同步问题

         下面的代码中用volatile修饰布尔型变量,实现多线程信息同步:

public class VolatileTest {
    public static void main(String[] args) throws InterruptedException {
        Runnable2 runnable2 = new Runnable2();
        new Thread(runnable2).start();
        Thread.sleep(10);
        runnable2.flag = false;
        System.out.println("main thread existing");
    }
}

class Runnable2 implements Runnable {
    public volatile boolean flag = true;

    @SneakyThrows
    @Override
    public void run() {
        while (flag) {

        }
        System.out.println("sub thread existing");
    }
}

        运行结果:

main thread existing
sub thread existing

        可以看到,子线程初始运行时flag=true,于是进入while循环。之后main线程修改了flag=false,子线程立即看到了该变化,退出了while循环。

        如果将上述代码中flag去掉volatile修饰,则子线程会进行死循环,不会退出,运行结果如下:

main thread existing

        该运行结果说明,主线程已经退出了,内存中的flag已经为false,但子线程中的flag是使用其工作缓存中的值,该值仍然为true,因此子线程一直在运行,没有退出。

 方案二:synchronized 关键字 对代码块/函数加锁,实现指定代码段的互斥访问。

        互斥:某个线程运行一个代码段(关键区),其他线程不能同时运行这个代码段。互斥是同步的一种特例。互斥的关键字是synchronized.

        同步:多个线程的运行,必须按照某种规定的先后顺序来运行。

        synchronized关键字修饰的代码块/函数,同一时刻只能一个线程访问。

        synchronized加大性能负担,但是使用简便。

        synchronized如果修饰代码段,则必须加锁在某一个对象上,只要是一个非空的对象都可以。所有线程要执行关键区的代码,必须先抢到这把锁。

        代码示例:上文卖票的示例中,抽取一个卖票的函数,用synchronized修饰,实现多线程正确访问共享变量

public class SynchronizedTest {
    public static void main(String[] args) {
        Runnable1 runnable1 = new Runnable1();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();
    }
}

class Runnable1 implements Runnable {
    private int tickets = 100;

    @SneakyThrows
    @Override
    public void run() {
        while(true) {
            sale();
            Thread.sleep(10); // 为了方便线程阻塞后切换另一个线程
            if(tickets <= 0) {
                break;
            }
        }
    }

    private synchronized void sale() {
        if(tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " is " +
                    "selling ticket : " + tickets);
            tickets--;
        }
    }
}

        运行结果:

Thread-0 is selling ticket : 100
Thread-3 is selling ticket : 99
Thread-2 is selling ticket : 98
Thread-1 is selling ticket : 97
Thread-0 is selling ticket : 96
Thread-3 is selling ticket : 95
Thread-2 is selling ticket : 94
Thread-1 is selling ticket : 93
Thread-0 is selling ticket : 92
Thread-1 is selling ticket : 91
Thread-2 is selling ticket : 90
Thread-3 is selling ticket : 89

... 四个线程按顺序售卖了第100 到第1张票。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值