1. 线程间的通信和协调、 协作
线程开始运行, 拥有自己的栈空间, 就如同一个脚本一样, 按照既定的代码一步一步地执行, 直到终止。但是, 每个运行中的线程, 如果仅仅是孤立地运行, 那么没有一点儿价值,或者说价值很少。如果多个线程能够相互配合完成工作,这就离不开线程间的通信和协调、 协作,包括数据之间的共享,协同处理事情,这将会带来巨大的价值。
1.1. 管道输入输出流
我们已经知道,进程间有好几种通信机制,其中包括了管道,其实 Java 的线程里也有类似的管道机制, 用于线程之间的数据传输, 而传输的媒介为内存。
设想这么一个应用场景:通过 Java 应用生成文件, 然后需要将文件上传到云端,比如:
1 、页面点击导出后, 后台触发导出任务, 然后将 mysql 中的数据根据导出条件查询出来, 生成 Excel 文件, 然后将文件上传到 oss,最后发布一个下载文件的链接。
2、和银行以及金融机构对接时,从本地某个数据源查询数据后, 上报 xml 格式的数据,给到指定的 ftp、或是 oss 的某个目录下也是类似的。
我们一般的做法是, 先将文件写入到本地磁盘, 然后从磁盘读出文件上传到云盘,但是通过 Java 中的管道输入输出流一步到位,则可以避免写入磁盘这 一步。
Java 中的管道输入/输出流主要包括了如下 4 种具体实现:
管道字节流:PipedOutputStream 、PipedInputStream ;管道字符流:PipedReader 和 PipedWriter
/**
* main线程和PrintThread线程之间传输数据
*/
public class Pipeline {
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
/* 将输出流和输入流进行连接,否则在使用时会抛出IOException*/
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
/*将键盘的输入,用输出流接受,在实际的业务中,可以将文件流导给输出流*/
while ((receive = System.in.read()) != -1){
out.write(receive);
}
} finally {
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive = 0;
try {
/*输入流从输出流接收数据,并在控制台显示
*在实际的业务中,可以将输入流直接通过网络通信写出 */
while ((receive = in.read()) != -1){
System.out.print((char) receive);
}
} catch (IOException ex) {
}
}
}
}
总结
管道流中,入口处是输出流,需要输出流往管道中写数据(往管道中输出数据)。出口处是输入流,需要输入流从管道中消费(读)数据。
1.2. join 方法
join(),把指定的线程加入到当前线程, 可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的Join()方法, 直到线程 A 执行完毕后, 才会继续执行线程 B 剩下的代码。
/**
*类说明:演示Join()方法的使用
*/
public class UseJoinDemo {
public static void main(String[] args) throws Exception {
System.out.println(Thread.currentThread().getName() + "线程开始执行...");
Thread threadA = new Thread(new RunnableA());
threadA.setName("ThreadA");
Thread threadB = new Thread(new RunnableB(threadA));
threadB.setName("ThreadB");
threadA.start();
threadB.start();
threadB.join();
Thread.sleep(2000);//让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + "线程执行完成...");
}
static class RunnableA implements Runnable {
public void run() {
System.out.println("ThreadA开始执行...");
try {
Thread.sleep(2000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 执行结束...");
}
}
static class RunnableB implements Runnable {
private Thread thread;
public RunnableB(Thread thread) {
this.thread = thread;
}
public RunnableB() {}
public void run() {
System.out.println("ThreadB开始执行...");
try {
if(thread!=null) thread.join();
// Thread.sleep(2000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 执行结束...");
}
}
}
/**
*类说明:无Join时线程的表现
*/
public class NoUseJoin {
public static void main(String[] args) throws Exception {
System.out.println(Thread.currentThread().getName() + "线程开始执行...");
Thread threadA = new Thread(new RunnableA());
threadA.setName("ThreadA");
Thread threadB = new Thread(new RunnableB());
threadB.setName("ThreadB");
threadA.start();
threadB.start();
Thread.sleep(2000);//让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + " 执行完成...");
}
static class RunnableA implements Runnable {
public void run() {
System.out.println("ThreadA开始执行...");
try {
Thread.sleep(2000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 执行结束...");
}
}
static class RunnableB implements Runnable {
private Thread thread;
public RunnableB(Thread thread) {
this.thread = thread;
}
public RunnableB() {}
public void run() {
System.out.println("ThreadB开始执行...");
try {
Thread.sleep(2000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 执行结束...");
}
}
}
面试题
现在有 T1、T2、T3 三个线程, 你怎样保证 T2 在 T1 执行完后执行, T3 在 T2 执行完后执行?
答:用 Thread#join 方法即可, 在 T3 中调用 T2.join,在 T2 中调用 T1.join。
1.3. synchronized 内置锁
Java 支持多个线程同时访问一个对象或者对象的成员变量。但是,多个线程同时访问同一个变量,线程之间的执行顺序是不可预知的,会产生不可预料的结果。如下这段程序,每次执行后的结果是不固定的,这便是出现了线程安全问题。
public class SynDemo {
private int count =0;
public long getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public void incrementCountNoSyn(){
count++;
}
//线程
private static class Count extends Thread {
private SynDemo synDemo;
public Count(SynDemo synDemo) {
this.synDemo = synDemo;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synDemo.incrementCountNoSyn();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynDemo synDemo = new SynDemo();
//启动两个线程
Count count1 = new Count(synDemo);
Count count2 = new Count(synDemo);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(synDemo.count);//20000?
}
}
运行结果
12907
为此,Java提供了synchronized关键字来解决线程安全问题。我们先来看下使用synchronized关键字之后的效果
/**
*类说明:synchronized关键字的使用方法
*/
public class SynDemo {
private int count = 0;
private Object obj = new Object();//作为一个锁
public long getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
/*无锁时*/
public void incrementCountNoSyn(){
count++;
}
/*synchronized直接放在方法上*/
public synchronized void incrementCountMethod(){
count++;
}
/*用在同步块上*/
public void incrementCountBlock(){
synchronized (this){
count++;
}
}
/*用在同步块上,但是锁的是单独的对象实例*/
public void incrementCountObj(){
synchronized (obj){
count++;
}
}
//线程
private static class SyncThread extends Thread {
private SynDemo synDemo;
public SyncThread(SynDemo synDemo) {
this.synDemo = synDemo;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// synDemo.incrementCountNoSyn();
synDemo.incrementCountMethod();
// synDemo.incrementCountBlock();
// synDemo.incrementCountObj();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynDemo synDemo = new SynDemo();
//启动两个线程
SyncThread syncThread1 = new SyncThread(synDemo);
SyncThread syncThread2 = new SyncThread(synDemo);
syncThread1.start();
syncThread2.start();
Thread.sleep(50);
System.out.println(synDemo.count);//20000
}
}
运行结果
20000
1.3.1. 概念及特性
概念
synchronized是java中的关键字,可以在需要线程安全的业务场景中进行使用,保证线程安全,它是利用锁机制来实现同步的。
synchronized 可以修饰方法或者以同步块的形式进行使用,它主要确保多个线程在同一个时刻, 只能有一个线程处于方法或者同步块中(将并发的访问变成串行的访问),它保证了线程对变量访问的可见性和排他性, 使多个线程访问同一个变量的结果正确(保证线程安全),它又称为内置锁机制。
特性
- 原子性:同一时间只允许一个线程持有某个对象锁,对需同步的代码块进行访问,是一种排他的机制。因此,被synchronized关键字修饰的同步代码块是无法被中途打断的,能保证代码的原子性;
- 可见性:synchronized关键字包括monitor enter和monitor exit两个JVM命令,它能保证在任何时候任何线程,执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存;
- 有序性:synchronized的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter;这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码块中代码指令也会发生指令重排序的情况;
1.3.2. 用法分类
1. 按照修饰对象
● 修饰代码块
synchronized(this|object) {}
synchronized(类.class) {}
● 修饰方法
修饰非静态方法
修饰静态方法
2. 按照获取的锁分类
● 获取对象锁
synchronized(this|object) {}
修饰非静态方法
● 获取类锁
synchronized(类.class) {}