Java并发编程之基础篇(一)-- 线程与任务
进程和线程理解
提到并发编程,很多人会想到多线程;希望让多个线程共同完成一项任务,以提高生产效率。所以要聊并发编程之前,就要先说一下什么是线程,要说什么是线程就需要说一下什么是进程。
进程:在现代操作系统中,每一个独立运行的程序都是一个进程,比如运行中的word,微信等等都是一个独立进程。
线程:在现代操作系统中,线程也叫轻量级进程,每个进程里面可以包含多个线程。CPU资源可以在多个线程之间不断切换,仿佛所有线程在并行执行。每个线程都有自己的计数器,堆栈,和局部变量等属性。这些线程也能够访问共享的内存变量。这将成为日后阻碍我们写出健壮且安全的并发程序的最大障碍。
任务和线程的关系
在Java里面,任务和线程是两个完全独立的概念,所谓的线程就是你通过 Thread thread = new Thread() 方式创建的一个线程,如果你只是这样创建一个线程,那么什么意义都没有。因为这个线程什么都不会做,这条语句的意义,仅仅是创建了一个线程。你可以理解成,只创建了生产线上的一个工人,但是没给工人安排任务。
我们创建线程的目的显然是为了更高效的完成一些任务。所以我们需要把任务和线程进行关联。Java里面的任务是通过Runnable或者Callable接口来表达的。线程是驱动任务的媒介,必须将线程和任务进行绑定才有意义,他们才能各自发挥价值,真正的做到一加一大于二的效果。如果你不理解我说的这段话,没关系,下面会通过一些代码示例帮助大家理解线程和任务之间的关系。
如何创建线程
创建线程的方式很多人都会说有两种,一种通过继承Thread类来实现,另外一种是实现Runnable接口。从某种角度来看,这么说是没有错,但我更愿意说只有一种方式。因为Thread类也实现了Runnable接口。所以归根到底还都是通过实现Runnable接口来描述任务,然后将任务和线程进行绑定。
什么都不做的线程
创建一个线程并启动其实很简单,只需要两行代码。但是下面的线程什么都做不了。因为我们没有让他跟任何任务关联。
//创建线程
Thread thread = new Thread();
//启动线程
thread.start();
会说 hello world 的多线程
上面新建的线程,什么工作都做不了,显然不是我们想要的,下面我们再来创建一个线程,这个线程会打印出线程名字,并且说一句hello world。
Thread thread = new Thread(){
@Override
public void run(){
System.out.println(Thread.currentThread() + "hello world!");
}
};
thread.start();
上面的代码,new了一Thread对象,并且重写了run方法,将要执行的业务逻辑写到了run方法里面。这样就建立了线程和任务的关系。
通过继承实现自己的Thread
我们也可以通过继承的方式自定义一个Thread,如下:
public class HelloThread extends Thread {
private String name;
public HelloThread(String name){
this.name = name;
}
@Override
public void run(){
System.out.println( Thread.currentThread() + " hello " + name);
}
}
通过HelloThread()创建线程,并启动。
Thread thread = new HelloThread("Frank");
thread.start();
通过Runnable接口定义任务
上面示例都是通过重写Thread类的run方法,来完成业务逻辑的描述。我们也可以通过实现Runnable接口的方式,来实现业务逻辑的描述。
public class Task implements Runnable {
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"hello world");
}
}
将Runnable和Thread进行关联,让Thread驱动Runnable.
//创建线程,并绑定相应的业务逻辑
Thread thread = new Thread(new Task());
//启动线程,让线程驱动任务
thread.start();
使用Executors将线程和任务进行绑定
上面讲解的样例代码,在实际生产上很少用到,生产用的最多的是JDK1.5以后开始提供的Executor并发框架。这里先放一段代码示例,先简单感受一下,后面还会有专门的文章进行介绍。
//创建一个容量为2的线程池
ExecutorService exec = Executors.newFixedThreadPool(2);
//向线程池提交5个任务
for (int i = 0; i < 5; i++){
exec.execute(new Task());
}
exec.shutdown();
与线程相关的一些方法
yield()
yield
这个单词在英语里面有放弃,停止争论的意思,所以当我们执行Thread.yield();
方法的时候,表示当前线程愿意尝试放弃CPU资源,但是否放弃不确定。这个方法在日常工作中很少用到,主要用于debug,或者测试。
sleep()
sleep
这个单词大家应该很熟悉,睡眠的意思。所以当我们执行Thread.sleep(1000);
表示当前线程休眠1秒钟,然后继续执行。
join()
join
这个单词有连接,加入的意思。所以当调用join()
方法的时候,表示当前线程会等待调用join()这个方法的线程执行完成之后在继续执行。这么说会比较抽象,下面我写个简单的例子,让各位感受一下。
定义一个任务,该任务接收一个外部传入的线程,并调用传入线程的join()方法
public class JoinTask implements Runnable{
private Thread thread;
public JoinTask(Thread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "currentTime " + System.currentTimeMillis());
}
}
在main方法里面启动两个线程
Thread thread1 = new Thread(){
@Override
public void run(){
System.out.println( currentThread() + "currentTime " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread2 = new Thread(new JoinTask(thread1));
//启动两个线程
thread1.start();
thread2.start();
这段代码的输出结果为
Thread[Thread-0,5,main]currentTime 1588752684516
Thread[Thread-1,5,main]currentTime 1588752685516
从结果可以看出线程2等待线程1执行结束后,才继续执行。大家可以把join()方法注释掉之后在运行一下,感受一下join()的作用。
这个方法理论上可以实现简单的任务间协作(实际生产很少这样用),比如洗完盘子才能进行烘干。烘干线程要等待洗盘子线程结束才能开始工作。