进程和线程
程序(program)是对数据描述与操作的代码的集合,是应用程序执行的脚本。
进程(process)是程序的一次执行过程,是系统运行程序的基本单位。程序是静态的,进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。可以认为每个程序就是一个进程,但有些应用程序会有多个进程,即一个应用程序至少会启动一个进程.,多进程的实现基础是CPU时间片轮训或者抢占式执行
线程(thread)是进程中的一个执行场景,一个进程可以启动多个线程,使用多线程可以提高CPU的使用率,不是提高执行速度。
线程是进程内部的一个独立执行单元,相当于一个子程序,一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源,多线程能实现的基础是CPU轮训到该进程时,该进程又分为多个时间片(对应每个线程),在这个进程执行的时间里轮流执行
举个例子,我们知道机械磁盘的IO时间大概是ms级别,CPU处理速度是ns级别,电脑要同时读取两个压缩文件并解压,读取每个文件需要10秒,解压操作需要5秒,先读去A,再读取B的这段时间解压A,再解压B,可以将读取B文件的空余时间充分利用
多任务(multi task)一个系统中可以同时运行多个程序,即有多个独立运行的任务,每个任务对应一个进程。
进程示例:下图显示了多个程序同时运行(多线程),每个程序可能会有多个进程.
并行和并发
并行是指两个或两个以上的任务同时运行,即A任务在执行,B任务也在执行,C任务也在执行,常指进程(需要多核CPU或者多台电脑,比如常见的Xgboost可以并行运行一个算法)
并发是指在同一时间片段同时执行,比如两个任务请求执行,CPU只能先执行一个,将两个任务轮流执行,类似于串行.
上述一个程序最少有一个进程(可以多个),一个进程最少有一个线程(可以多个),多个进程之间相互独立,可以并行运行,多个线程只能并发执行,实际还是顺序执行,单核的cpu同一个时刻只支持一个线程任务,多线程并发就是多个线程排队申请调用cpu
以下重点论述多线程:
优缺点:
多线程的优点
提高CPU利用率;可以随时停止任务;可以设置各个任务的优先级以优化性能,提高程序的执行效率;线程执行完成后会自动销毁节省内存。
多线程的缺点:
设计复杂:多线程共享堆内存和方法区,因此里面的一些数据是可以共享的,在设计时要确保数据的准确性
资源消耗增多:多线程不共享栈内存,开启多线程会增加内存的消耗
创建多线程的三种方式
1Thread类
继承Thread可以创建线程,2中的单独实现Runnable接口并不能创建线程,还要借助Thread类的对象
使用Thread类创建线程的方式:
1自定义类继承Thread类
2.重写run方法
3.在run方法中编写线程中执行的代码
4.创建上面自定义类的对象
5.调用start方法启动线程
先看Thread的常用的构造方法:
Thread() 创建新的线程对象
Thread(String name) 基于指定的名字创建一个线程对象
Thread(Runnable target)基于Runnable接口实现类的实例(可以是匿名内部类)创建一个线程对象
Thread(Runnable t,String name) 根据给定的Runnable接口实现类的实例和指定的名字创建一个线程对象
常用方法:
void run() :包括线程运行时执行的代码,通常在子类中重写它。
synchronized void start():启动一个新的线程,然后虚拟机调用新线程的run方法
代码示例
package createThreads;
public class Thread1 {
public static void main(String[] args){
FirstThread ft=new FirstThread();
ft.start();
for (int i=0;i<50;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
class FirstThread extends Thread{
@Override
public void run() {
for (int i=0;i<50;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
out:
Thread-0 : 0
main : 0
Thread-0 : 1
main : 1
Thread-0 : 2
Thread-0 : 3
Thread-0 : 4
Thread-0 : 5
Thread-0 : 6
Thread-0 : 7
main : 2
Thread-0 : 8
main : 3
Thread-0 : 9
main : 4
main : 5
main : 6
main : 7
main : 8
main : 9
可以看到两个线程的方法在同时执行,如果将线程启动语句放到for循环后面,则不会多线程打印,原因是main方法的内容是顺序执行的,只有start()方法在main方法体中前面部分,线程启动后继续执行后面的语句,才能实现多线程交替打印
练习,不考虑线程安全的问题,让两个线程一起打印0-100直接的数字,两种方式,共享静态变量,共享堆内存中的对象(属性和方法)
1静态变量共享,通过共享静态变量的方式
public class ThreadTest01 {
public static void main(String[] args){
int i=0;
MyThread m1=new MyThread("线程1",i);
MyThread m2=new MyThread("线程2",i);
m1.start();
m2.start();
}
}
class MyThread extends Thread{
static int i;
public MyThread(String name,int i){
super(name);
this.i=i;
}
@Override
public void run() {
for (;i<100;i++){
System.out.println(getName()+" : "+i);
}
}
}
改进版:在上述代码上稍作精简
public class ThreadTest02 {
public static void main(String[] args){
MyThread1 m1=new MyThread1("线程1");
MyThread1 m2=new MyThread1("线程2");
m1.start();
m2.start();
}
}
class MyThread1 extends Thread{
private static int i;
public MyThread1(String name){
super(name);
setI(0);
}
public static int getI() {
return i;
}
public static void setI(int i) {
MyThread.i = i;
}
@Override
public void run() {
for (;i<100;i++){
System.out.println( getName()+" :"+i);
}
}
}
对象成员变量共享,这种方法在定义类构造方法时,接收一个对象,共用对象的成员变量,这种方式稍显麻烦
public class ThreadTest03 {
int i=0;
public static void main(String[] args){
ThreadTest03 tt03=new ThreadTest03();
MyThread2 mt1= new MyThread2("线程1",tt03);
MyThread2 mt2=new MyThread2("线程2",tt03);
mt1.start();
mt2.start();
}
}
class MyThread2 extends Thread{
ThreadTest03 tt;
public MyThread2(String name,ThreadTest03 tt03){
super(name);
tt=tt03;
}
@Override
public void run() {
for (;tt.i<100;tt.i++){
System.out.println(getName()+" : "+tt.i);
}
}
}
2Runnable接口
Runnable接口的源码:
public interface Runnable {
public abstract void run();
}
Runnable 接口中只有一个 run抽象 方法,实现该接口的类要重写该方法.
使用Runnable接口创建线程的步骤:
.1自定义类实现Runnable接口
2.重写run方法,run方法中是线程体
4.创建上述自定义类的对象
5.创建Thread对象,将上述自定义类对象作为参数传入Thread的构造方法
6.调用start方法启动线程
Thread类的其中一个构造方法
Thread(Runnable target)基于Runnable接口实现类的实例(可以是匿名内部类)创建一个线程对象
使用示例:两个线程各自打印0-20,不考虑线程安全
public class RunnableTest01 {
public static void main(String[] args){
MyRunnable mr=new MyRunnable();
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<20;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
out:
Thread-1 : 0
Thread-0 : 0
Thread-1 : 1
Thread-0 : 1
Thread-1 : 2
Thread-0 : 2
Thread-1 : 3
Thread-0 : 3
Thread-1 : 4
Thread-0 : 4
Thread-1 : 5
Thread-0 : 5
Thread-1 : 6
Thread-0 : 6
Thread-1 : 7
Thread-0 : 7
Thread-1 : 8
Thread-0 : 8
Thread-1 : 9
Thread-0 : 9
Thread-1 : 10
Thread-0 : 10
Thread-1 : 11
Thread-0 : 11
Thread-1 : 12
Thread-0 : 12
Thread-1 : 13
Thread-0 : 13
Thread-1 : 14
Thread-0 : 14
Thread-0 : 15
Thread-0 : 16
Thread-1 : 15
Thread-0 : 17
Thread-1 : 16
Thread-0 : 18
Thread-1 : 17
Thread-0 : 19
Thread-1 : 18
Thread-1 : 19
接上面的练习,实现Runnable接口,两个线程共同打印0-50,不考虑线程安全,代码要简洁的多
public class RunnableTest02 {
public static void main(String[] args){
MyRunnable1 mr1=new MyRunnable1();
Thread t1=new Thread(mr1,"线程1");
Thread t2=new Thread(mr1,"线程2");
t1.start();
t2.start();
}
}
class MyRunnable1 implements Runnable{
int i=0;
@Override
public void run() {
for (;i<50;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
Thread类和Runnable接口的区别:
- 1 Runnable接口更适合资源的共享
- 2 java的单继承,多实现功能,继承了其他类的类可以实现Runnable接口(必须重写run方法),而Thread类的继承类可以不重写run方法
- 3 Runnable接口的实现类,并不是真正的线程类,只是线程运行的目标类,若要以线程的方式执行run方法,需要依赖Thread类(及其子类)
3Callable接口
jdk1.5加入了Callable接口,实现Callable接口创建的线程会获取一个返回值,并且可以声明异常(Callable接口再java.lang包)
在叙述Cllable接口创建线程之前,先介绍一个概念(在这里不深入探究,以后写篇文章专门叙述这块内容):
线程池:
新来一个任务,要创建一个线程,线程任务结束后,线程会被销毁,资源回收。多次这样创建线程,销毁线程的话带来严重的系统开销,同时也不好管理工作线程。线程池解决了创建单个线程耗费时间和资源的问题
线程池是一种预创建线程的技术,先在线程池创建多个线程的集合,当执行完任务需要创建一个线程时,不需要再创建一个线程,而是直接去线程集合中获取,当任务结束时,不销毁这个线程,将这个线程放入线程池管理,等待线程池任务调度.
创建线程池的方式:
Executor接口,该接口只有一个接收Runnable对象的execute方法
ExecutorService是线程池接口,继承自Executor接口,ExecutorService里面有操作线程池的方法
- Future<T> submit(Callable<T> task),传入Callable对象的实现类,即提交任务,返回Future<T>类型对象
- void shutdown(),关闭线程池,不会再接收新的线程,未执行完成的线程不会被关闭
Executers:Executers类提供了四种线程池,有许多方法,其中列举几个需要用到的
- static ExecutorService newFixedThreadPool(int nThreads)返回一个创建好的固定线程个数的线程池对象,如果任务数量大于线程数量,则任务会进行等待。
- public static ExecutorService newCachedThreadPool(),返回线程池对象,该线程池对象根据需要创建线程个数,如果线程池内线程个数小于人物个数则会创建线程,,最大线程数量是Integer.MAX_VALUE,如果线程的处理速度小于任务的提交速度时,会不断创建新的线程来执行任务,可能会因为创建线程过多而耗尽系统资源(CPU和内存)
Future<T>接口的实现类用来接收多线程的执行结果
V get() throws InterruptedException, ExecutionException;get方法用来接收Callable实现类的call方法的返回值
Callable接口
public interface Callable<V> {
V call() throws Exception;
}
Callable接口只有一个call方法,返回一个泛型类型的对象,并且可以抛出异常,实现Callable接口,重写call方法可以创建线程
使用Callable接口创建线程的步骤:
- 1创建自定义类实现Callable接口,并重写call方法(线程体),call方法有返回值,且要声明或捕获异常
- 2创建ExecutorService线程池对象
- 3.将自定义类的对象放入线程池里面
- 4.获取线程的返回结果
- 5.关闭线程池,不再接收新的线程,未执行完的线程不会被关闭
使用示例:
package createThreads;
import java.util.concurrent.*;
public class CallableTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池对象,使用了多态
ExecutorService es=Executors.newFixedThreadPool(3);
//创建Callable接口的实现类对象
MyCallable mc1=new MyCallable(5);
MyCallable mc2=new MyCallable(4);
MyCallable mc3=new MyCallable(3);
//将自定义对象方法线程池中
Future <Integer>f1=es.submit(mc1);
Future <Integer>f2=es.submit(mc2);
Future <Integer>f3=es.submit(mc3);
//输出get方法返回的结果
System.out.println(f1.get()+":"+f2.get()+":"+f3.get());
//关闭线程池
es.shutdown();
}
}
class MyCallable implements Callable<Integer>{
private int count;
public MyCallable(int count) {
setCount(count);
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
//重写call方法
@Override
public Integer call() throws Exception {
//计算立方
int temp=0;
temp=count*count*count;
return temp;
}
}
out:
125:64:27
三种基本的创建方式介绍完了,回过头来看,三种方式创建线程的优缺点
继承Thread
优点:可以直接使用Thread类中的方法,重写run方法,代码简单
缺点:继承Thread类之后不能继承其他类,且不方便数据共享
实现Runnable接口
优点:在自定义类有继承父类的情况下,还可以使用Runnable接口,重写run方法,且方便数据共享
缺点: 在run方法内部需要获取到当前线程的Thread对象后才能使用Thread中的方法
实现Callable接口
优点:可以获取返回值,可以抛出异常
缺点:编写代码比较繁琐
参考:http://www.monkey1024.com/javase/655
http://www.monkey1024.com/javase/653