线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,是线程间进行通信之后,系统之间的交互性就会跟强大,在大大提高cpu利用率的同时还会是程序员对各线程任务在处理的过程中进行的把控与监督。在本次主要讲解以下的技术点:
- 使用wait/notify实现线程间的通信
- 生产者/消费者模式的实现
- 方法join的使用
- ThreadLocal类的使用
利用wait/notify实现线程间的通信
方法wait()的作用就是是当前执行的代码线程进行等待,wait()方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码出停止执行,知道接到通知或被中断为止。在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步代码块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时没有持有适当的锁,则抛出异常,但不需要显式的捕获。
方法notify()也要在同步方法或者同步方法块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出异常。该方法用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知,并使他等待获取该对象的对象锁。需要说明的是,在执行notify()方法方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是说退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在线程才可以获取该对象锁、当第一个获得了该对象锁的wait线程完毕之后,他会释放调该对象锁,此时如果该对象没有再次使用notify语句,则即使该对象已经空闲,其他wait装袋等待的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify或者ntotifyall。
用一句话来总结一下wait和notify:wait使线程停止运行,而notiy是停止的线程继续运行。
下面通过一个例子来了解wait和notfiy具体的使用。
创建myList具体实现类,具体代码如下:
import java.util.ArrayList;
import java.util.List;
public class myList {
private List<String> list = new ArrayList<String>();
public myList(List<String> list){
this.list = list;
}
public void add(){
list.add("hello");
}
public int size(){
return list.size();
}
}
创建线程类
//线程类A,只要实现往list中添加数据
public class ThreadA extends Thread{
myList my;
public ThreadA(myList my){
this.my = my;
}
public void run(){
synchronized (my) {
for(int i=0;i<10;i++){
my.add();
System.out.println("add successful ! and size = "+my.size());
if(my.size()==5){
my.notify();
System.out.println("notify send~!");
}
}
}
}
}
//线程类B,等待notify才能继续运行。
public class ThreadB extends Thread{
myList my ;
public ThreadB(myList my){
this.my = my;
}
public void run(){
System.out.println("threadb start!");
synchronized (my) {
try {
System.out.println("threadb start! and wait lock");
my.wait();
System.out.println("threadb start! and had the lock");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试主体类:
import java.util.ArrayList;
import java.util.List;
public class Run {
public static void main(String[] args) throws InterruptedException {
List list = new ArrayList<String>();
myList my = new myList(list);
ThreadA a = new ThreadA(my);
ThreadB b = new ThreadB(my);
b.start();
Thread.sleep(100);//确保B线程先运行。
a.start();
}
}
运行结果如图:
可以看到,在size=5时,确实发出了一个通知,但是等到ThreadA中run中的代码执行完毕之后,ThreadB才获取了锁,并且开始运行。这也证实了前面所说的nofify并不能马上就唤醒wait,而是要等到对象锁中的代码执行完毕之后才能唤醒wait中大代码。
也许你会很好奇,如果在wait之前就已经发出了notify,那么结果会怎么样?
修改运行类中代码,让ThreadA先执行,及可以观察到这样的结果。
import java.util.ArrayList;
import java.util.List;
public class Run {
public static void main(String[] args) throws InterruptedException {
List list = new ArrayList<String>();
myList my = new myList(list);
ThreadA a = new ThreadA(my);
ThreadB b = new ThreadB(my);
a.start();
Thread.sleep(100);
b.start();
}
}
运行结果如图:
可以看到,ThreadB还一直在wait,但是因为在ThreadA之前就已经notify过了,所以ThreadB将会一直处于阻塞状态。那么如何修改程序,达到只要nofify过,wait就不会一直阻塞呢?有兴趣的可以自己去尝试尝试。
另外,这里有几点要进行说明:
- 当方法wait()被执行完后,锁自动被释放,而执行notify()之后,锁却不会自动释放。
- 当存在多个wait(),而只执行一次notify()时,会随机唤醒一个wait,如果想一次性将所有持有相同锁的wait唤醒的话,可以使用notifyAll()。
- wait()方法还可以带一个参数,形如:wait(long),功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。
- 在使用wait/notify模式时,还需要注意另一种情况,就是wait等待的条件发生了变化时,容易造成程序逻辑的混乱。
通过管道进行线程间的通信
在java中提供了各种各样的输入/输出流,使我们能够很方便的对数据进行操作,其中的管道流(PipeStream)是一种很特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。通过使用管道,实现不同线程间的通信,而无须借助类似于临时文件之类的东西。
这里我们主要介绍下面4个类:
1 PipedInputStream 和 PipedOutputStream
2 PipedReader 和 PipedWriter
下面我们直接通过例子来看这4个类该如何使用:
通过字节流实现线程间的数据传递:
WriterData写如数据的文件代码如下:
import java.io.IOException;
import java.io.PipedOutputStream;
public class WriterData {
public void writer(PipedOutputStream out){
try{
System.out.println("writer:");
for(int i=0;i<310;i++){
String outData = ""+(i+1);
out.write(outData.getBytes());
System.out.printf(" "+outData);
}
System.out.println();
out.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
读数据ReadData的文件代码如下:
import java.io.IOException;
import java.io.PipedInputStream;
public class ReadData {
public void readData(PipedInputStream in) {
try {
System.out.println("read :");
byte[] byteArray = new byte[32];
int readLength = in.read(byteArray);
while(readLength != -1){
String newData = new String(byteArray,0,readLength);
System.out.printf(""+newData);
readLength = in.read(byteArray);
}
System.out.println();
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
线程类:
import java.io.PipedOutputStream;
public class ThreadA implements Runnable{
private WriterData writerData;
private PipedOutputStream out;
public ThreadA(WriterData writerData,PipedOutputStream out){
this.writerData = writerData;
this.out = out;
}
@Override
public void run() {
writerData.writer(out);
}
}
//线程B
import java.io.PipedInputStream;
public class ThreadB implements Runnable{
private ReadData readData;
private PipedInputStream in;
public ThreadB(ReadData readData,PipedInputStream in){
this.readData = readData;
this.in = in;
}
@Override
public void run() {
readData.readData(in);
}
}
测试主体类:
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class runTest {
public static void main(String[] args) throws Exception{
WriterData writerData = new WriterData();
ReadData readData = new ReadData();
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream();
out.connect(in);//使两个Stream之间产生通信连接
//in.connect(out);
Runnable a = new ThreadA(writerData,out);
Runnable b = new ThreadB(readData,in);
Thread thA = new Thread(a);
Thread thB = new Thread(b);
thA.start();
Thread.sleep(1000);
thB.start();
}
}
运行结果如图:
[图片上传中。。。(3)]
从程序的结果来看,两个线程通过管道成功进行了数据传递。
字符流实现:
因为代码同字节流的大部分相同,这里一并粘贴显示出来
//写数据
import java.io.IOException;
import java.io.PipedWriter;
public class WriterData {
public void writerDate(PipedWriter out ) throws IOException{
System.out.println("writer:");
for(int i=0;i<300;i++){
String outData = ""+(i+1);
out.write(outData);
System.out.print(outData);
}
System.out.println();
out.close();
}
}
//读数据
import java.io.IOException;
import java.io.PipedReader;
public class ReadData {
public void readData(PipedReader in) throws IOException{
System.out.println("read:");
char[] charArray = new char[32];
int readLength =in.read(charArray);
while(readLength != -1){
String newData = new String(charArray,0,readLength);
System.out.print(newData);
readLength = in.read(charArray);
}
System.out.println();
in.close();
}
}
//线程A的代码
import java.io.IOException;
import java.io.PipedWriter;
public class ThreadA extends Thread {
private PipedWriter writer;
private WriterData writerData;
public ThreadA(WriterData writerData,PipedWriter writer){
this.writer = writer;
this.writerData = writerData;
}
public void run(){
try {
writerData.writerDate(writer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//线程B的代码
import java.io.IOException;
import java.io.PipedReader;
public class ThreadB extends Thread{
private ReadData readData ;
private PipedReader reader;
public ThreadB(ReadData readData,PipedReader reader){
this.readData = readData;
this.reader = reader;
}
public void run() {
try {
readData.readData(reader);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//测试类主体
import java.io.PipedReader;
import java.io.PipedWriter;
public class runTest {
public static void main(String[] args) throws Exception{
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
WriterData writerData = new WriterData();
ReadData readData = new ReadData();
writer.connect(reader);
ThreadA a = new ThreadA(writerData,writer);
ThreadB b = new ThreadB(readData,reader);
a.start();
Thread.sleep(1000);
((Thread) b).start();
}
}
运行结果同字节流的输出结果是一样的。这里就不在粘贴结果了。更多关于这四个关键字的用法,参考javaI/O中的内容。
join()方法
方法join的作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程y进行无限期的阻塞。等待线程x销毁后在继续执行线程z后面的代码。
方法join具有使线程排队运行的作用,有些类似同步的运行效果。join与synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理作为同步。
在join过程中,如果当前线程对象被中断,则当前线程出现异常。下面以一个例子来说明join方法的使用:
线程类:
public class MyThread extends Thread{
public void run(){
int randValue = (int)(Math.random()*10000);
System.out.println("我要运行"+randValue+"毫秒,这个时间每次都是是不确定的");
try {
Thread.sleep(randValue);
System.out.println("终于运行完毕了,我要退出了");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
测试类。
public class runTest {
public static void main(String[] args) throws InterruptedException{
MyThread myThread = new MyThread();
myThread.start();
//myThread.join();
System.out.println("这句话,我想等线程执行完我在说出来");
}
}
当没有加入join方法时的运行效果:
当加入join方法时的运行效果:
从例子中,应该能明显感受到join的威力了吧,但join还可以当成join(long)来进行使用。如果上例中的运行类修改呈下面的代码:
public class runTest {
public static void main(String[] args) throws InterruptedException{
MyThread myThread = new MyThread();
myThread.start();
myThread.join(2000);
System.out.println("这句话,我想等线程执行完我在说出来");
}
}
可能出现的结果如图:
补充说明:
程序一开始就输出第一句:“我要运行5687毫秒,这个时间每次都是是不确定的”
间隔2000毫秒后输出:“这句话,我想等线程执行完我在说出来”
最后在距离程序开始5687毫秒的时候输出最后一句话。
join(long)方法的作用就不言而喻了,其实质就是给某个线程执行多长时间。与sleep(long)有一定的相同之处。
但是join(long)与sleep(long)的最大区别就是join(long)方法具有释放锁的特点(其内部是使用wait(long)来实现的),而sleep(long)不具备释放锁的特点。
ThreadLocal类的使用
变量值的共享可以使用public static变量的形式,所有的线程都使用同一个public static变量。如果想实现每一个线程都有自己的共享变量则可以通过ThreadLocal类来实现。
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。(源码的解读 可以参android开发艺术探索的第*章)
例如,以下类生成对每个线程唯一的局部标识符。线程 ID 是在第一次调用UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,在后续调用中不会更改。
import java.util.concurrent.atomic.AtomicInteger;
public class UniqueThreadIdGenerator {
private static final AtomicInteger uniqueId = new AtomicInteger(0);
private static final ThreadLocal < Integer > uniqueNum =
new ThreadLocal < Integer > () {
@Override protected Integer initialValue() {
return uniqueId.getAndIncrement();
}
};
public static int getCurrentThreadId() {
return uniqueId.get();
}
} // UniqueThreadIdGenerator
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
该类有四个方法:
- protected T initialValue();
返回此线程局部变量的当前线程的“初始值”。线程第一次使用 get() 方法访问变量时将调用此方法,但如果线程之前调用了 set(T) 方法,则不会对该线程再调用 initialValue 方法。通常,此方法对每个线程最多调用一次,但如果在调用 get() 后又调用了 remove(),则可能再次调用此方法。
该实现返回 null;如果程序员希望线程局部变量具有 null 以外的值,则必须为 ThreadLocal 创建子类,并重写此方法。通常将使用匿名内部类完成此操作。 - public T get()
返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用 initialValue() 方法返回的值。 - public void set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。 - public void remove()
移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取,且这期间当前线程没有设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue 方法。
如果在子线程中取得父线程继承下来的值,则需要用到InheritableThreadLocal。
修改默认值可以实例代码如下:
public class Locat extends InheritableThreadLocal<String>{
@Override
protected String initialValue() {
//return super.initialValue();
return "默认的值在这里进行修改";
}
}
//运行类
public class runTest {
static Locat t1 = new Locat();
public static void main(String [] args){
System.out.println(t1.get());
}
}
输出结果就是:
如果在继承的同时还需要对值进行进一步的处理,可以这么干:
通过继承InheritableThreadLocal修改默认值和继承值
public class Locat extends InheritableThreadLocal<String>{
@Override
protected String initialValue() {
//return super.initialValue();
return "默认的值在这里进行修改";
}
@Override
protected String childValue(String parentValue) {
//return super.childValue(parentValue);
return parentValue+"这里修改子线程的值";
}
}
把数据封装到tools中
public class Tools {
public static Locat t1 = new Locat();
}
线程a的代码
public class ThreadA extends Thread{
public void run(){
System.out.println(Tools.t1.get());
}
}
运行类的代码如下:
public class runTest {
public static void main(String [] args){
System.out.println(Tools.t1.get());
ThreadA a = new ThreadA();
a.start();
}
}
运行结果如下:
可见。我们成功修改了初始值和子线程的初始值。
值得注意的是,如果子线程取得值的同时,主线程将InheritableLocal中的值进行修改,那么子线程取得到的值还是旧值。
JA