线程是一个单独的执行体。初学者往往分不清线程和任务,通常所说的创建一个线程,比如写一个实现Runnable
接口的类或者继承Thread
重写run()
方法的类,通常是指创建一个被单独线程执行的任务,真正线程的创建时在调用Thread.start()
方法开始的。
多线程的创建与任务的执行
记住,线程是一个具有执行能力的资源,我们的任务要使用该资源完成自身的执行过程。我们要做的就是创建一些任务(根据具体业务需要),再创建一些线程来执行这些任务。
当然,可以先创建任务,再使用任务创建线程。比如实现Runnable
接口的类或者继承Thread
重写run()
方法,然后通过start()
方法创建线程并执行任务。这样的线程执行完run()
方法中的任务后就退出,是一次性的。
public class Demo {
public static void main(String[] args) {
//create three Thread
Thread t1=new Thread(new demo1());//使用Runnable创建
Thread t2=new demo2();//使用Thread子类创建
Thread t3=new Thread(new Runnable(){//使用匿名内部类创建
public void run(){printFun();}
});
//start the thread
t1.start();
t2.start();
t3.start();
}
public static void printFun(){
System.out.println(Thread.currentThread().getName()
+" run print function");
}
}
class demo1 implements Runnable{
@Override
public void run() {
Demo.printFun();
}
}
class demo2 extends Thread{
@Override
public void run() {
Demo.printFun();
}
}
也可以把线程和任务分开创建。使用线程池,创建独立于任务的一些类线程,然后把任务提交给线程池执行。线程池中的线程是可以反复利用的,执行完提交的前一个任务再去取出任务队列中的后续任务进行执行。
public class DemoThreadPool {
public static void main(String[] args) {
int threadCount=3;//线程池中线程数量
//创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(threadCount);
//循环创建10个任务并提交线程池执行
for(int i=0; i<10; ++i){
//任务编号
final int currentTaskNum=i;
//创建任务
Runnable task=new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" execute task "+currentTaskNum);
}
};
//提交执行
threadPool.submit(task);
}
threadPool.shutdown();
}
}
使用线程池情况下,除了Runnable
外还有另一个接口供我们创建任务,即Callable
接口。任务示例如下:
public class DemoThreadPool {
public static void main(String[] args) throws Exception {
int threadCount=3;
//线程池
ExecutorService threadPool = Executors.newFixedThreadPool(threadCount);
//存放任务执行后的返回结果
List<Future<String>> results=new ArrayList<>();
for(int i=0; i<10; ++i){
final int currentTaskNum=i;
//创建任务
Callable<String> task=new Callable<String>(){
@Override
public String call() throws Exception {
// 任务代码
System.out.println("task "+currentTaskNum);
return "task "+currentTaskNum+" done";
}
};
//提交任务
Future<String> result = threadPool.submit(task);
results.add(result);
}
//取出任务执行结果并打印
for(Future<String> ret : results){
System.out.println(ret.get());
}
//关闭线程池
threadPool.shutdown();
}
}
与Runnable不同的是Callable接口运行任务执行过程中抛出受检异常,也允许有返回值。返回值使用Future接口,代表未来的的某个时候(具体指该任务执行完成以后)可以取出任务执行的结果(返回值)。
多线程同步与通信
大多数情况下,任务之间都是相互关联的。多个任务再多线程中执行,线程之间需要一定的通信机制来保证任务的协同性。
另外,有一些数据是可供多个线程同时访问的,这种访问包括读取和改写,如果一个线程在写数据的同时有另外的线程在读数据,则会破坏线程间的数据一致性,可能读取到难以理解的“脏数据”!因此多线程之间的共享变量需要同步来保证多线程对数据访问的顺序性。
最常见的多线程通信的例子就是 生产者—消费者 模式,生产者可以是一个或多个,消费者也可以是一个或多个。先来看单生产者—消费者模式的例子:
public class ProducerConsumer {
public static void main(String[] args) {
final ProducerConsumerDemo demo = new ProducerConsumerDemo();
//简洁起见这里使用Lambda表达式吧,Java8都出来这么久了,这个你应该掌握
Runnable producer = ()->demo.produce();
Runnable consumer = ()->demo.comsume();
//启动线程
new Thread(producer).start();
new Thread(consumer).start();
}
}
class ProducerConsumerDemo {
private static final int loopTime = 10;//循环次数
private volatile boolean hasData = false; //标志位
private Data data; //生产者消费者交换数据
public void produce() {
for (int i = 0; i < loopTime; ++i) {
synchronized (this) {
while (hasData) {
try {
wait(); //如果已经有数据,则生产者线程等待消费者线程把数据消费掉之后再生产
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
data = new Data();//生产
data.value = i;
hasData = true; //改变标志位
System.out.println("producer produce a data: " + data.value);
notify(); //唤醒消费者线程来取数据
}
}
}
public void comsume() {
for (int i = 0; i < loopTime; ++i) {
synchronized (this) {
while (!hasData) {
try {
wait();//如果没有数据则等待生产者线程生产数据
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
int value = data.value;
hasData = false;
//消费数据
System.out.println("consumer consume a data: " + value);
notify();//唤醒生产者线程进行下一次生产
}
}
}
//存放数据的JavaBean
static class Data {
public int value;
}
}
如果存在多个生产者多个消费者,则应该把代码中的 notify()
改为notifyAll()
。因为这里所有的生产者消费者都阻塞在监视器对象this
上,notify的作用是随机唤醒一个等待该监视器对象的线程,如果一个生产者恰巧唤醒了另一个生产者线程,后面的生产者线程检查 hasData ,有数据,不能进行生产,那就继续 wait下去,这样所有的线程都在wait , 就是一个死锁状态了!如果是 notifyAll 则唤醒所有阻塞在该监视器上的线程,这样如果生产者线程获得锁查看有数据,进入wait状态,而消费者线程继而获取监视器锁并进行消费。
造成这种状况的根本原因是生产者消费者线程都部分彼此的排在同一个等待队列中,从而唤醒的时候不知道唤醒的是生产者还是消费者。
Java的concurrent包给我们提供了一种新型的锁,可以做到将生产者和消费者排在不同的队列(Condition)上,这样就可以有针对性的唤醒线程,既简化了编程,又提高了效率。
class ProducerConsumerDemo {
private static final int loopTime = 10;
private volatile boolean hasData = false;
private Data data;
private final Lock lock=new ReentrantLock();
private final Condition producerCondition=lock.newCondition();
private final Condition consumerCondition=lock.newCondition();
public void produce() {
for (int i = 0; i < loopTime; ++i) {
try {
lock.lock();
while (hasData) {
try { //继续等待
producerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
data = new Data();
data.value = i;
hasData = true;
System.out.println("producer produce a data: " + data.value);
//唤醒消费者线程
consumerCondition.signal();
}finally{
lock.unlock();
}
}
}
public void comsume() {
for (int i = 0; i < loopTime; ++i) {
try{
lock.lock();
while (!hasData) {
try { //继续等待
consumerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
int value = data.value;
hasData = false;
System.out.println("consumer consume a data: " + value);
//唤醒生产者线程
producerCondition.signal();
}finally{
lock.unlock();
}
}
}
//存放数据
static class Data {
public int value;
}
}
无论是使用JVM内置锁对象的 wait / nofity / nofityAll 还是Lock/Condition对象的await / signal / signalAll 方法实现生产者消费者模式都不是一个好的办法。这样的做法既复杂又容易出错,我们应该尽可能的用现有存在的同步工具来实现需要的功能。比如用BlockQueue来实现生产者—消费者模式将会简单很多:
class BlockedProducerConsumerDemo {
private static final int loopTime = 10;
private final BlockingQueue<Data> queue = new ArrayBlockingQueue<>(1);
public void produce() {
for (int i = 0; i < loopTime; ++i) {
try {
Data data = new Data();
data.value = i;
queue.put(data); //将数据放入队列中
System.out.println("producer produce a data: " + data.value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void comsume() {
for (int i = 0; i < loopTime; ++i) {
try {
Data data = queue.take(); //从队列中取出数据
int value = data.value;
System.out.println("consumer consume a data: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Data {
public int value;
}
}