多线程
概念
程序:
是为了完成特定任务,用某种语言编写的一组静态代码,静态对象
进程:
是一个程序的一次执行过程,或是正在运行的程序,是一个动态的过程,存在生命周期。一个进程可以有一个或多个线程,进程作为内存资源分配的单元,系统在运行时会为每个进程分配不同的内存空间。
线程:
线程是进程细化的概念,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,那么其称之为 — 多线程
线程作为执行和调度的单位,每一个线程,拥有独立的运行栈和程序计数器(PC),线程切换的开销小
一个进程内的多个线程共同分享进程中的内存空间,他们从同一个堆空间中分配对象,他们都可以访问,修改该进程中的变量和对象,这使得多个线程之间可以很方便的使用变量和对象,但是这也造成了线程的不安全
并行:多个CPU同时执行多个任务 例如:一起考试
并发:多个CPU同时执行相同任务 例如:商品抢购
多线程的优缺点以及使用场景
缺点:
执行多个任务,单线程快于多线程
优点:
提高应用程序响应,增强用户体验
提高计算机CPU利用率
改善程序结构,可以将一个长且复杂的线程分化为很多简单的线程,易于理解与修改
使用场景:
程序需要多个线程一起执行
执行一些需要等待的任务时,比如 加载,下载,网络请求,文件读写等
程序需要后台运行时
线程的创建方式
1.继承的方式
创建一个继承于Thread类的子类
重写其中run方法
创建其对象
执行该对象的.start( )方法
public class MultiThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
new Thread(){
@Override
public void run() {
System.out.println("匿名子类重写的run方法");
}
}.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("重写的run方法");
}
}
tips:如果该线程中的方法只使用一遍,比较多的采用匿名类的方式。想要执行多线程,必须使用Thread的start( )方法。
Thread.start( )方法的作用:1.启动该线程(主线程),2.调用该线程的run方法(子线程)。如果直接调用run方法,虽然程序还是可以正常执行,但是还是在主线程中(造对象,调用对象的方法,没有分出线程)
2.实现Runnable接口
创建一个实现类去实现Runnable接口
实现其中的run( )方法
创建该实现类的对象
将该对象填入Thread的构造器构造出Thread对象
最后通过Thread对象调用其中的start( )方法开启该线程,并执行该线程中的run( )方法
public class MultiThreadTest {
public static void main(String[] args) {
//创建实现类对象
MyThread myThread = new MyThread();
//将此对象填入Thread类的构造器中,创建Thread对象
Thread thread = new Thread(myThread);
//利用start()方法启动该线程,并调用该线程的run()方法
thread.start();
}
}
//创建Runnable的实现类
class MyThread implements Runnable{
//实现Runnalbe其中run方法
@Override
public void run() {
System.out.println("重写Runnable接口中的run方法");
}
}
为什么start( )方法中说明的是,启动该线程并实现该线程(Thread)的run( )方法。但是上述代码中的MyThread类中并没有重写Thread中的run( )方法,而是实现了接口Runnable中的run( )方法,这是怎么调用到的呢?
Thread源码:
//如果Thread类中的run没有被重写,执行的是target的run方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
//构造器中填入Runnable的实现类,给target赋值
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
由Thread代码可知,Thread 类中的run( )方法再没有被重写时,执行的是target的run( ),又由构造器得知,这个target实际上就是一个Runnable接口的实现类。简而言之,如果Thread类中的run( )方法没有被重写,那么执行的就是Thread类构造器中Runnable接口实现类的run( )方法。
当然如果重写了Thread类中的run( )方法,那执行的一定是被重写过的(线程创建方式一)
3.实现Callable接口
创建Callable的实现类
完成Callable接口中的call( )方法
创建该实现类的对象
把对象作为参数传入FutureTask类的构造器 创建FutureTask类的对象
把FutureTask类的对象作为参数填入Thread类的构造器,创建Thread对象并使用start()开启该线程
*Callable接口中的call( )方法允许返回值,如果需要返回值,可以用FutureTask类的对象的get( )方法接收其返回值
public class CallableTest {
//创建Callable的实现类
class MyThread implements Callable{
//重写接口中的call方法
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 20 ; i++) {
if (i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
public static void main(String[] args) {
//创建Callable接口的实现类对象
MyThread myThread = new MyThread();
//用callable实现类的对象创建FutureTask接口的对象
FutureTask futureTask = new FutureTask(myThread);
//新建线程并start
new Thread(futureTask).start();
//使用FutureTask类的对象的get()方法 接收call()方法的返回值
try {
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
4.线程池 一般使用
使用工具类Executors的newXXXThreadPool()的方法创建ExecutorService接口的实现类对象
Executors.newCachedThreadPool(); 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor(); 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(); 创建一个线程池,可以设置给定时间后运行或定期执行
用ExecutorService接口的实现类对象的execute()或submit()方法执行线程 其中execute( )方法适用于实现Runnable接口的线程,而submit( )适用于实现Callable接口的线程,submit( )方法拥有返回值,返回值为FutureTask<>
使用结束调用shutdown( )方法关闭线程池连接
public class ThreadPoolTest {
public static void main(String[] args) {
//Executors.newXXXThreadPool()的方式创建ExecutorService接口的实现类对象
//这里newFixedThreadPool固定创建了10个线程大小的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//用ExecutorService接口的实现类对象的execute()或submit()方法执行线程
//.execute()方法适用于Runnable接口的实现类对象
service.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
});
//.submit()方法适用于Callable接口的实现类对象 因为Callable接口有返回值
// 使得submit()方法也有返回值,可以选择不接收
FutureTask<Integer> futureTask = (FutureTask<Integer>) service.submit(new Callable() {
@Override
public Integer call() throws Exception {
return 15;
}
});
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//关闭线程池连接
service.shutdown();
}
}
tips:线程池的管理函数(用ExecutorService接口的实现类对象调用 ,常见如ThreadPoolExecutor)
ThreadPoolExecutor s = (ThreadPoolExecutor) Executors.newFixedThreadPool(10); //创建ExecutorService接口的实现类对象
s.setCorePoolSize(); 设置核心池的大小
s.setKeepAliveTime(); 线程没有任务时最多保持多长时间后会被终止
s.setMaximumPoolSize(); 最大线程数
......
方式一和方式二的比较
在开发中是优先选择方式二(实现Runnable接口)创建线程的
原因:
1.实现Runnable接口天然实现数据的共享(创建Thread类对象时传入的都是同一个Runnable实现类对象时)
2.实现Runnable接口不是继承,继承有唯一性,而接口的实现没有数量要求,扩展性高
实现Callable接口创建线程的方式较之前的方法更为强大
原因:
1.Callable接口拥有返回值
2.Callable接口可以抛出异常
3.Callable接口支持泛型
线程池的优势 为什么项目中都常用线程池
真正的使用时,一个个创建线程,销毁线程太麻烦了。特别是经常创建销毁特别大的资源时,对性能的影响非常大
不如先创建好线程,再将其放入线程池中,使用的时候调用,不需要的时候归还线程池(不销毁),这样创建和销毁的次数就大大减少,实现重复利用
优势:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中的线程,不需要每次都创建和销毁)
便于线程集中管理
2.Callable接口可以抛出异常
3.Callable接口支持泛型
线程池的优势 为什么项目中都常用线程池
真正的使用时,一个个创建线程,销毁线程太麻烦了。特别是经常创建销毁特别大的资源时,对性能的影响非常大
不如先创建好线程,再将其放入线程池中,使用的时候调用,不需要的时候归还线程池(不销毁),这样创建和销毁的次数就大大减少,实现重复利用