java 多线程简介

进程和线程

程序(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

 
 

猜你喜欢

转载自blog.csdn.net/sinat_41132860/article/details/84437286