多线程并发学习(五):线程间的协作/通信

1. 线程同步synchronized和volatile

要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性。多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现。

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,用于保护共享数据,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。使用synchronized修饰的方法或者代码块可以看成是一个原子操作。

public ThreadTest implements Runnable{ 
    public synchronized void run(){ 
        for(int i=0;i<10;i++){ 
            System.out.println(" " + i); 
        } 
    } 
    public static void main(String[] args) { 
        Runnable r1 = new ThreadTest(); 
        Runnable r2 = new ThreadTest(); 
        Thread t1 = new Thread(r1); 
        Thread t2 = new Thread(r2); 
        t1.start(); 
        t2.start(); 
    } 
}
//以上这段程序中的 i 变量并不是共享数据,也就是这里的Synchronized关键字并未起作用。因为t1,t2两个线程是两个对象(r1,r2)的线程。//不同的对象其数据是不同的,所以r1和r2两个对象的i变量是并不是共享数据。 
//当把代码改成如下:Synchronized关键字才会起作用 
   Runnable r = new ThreadTest(); 
    Thread t1 = new Thread(r); 
    Thread t2 = new Thread(r); 
    t1.start(); 
    t2.start();

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新到共享内存,它能保证所有线程对变量访问的可见性。 

volatile是第二种Java多线程同步的机制,根据JLS(Java LanguageSpecifications)的说法,一个变量可以被volatile修饰,在这种情况下内存模型(主内存和线程工作内存)确保所有线程可以看到一致的变量值,来看一段代码:

class Test {
    static int i = 0, j = 0;
    static void one() {
        i++;
        j++;
    }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

   一些线程执行one方法,另一些线程执行two方法,two方法有可能打印出j比i大的值,按照之前分析的线程执行过程分析一下:将变量i从主内存拷贝到工作内存;改变i的值;刷新主内存数据;将变量j从主内存拷贝到工作内存;改变j的值;刷新主内存数据;这个时候执行two方法的线程先读取了主存i原来的值又读取了j改变后的值,这就导致了程序的输出不是我们预期的结果,要阻止这种不合理的行为的一种方式是在one方法和two方法前面加上synchronized修饰符:

class Test {
    static int i = 0, j = 0;
    static synchronized void one() {
        i++;
        j++;
    }
    static synchronized void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

根据前面的分析,我们可以知道,这时one方法和two方法再也不会并发的执行了,i和j的值在主内存中会一直保持一致,并且two方法输出的也是一致的。另一种同步的机制是在共享变量之前加上volatile(volatile和final不能同时修饰一个字段):

class Test {
    static volatile int i = 0, j = 0;
    static void one() {
        i++;
        j++;
    }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

2. 线程协作-生产者/消费者问题

在很多情况下,仅仅同步是不够的,还需要线程与线程协作(通信),生产者/消费者问题是一个经典的线程同步以及通信的案例。该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”和“消费者”在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者,通常采用线程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

(1)以下讲述了JDK5之前传统线程的通信方式:

假设有这样一种情况,有一个盘子,盘子里只能放一个鸡蛋,A线程专门往盘子里放鸡蛋,如果盘子里有鸡蛋,则一直等到盘子里没鸡蛋,B线程专门从盘子里取鸡蛋,如果盘子里没鸡蛋,则一直等到盘子里有鸡蛋。这里盘子是一个互斥区,每次放鸡蛋是互斥的,每次取鸡蛋也是互斥的,A线程放鸡蛋,如果这时B线程要取鸡蛋,由于A没有释放锁,B线程处于等待状态,进入阻塞队列,放鸡蛋之后,要通知B线程取鸡蛋,B线程进入就绪队列,反过来,B线程取鸡蛋,如果A线程要放鸡蛋,由于B线程没有释放锁,A线程处于等待状态,进入阻塞队列,取鸡蛋之后,要通知A线程放鸡蛋,A线程进入就绪队列。我们希望当盘子里有鸡蛋时,A线程阻塞,B线程就绪,盘子里没鸡蛋时,A线程就绪,B线程阻塞,代码如下:

import java.util.ArrayList;
import java.util.List;
/** 定义一个盘子类,可以放鸡蛋和取鸡蛋 */
public class Plate {
	/** 装鸡蛋的盘子 */
	List<Object> eggs = new ArrayList<Object>();
	/** 取鸡蛋 */
	public synchronized Object getEgg() {
		while (eggs.size() == 0) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		Object egg = eggs.get(0);
		eggs.clear();// 清空盘子
		notify();// 唤醒阻塞队列的某线程到就绪队列
		System.out.println("拿到鸡蛋");
		return egg;
	}
	/** 放鸡蛋 */
	public synchronized void putEgg(Object egg) {
		while (eggs.size() > 0) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		eggs.add(egg);// 往盘子里放鸡蛋
		notify();// 唤醒阻塞队列的某线程到就绪队列
		System.out.println("放入鸡蛋");
	}
	static class AddThread implements Runnable  {
		private Plate plate;
		private Object egg = new Object();
		public AddThread(Plate plate) {
			this.plate = plate;
		}
		public void run() {
			plate.putEgg(egg);
		}
	}
	static class GetThread implements Runnable  {
		private Plate plate;
		public GetThread(Plate plate) {
			this.plate = plate;
		}
		public void run() {
			plate.getEgg();
		}
	}
	public static void main(String args[]) {
		Plate plate = new Plate();
		for(int i = 0; i < 10; i++) {
			new Thread(new AddThread(plate)).start();
			new Thread(new GetThread(plate)).start();
		}
	}
}
输出结果:
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋

在调用wait方法时,都是用while判断条件的,而不是if,在wait方法说明中,也推荐使用while,因为在某些特定的情况下,线程有可能被假唤醒,使用while会循环检测更稳妥。

(2)使用 Condition 实现方式

3.线程间的通信:管道

PipedInputStream类 与 PipedOutputStream类 用于在应用程序中创建管道通信。一个PipedInputStream实例对象必须和一个PipedOutputStream实例对象进行连接而产生一个通信管道。PipedOutputStream可以向管道中写入数据,PipedIntputStream可以读取PipedOutputStream向管道中写入的数据,这两个类主要用来完成线程之间的通信。一个线程的PipedInputStream对象能够从另外一个线程的PipedOutputStream对象中读取数据,如下图所示:

            线程通信示意图之管道.jpg-30.5kB

  PipedInputStream和PipedOutputStream的实现原理类似于”生产者-消费者”原理,PipedOutputStream是生产者,PipedInputStream是消费者。在PipedInputStream中,有一个buffer字节数组,默认大小为1024,作为缓冲区,存放”生产者”生产出来的东西。此外,还有两个变量in和out —— in用来记录”生产者”生产了多少,out是用来记录”消费者”消费了多少,in为-1表示消费完了,in==out表示生产满了。当消费者没东西可消费的时候,也就是当in为-1的时候,消费者会一直等待,直到有东西可消费。 
   
  在 Java 的 JDK 中,提供了四个类用于线程间通信:

  • 字节流:PipedInputStream 和 PipedOutputStream;
  • 字符流:PipedReader 和 PipedWriter;
class ThreadRead extends Thread {
 
    private ReadData read;
    private PipedInputStream input;
 
    public ThreadRead(ReadData read, PipedInputStream input) {
        super();
        this.read = read;
        this.input = input;
    }
 
    public void readMethod(PipedInputStream input) {
        try {
            System.out.println("read  :");
            byte[] byteArray = new byte[20];
            int readLength = input.read(byteArray);
            while (readLength != -1) {
                String newData = new String(byteArray, 0, readLength);
                System.out.print(newData);
                readLength = input.read(byteArray);
            }
            System.out.println();
            input.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public void run() {
        this.readMethod(input);
    }
}
 
//写线程
class ThreadWrite extends Thread {
 
    private WriteData write;
    private PipedOutputStream out;
 
    public ThreadWrite(WriteData write, PipedOutputStream out) {
        super();
        this.write = write;
        this.out = out;
    }
 
    public void writeMethod(PipedOutputStream out) {
        try {
            System.out.println("write :");
            for (int i = 0; i < 30; i++) {
                String outData = "" + (i + 1);
                out.write(outData.getBytes());
                System.out.print(outData);
            }
            System.out.println();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public void run() {
        this.writeMethod(out);
    }
}
 
//测试
public class Run {
 
    public static void main(String[] args) {
        try {
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();
 
            PipedInputStream inputStream = new PipedInputStream();
            PipedOutputStream outputStream = new PipedOutputStream();
 
            // inputStream.connect(outputStream);   // 效果相同
            outputStream.connect(inputStream);
 
            ThreadRead threadRead = new ThreadRead(readData, inputStream);
            threadRead.start();
 
            Thread.sleep(2000);
 
            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
            threadWrite.start();
 
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 

4. join()方法的使用

假如在main线程中调用thread.join方法,则main线程会等待thread线程执行完毕或者等待一定的时间。详细地,如果调用的是无参join方法,则等待thread执行完毕;如果调用的是指定了时间参数的join方法,则等待一定的时间。join()方法有三个重载版本:

public final synchronized void join(long millis) throws InterruptedException {...}
public final synchronized void join(long millis, int nanos) throws InterruptedException {...}
public final void join() throws InterruptedException {...}

以 join(long millis) 方法为例,其内部调用了Object的wait()方法,如下图: 
            join 的定义.png-39.6kB

  根据以上源代码可以看出,join()方法是通过wait()方法 (Object 提供的方法) 实现的。当 millis == 0 时,会进入 while(isAlive()) 循环,并且只要子线程是活的,宿主线程就不停的等待。 wait(0) 的作用是让当前线程(宿主线程)等待,而这里的当前线程是指 Thread.currentThread() 所返回的线程。所以,虽然是子线程对象(锁)调用wait()方法,但是阻塞的是宿主线程。

事实上,有wait就必然有notify。在整个jdk里面,我们都不会找到对thread1线程的notify操作。这就要看jvm代码了:

作者:cao
链接:https://www.zhihu.com/question/44621343/answer/97640972
来源:知乎
 
//一个c++函数:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;
 
//这个函数的作用就是在一个线程执行完毕之后,jvm会做的收尾工作。里面有一行代码:ensure_join(this);
 
该函数源码如下:
 
static void ensure_join(JavaThread* thread) {
    Handle threadObj(thread, thread->threadObj());
 
    ObjectLocker lock(threadObj, thread);
 
    thread->clear_pending_exception();
 
    java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
 
    java_lang_Thread::set_thread(threadObj(), NULL);
 
    //thread就是当前线程,就是刚才说的thread1线程。
    lock.notify_all(thread);
 
    thread->clear_pending_exception();
}

由于 join方法 会调用 wait方法 让宿主线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。结合 join 方法的声明,可以总结出以下三条:

  • join方法同样会会让线程交出CPU执行权限;

  • join方法同样会让线程释放对一个对象持有的锁;

  • 如果调用了join方法,必须捕获InterruptedException异常或者将该异常向上层抛出。

5.ThreadLocal线程变量详解

ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题,比如Hibernate中的OpenSessionInView,就是使用ThreadLocal保存Session对象,还有我们经常用ThreadLocal存放Connection,代码如:

/**
 * 数据库连接管理类
 * @author 爽
 *
 */
public class ConnectionManager {
 
	/** 线程内共享Connection,ThreadLocal通常是全局的,支持泛型 */
	private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
	
	public static Connection getCurrConnection() {
		// 获取当前线程内共享的Connection
		Connection conn = threadLocal.get();
		try {
			// 判断连接是否可用
			if(conn == null || conn.isClosed()) {
				// 创建新的Connection赋值给conn(略)
				// 保存Connection
				threadLocal.set(conn);
			}
		} catch (SQLException e) {
			// 异常处理
		}
		return conn;
	}
	
	/**
	 * 关闭当前数据库连接
	 */
	public static void close() {
		// 获取当前线程内共享的Connection
		Connection conn = threadLocal.get();
		try {
			// 判断是否已经关闭
			if(conn != null && !conn.isClosed()) {
				// 关闭资源
				conn.close();
				// 移除Connection
				threadLocal.remove();
				conn = null;
			}
		} catch (SQLException e) {
			// 异常处理
		}
	}
}

 这样处理的好处:

  1. 统一管理Connection;
  2. 不需要显示传参Connection,代码更优雅;
  3. 降低耦合性。

四种方法:

protected T initialValue() 返回此线程局部变量的当前线程的初始值。

public T get() 返回此线程局部变量的当前线程的值

public void set(T value)  value- 存储在此线程局部变量的当前线程副本中的值。

public void remove() 移除此线程局部变量的值。

 

原文:https://blog.csdn.net/ghsau/article/details/7433673    

 https://blog.csdn.net/ghsau/article/details/7424694 

https://blog.csdn.net/huangshulang1234/article/details/79784323

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值