我们通常希望多个线程之间有信息的通信,而不是每个线程各自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 ... 省略,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;@Overridepublic 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 ... 省略,四个线程共卖了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;@Overridepublic void run() {while(tickets > 0) {System.out.println(Thread.currentThread().getName() + " is " +"selling ticket : " + tickets);tickets--;}}
}
运行结果:
Thread-0 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@Overridepublic 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@Overridepublic 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张票。 |