主要介绍JAVA多线程的一些基本概念,包括如何实现多线程,多线程之间如何共享数据,还有典型的生产者消费者模式。
1 JAVA多线程
Java在语言级提供了对多线程程序设计的支持。实现多线程程序的两种方式:
(1)实现Runnable接口
【例1】
//通过实现Runnable接口实现线程的共享变量
public class MyThread implements Runnable
{
int index=0; //多个线程共享变量
//线程的入口函数
public void run()
{ synchronized (this){ //同步
System.out.println(Thread.currentThread().getName());
System.out.println(index++);
} //end synchronized
}
}
//----------------------------------------------------------------------
public class MultiThread
{
public static void main(String[] args)
{
MyThread mt=new MyThread();
//开始执行线程:mt作为参数传入是相同的对象mt,所以访问的MyThread中的变量也是同一个变量(多线程访问了同一种资源)
Thread thread1=new Thread(mt);//第一个线程
Thread thread2=new Thread(mt);//第二个线程
thread1.start();
thread2.start();
}
}
(2)从Thread类继承,必须重写run()方法(一个Thread对象代表了进程中的一个线程)
//定义线程
public class MyThread extends Thread
{
//线程的入口函数
public void run()
{
System.out.println(getName());
yield(); //暂停该线程,允许其他线程运行
}
}
//-----------------------------------------------------------------
public class MultiThread
{
public static void main(String[] args)
{
MyThread thread1=new MyThread(); //第一个线程
thread1.start();
MyThread thread2=new MyThread();//第二个线程
thread2.start();
System.out.println("main:"+Thread.currentThread().getName());
}
}
(3)两种实现方式比较
使用“实现Runnable接口”实现线程,通常不需修改线程类(Thread)中的方法(除了run方法)时,使用Runnable接口实现线程。有以下两个好处:
当类已经继承了一个类,java不允许多继承,此时只能用Runnable接口;
当多个线程需要访问同一种资源时,如:如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据(像上述例子中的index变量,为2个线程共享)。
2 同步
在介绍什么是同步之前,我们首先要明白为什么要同步?2.1 为什么要同步
当多个线程对同一个数据进行操作时,可能会导致每个线程拿到数据的状态不一致,因为此时每个线程对这个共享数据的操作对其他线程是不可见的。数据同步就是指在同一时间,只能由一个线程来访问被同步的类变量,当前线程访问完这些变量后,其他线程才能继续访问。这里说的访问是指有写操作的访问,如果所有访问类变量的线程都是读操作,一般是不需要数据同步的。如下图[1],当B执行到与A相同的锁监视的同步块时,A在同步快之中或之前所做的每件事,对B都是可见的。如果没有同步,就没有这样的保证。
何时需要同步?当访问共享的、可变的数据要求时要使用同步。[1]
2.2 JAVA中如何实现同步
The code segments within a program that access the same object from separate, concurrent threads are called “critical sections”(临界区)。
每一个对象都有一个监视器,或者叫做锁。
同步方法利用的是this所代表的对象的锁。
每个class也有一个锁,是这个class所对应的Class类对象的锁。
JAVA同步的两种方式:同步块和同步方法。
【例2】
/*
火车票售票系统(线程同步的2种方法):四个线程,同时卖这100张票
*/
public class TicketsSystem
{
public static void main(String[] args)
{
//创建四个线程,同时卖这100张票(共享同一个资源tickets)
SellThread st=new SellThread();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
}
}
public class SellThread implements Runnable
{
int tickets=100; //有100张票,多线程共享变量,注意同步
Object obj=new Object();
public void run()
{
//当前时哪个线程卖了第几张票
while(true)
{
/*【同步块】:synchronized(对象){临界区代码} //每一个对象都有一个监视器,或者叫做锁,用该对象来标识锁(加锁,解锁)
线程A进入同步块前检查对象obj是否加锁:
若加锁,等待;
若已解锁,进入同步块,并将对象obj加锁,直到出来同步块时,将对象解锁
*/
/* synchronized(obj)
{
if(tickets>0)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"sell tickets:"+tickets);
tickets--;
}
}*/
sell();
}//end while
}//end run()
//【同步方法】:在方法前加关键字synchronized(给方法中的this对象加锁和解锁)
public synchronized void sell()
{
if(tickets>0)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "sell tickets:"+tickets); //模拟买票
tickets--;
}
} //end sell()
} //class SellThread
3 生产者消费者模型
实际上,准确说应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力了,这里的仓库是多线程之间共享的数据。对于此模型,应该明确一下几点:
//----------------------------------------------------------------------
/**
* 存放共享数据的容器,此处为仓库
* 设计为线程安全的类,供消费线程安全使用
*/
public class ShareData {
private static final int max_size = 100; //最大库存量
private int curnum; //当前库存量
ShareData() {
}
ShareData(int curnum) {
this.curnum = curnum;
}
/**
* 生产指定数量的产品
* @param neednum
*/
public synchronized void produce(int neednum) {
//是否需要生产
while (neednum + curnum > max_size) {
System.out.println("要生产的产品数量" + neednum + "超过剩余库存量" + (max_size - curnum) + ",暂时不能执行生产任务!");
try {
wait(); //当前的生产线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//满足生产条件,则进行生产,这里简单的更改当前库存量
curnum += neednum;
System.out.println("已经生产了" + neednum + "个产品,现仓储量为" + curnum);
notifyAll(); //唤醒在此对象监视器上等待的所有线程
}
/**
* 消费指定数量的产品
* @param neednum
*/
public synchronized void consume(int neednum) {
//是否可消费
while (curnum < neednum) {
try {
wait(); //当前的生产线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//满足消费条件,则进行消费,这里简单的更改当前库存量
curnum -= neednum;
System.out.println("已经消费了" + neednum + "个产品,现仓储量为" + curnum);
notifyAll(); //唤醒在此对象监视器上等待的所有线程
}
}
//----------------------------------------------------------------------
生产者线程
//----------------------------------------------------------------------
/**
* 生产者
*/
public class Producer implements Runnable {
private int neednum; //生产产品的数量
private ShareData shareData; //仓库
Producer(int neednum, ShareData shareData) {
this.neednum = neednum;
this.shareData = shareData;
}
public void run() {
//生产指定数量的产品
shareData.produce(neednum);
}
} //end class Producer
//----------------------------------------------------------------------
消费者线程
/**
* 消费者
*/
public class Consumer implements Runnable {
private int neednum; //生产产品的数量
private ShareData shareData; //仓库
Consumer(int neednum, ShareData shareData) {
this.neednum = neednum;
this.shareData = shareData;
}
public void run() {
//消费指定数量的产品
shareData.consume(neednum);
}
}//end class Consumer
整个工作的任务单
//----------------------------------------------------------------------
/**
* Java线程:并发协作-生产者消费者模型
*/
public class Job {
public static void main(String[] args) {
ShareData shareData = new ShareData(30); //共享数据
Thread c1 = new new Thread(new Consumer(50, shareData));
Thread c2 = new new Thread(new Consumer(20, shareData));
Thread p1 = new new Thread(new Producer(10, shareData));
Thread p2 = new new Thread(new Producer(10, shareData));
Thread p3 = new new Thread(new Producer(10, shareData));
Thread p4 = new new Thread(new Producer(10, shareData));
Thread p5 = new new Thread(new Producer(10, shareData));
Thread p6 = new new Thread(new Producer(10, shareData));
Thread p7 = new new Thread(new Producer(80, shareData));
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
p6.start();
p7.start();
}
} //end class Job
//----------------------------------------------------------------------
1)每一个对象除了有一个锁之外,还有一个等待队列(wait set),当一个对象刚创建的时候,它的等待队列是空的。
2)我们应该在当前线程锁住对象的锁后,去调用该对象的wait方法
(有两个含义:wait方法只能在同步块或同步方法中被调用;当调用wait方法时,该线程进入了该对象的等待队列中)。
3)当调用对象的notify方法时,将从该对象的等待队列中删除一个任意选择的线程,这个线程将再次成为就绪状态的线程。
4)当调用对象的notifyAll方法时,将从该对象的等待队列中删除所有等待的线程,这些线程将成为就绪状态的线程。
notifyAll() 方法,起到的是一个通知作用,不释放锁,也不获取锁。只是告诉该对象上等待的线程“可以竞争执行了,都醒来去执行吧”。
5)wait和notify主要用于producer-consumer(生产者-消费者)这种关系中,并且只能在同步方法或同步块中调用,并且是针对同一个对象的等待和通知。
4 线程通信
JAVA中实现多线程间通信则主要采用"共享变量"和"管道流"这两种方法。
4.1 线程通信-共享变量
1、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。如【例2】
2、如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
(1)将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
public class WorkThread {
int shareData=1; //共享数据
public void newJob(){
WorkThreadA workThreadA=new WorkThreadA();
Thread threadA=new Thread(workThreadA); //线程A
WorkThreadB workThreadB=new WorkThreadB();
Thread threadB=new Thread(workThreadB); //线程B
threadA.start();
threadB.start();
}//end newJob()
//线程A
class WorkThreadA implements Runnable{
public void run(){
...
}
}//end class WorkThreadA
//线程B
class WorkThreadB implements Runnable{
public void run(){
...
}
}//end class WorkThreadB
}//end class WorkThread
(2)将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。
//------------------------------------------------------------
public class WorkThread {
public void newJob(){
ShareData shareData=new ShareData(); //多个线程使用同一个对象,所以对象中的成员数据也是共享数据
WorkThreadA workThreadA=new WorkThreadA(shareData);
Thread threadA=new Thread(workThreadA); //线程A
WorkThreadB workThreadB=new WorkThreadB(shareData);
Thread threadB=new Thread(workThreadB); //线程B
threadA.start();
threadB.start();
}//end newJob()
}//end class WorkThread
//------------------------------------------------------------
//共享数据封装:将共享数据封装在一个线程安全的类中
//外部消费线程使用该类说明:本类已经封装为一个线程安全的类,通过使用方法getShareData(),setShareData对共享数据进行操作,
//外部消费线程无需在同步
public class ShareData{
private int shareData;
//外部消费线程,通过以下同步方法对sharData进行操作
public synchronized int getShareData(){
return shareData;
}//end getShareData
public synchronized int setShareData(int shareData){
return this.shareData=shareData;
}//end setShareData
}//end class ShareData
//------------------------------------------------------------
//线程A
public class WorkThreadA implements Runnable{
private ShareData shareData;
public WorkThreadA(){}
public WorkThreadA(ShareData shareData){
this.shareData=shareData;
}
public void run(){
...
WorkThread.getShareData(); //因为共享数据已经封装为一个线程安全的类,消费线程无需在同步了
...
WorkThread.setShareData();
...
}
}//end class WorkThreadA
//------------------------------------------------------------
//线程B
public class WorkThreadB implements Runnable{
private ShareData shareData;
public WorkThreadB(){}
public WorkThreadB(ShareData shareData){
this.shareData=shareData;
}
public void run(){
...
WorkThread.getShareData();
...
WorkThread.setShareData();
...
}
}//end class WorkThreadB
(3)总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
(4)极端且简单的方式,即在任意一个类中定义一个static的变量,这将被所有线程共享。
4.2 线程通信-管道流
public class CommunicateWhitPiping {
public static void main(String[] args) {
/**
* 创建管道输出流
*/
PipedOutputStream pos = new PipedOutputStream();
/**
* 创建管道输入流
*/
PipedInputStream pis = new PipedInputStream();
try {
/**
* 将管道输入流与输出流连接 此过程也可通过重载的构造函数来实现
*/
pos.connect(pis);
} catch (IOException e) {
e.printStackTrace();
}
/**
* 创建生产者线程
*/
Producer p = new Producer(pos);
/**
* 创建消费者线程
*/
Consumer c = new Consumer(pis);
/**
* 启动线程
*/
p.start();
c.start();
}
}
/**
* 生产者线程(与一个管道输入流相关联)
*
*/
class Producer extends Thread {
private PipedOutputStream pos;
public Producer(PipedOutputStream pos) {
this.pos = pos;
}
public void run() {
int i = 8;
try {
pos.write(i);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 消费者线程(与一个管道输入流相关联)
*
*/
class Consumer extends Thread {
private PipedInputStream pis;
public Consumer(PipedInputStream pis) {
this.pis = pis;
}
public void run() {
try {
System.out.println(pis.read());
} catch (IOException e) {
e.printStackTrace();
}
}
}
参考文献
[1]《Java Concurrency in Practice》