1、什么是多线程和使用多线程的意义
2、多线程基础知识点框架图
3、实现多线程的三种方式
4、三种方式对比
1、什么是多线程合使用多线程的意义
- 单进程单线程:一个人在一个桌子上吃菜;(开桌子的意思是指创建进程。开销这里主要指的是时间开销。)
- 单进程多线程:多个人在同一个桌子上一起吃菜;
- 多进程单线程:多个人每个人在自己的桌子上吃菜;
多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。
- 对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。
- 对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点大家要学习进程间通讯的方法。
- 做个实验:创建一个进程,在进程中往内存写若干数据,然后读出该数据,然后退出。此过程重复 1000 次,相当于创建/销毁进程 1000 次。在我机器上的测试结果是:UbuntuLinux:耗时 0.8 秒;Windows7:耗时 79.8 秒。两者开销大约相差一百倍。这意味着,在 Windows 中,进程创建的开销不容忽视。换句话说就是,Windows 编程中不建议你创建进程,如果你的程序架构需要大量创建进程,那么最好是切换到 Linux 系统。
2、 多线程基础知识点框架图
3、实现多线程的三种方式
(1) 创建线程的第一种方式:继承Thread类,覆盖其中的run方法
public class Ticket extends Thread{
private static int ticketCount = 100;//此处必须是static,否则每个线程都会有100张票
public void run(){
while(true){
if(ticketCount>0){
System.out.println(Thread.currentThread().getName()+"------"+ticketCount--);
}
}
}
}
public static void main(String args[]){
//启动四个线程,分别执行
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
}
多个线程共享一个资源
(2) 实现Runnable接口,实现其中run方法
步骤:
1、定义类实现Runnable接口。
2、覆盖Runnable接口中的run方法中。将线程要运行的代码存放在改run方法中。
3、通过Thread类建立线程对象。
4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。自定义的run方法所属的对象是Runnable接口的子类对象。
5、调用Thread类的start方法开启线程并调用Runnable接口的子类的run方法。
//创建Ticket类,实现Runnable方法
public class Ticket implements Runnable{
private int ticketCount = 100;
public void run(){
while(true){
if(ticketCount>0){
System.out.println(Thread.currentThread().getName()+"------"+ticketCount--);
}
}
}
}
public static void main(String args[]){
Ticket t = new Ticket();
//启动四个线程,共享Ticket
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
(3) 实现Callable接口,重写call()方法
Callable接口实际上是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更加强大的功能。
Callable接口和Runnable接口的不同之处:
- Callable规定的方法是call,而Runnable是run
- Callable可以在任务结束的时候提供一个返回值,Runnable无法提供这个功能;
- Callable的call方法分可以抛出异常,而Runnable的run方法不能抛出异常。
多线程的实现有以下4个步骤:
step1 创建一个线程,创建Callable的实现类Race,并且重写call方法;
ExecutorService ser=Executors.newFixedThreadPool(线程数目);
Race tortoise = new Race();
step2 得到Future对象
Future<Integer> result=ser.submit(tortoise);
step3 获取返回值
int num=result.get();
step4 停止服务
ser.shutdown();
一个完整的例子
1 定义类,实现Callable接口
/**
* 方法4:不通过FutureTask,来实现Callable实例作为线程的执行体
*/
public class CreateThread_4 implements Callable<String> {
private static int ticket;
public CreateThread_4(int ticket){
this.ticket = ticket;
}
public String call() throws Exception {
return "this task num is "+ ticket;
}
}
2 单线程提交打印
- Callable接口中的call()方法的返回类型为Future接口类型,Future接口提供一个实现类FutureTask。FutureTask 还实现了Runnable接口,由于Thread的执行体只接受Runnable接口实例,所以如果想Callable实例作为Thread的执行体就必须通过FureTask来作为桥梁,即:正常情况下我们是将一个Runable接口实例设置进去(在这里这个实例即是FutureTask对象,因为FutureTask实现了Runable接口,FutureTask对象即是Runnable接口的一个实例,而Callable接口实例的类型是Future类型,而FutureTask实现了Future接口,所以Callable接口实例即相当于FutureTask实例:ps:其实不是,但是可以这样理解)。
下面看Callable接口实例作为Thread的执行体。
/**
* 一个线程
*/
@Test
public void createThread_4_call_1() {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new CreateThread_4(190));
try {
System.out.println(future.get());
} catch (Exception e) {
} finally {
threadPool.shutdown();
}
}
3 多线程提交打印
- 下面还介绍一种不通过FutureTask,来实现Callable实例作为线程的执行体
/**
* 多个线程
*/
@Test
public void createThread_4_call() {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> list = new ArrayList<Future<String>>();
for (int i = 0; i < 30; i++) {
CreateThread_4 callable = new CreateThread_4(i);//创建Callable接口实例
list.add(exec.submit(callable));//将callable接口实例提交进线程池
}
for (Future<String> fs : list) {
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
4、三种方法的对比
第一种方式(继承)和第二种方式(实现)的区别
1、继承Thread,线程代码放在Thread的run方法中;
2、实现Runnable,线程代码放在接口子类的run方法中;该实现方式可以避免单继承的局限性;
3、实现Callable和Runnable接口都能够创建线程的执行体,但是Runnable接口并不返回任何值,如果你希望任务在完成的时候能够返回一个值,就可以通过实现Callable接口来实现。在JavaAPI中是这样描述Callable接口的:Callable和Runnable相似,他们的类实例都能够被其他Thread执行,但是Callable接口能够返回一个值或者抛出一个异常,Runnable却不能,实现Callable接口需要重写其唯一的call()方法。