美文网首页
多线程基础

多线程基础

作者: 随时学丫 | 来源:发表于2017-11-12 15:11 被阅读21次

线程和进程的基本概念

一、进程和线程

进程

进程:指在系统中正在运行的一个应用程序。

每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间。

比如同时打开QQ、Xcode,系统就会分别启动2个进程

通过“活动监视器”可以查看Mac系统中所开启的进程

进程.png

线程

线程:一个进程想要执行任务,必须得有线程(每个进程至少有一个线程)

线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行

比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行

线程.png

线程的串行

一个线程中任务的执行是串行的。如果要在一个线程中执行多个任务,那么只能一个一个按照顺序去执行。在同一时间里,一个线程只能执行一个任务。

在一个线程中下载3个文件(分别是A、B、C)

多线程下载.png

二、多线程

多线程:一个进程中可以开启多个线程。每条线城可以并行(同时)执行不同的任务。

进程 -> 车间,线城 -> 车间工人

同时开启3条线程分别下载3个文件(A、B、C)

并发和并行.png

多线程原理

同一时间,CPU只能处理一条线程,只有一条线程在工作(执行),多线程并发(同时)进行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发访问的假象。

思考:如果线程非常多,会发生什么情况?

CPU在N多线程之间调度,CPU会累死,消耗大量CPU资源。每条线程被调度执行的频次会降低(线程执行的效率降低)

多线程的优点

  • 能适当提高程序执行效率
  • 能适当提高资源利用率(CPU、内存利用率)

多线程的缺点

  • 开启线程需要占用一定的内存空间(默认情况下下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量内存空间,降低程序性能。
  • 线程越多,CPU在调度线程上的开销就越大
  • 程序设计更加复杂:比如线程之间的通信,多线程的数据共享

并发与并行

并行:多个CPU实例或多台机器同时执行一段处理逻辑,是真正的同时

并发:通过CPU调度算法,让用户看上去同时执行,实际上从CPU操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源网网产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

线程安全:经常用来描述一段代码。指在并发情况下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只关注系统的内存,CPU是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。

如不加事务的转账代码

void transferMoney(User from, User to, float amount){
  to.setMoney(to.getBalance() + amount);
  from.setMoney(from.getBalance() - amount);
}

同步:Java中的同步指的是人为控制和调度,保证共享资源的多线程访问完成线程安全,来保证结果的准确。如上面的代码简单加上@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

三、线程的状态

线程状态转换.jpeg
  1. 新建状态(NEW):新创建一个线程对象。

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start() 方法,该状态的线程位于可运行线程池中,等待获取CPU的使用权。

  3. 运行状态(Running):就绪状态的线程获取了CPU,执行代码程序。

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

    阻塞分三种:

    • 等待阻塞:运行到线程执行了 wait() 方法,JVM会把该线程放入等待池中。
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别到线程占用,则JVM会把该线程放入锁池中。
    • 其他阻塞:运行到线程执行sleep()或join()方法,或者发出I/O请求时,JVM会把线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程执行完了或者因为异常退出了run()方法,该线程结束生命周期。

四、创建线程的三种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用Callable和Future

继承Thread类

  1. 定义Thread类的子类,并重写run方法,该方法的方法体是需要线程完成的任务
  2. 创建Thread子类的实例,也就创建了线程的对象
  3. 启动线程,即是调用线程的start()方法。

注意:线程的start()和run()方法都可以被线程实例调用,只有调用start()方法才是开启线程,拥有线程的特性,而只调用run()方法,相当于普通方法。

public class MyThread extends Thread{//继承Thread类
  public void run(){
    //重写run方法
  }

}
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

实现Runnable接口

  1. 定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
  2. 创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
  3. 第三部依然是通过调用线程对象的start()方法来启动线程
public class MyThread2 implements Runnable {//实现Runnable接口
  public void run(){
    //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread2 myThread=new MyThread2();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者    new Thread(new MyThread2()).start();
  }

}

使用Callable和Future

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

  1. call()方法可以有返回值
  2. call()方法可以声明抛出异常

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

  • boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
  • V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
  • V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
  • boolean isDone():若Callable任务完成,返回True
  • boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

创建病启动有返回值的线程

  1. 创建Callable接口的实现类,并实现call()方法,然后创建该类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class Main {
  public static void main(String[] args){
   MyThread3 th=new MyThread3();
   //使用Lambda表达式创建Callable对象
    //使用FutureTask类来包装Callable对象
   FutureTask<Integer> future=new FutureTask<Integer>(
    (Callable<Integer>)()->{
      return 5;
    }
    );
   new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程
    try{
    System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回
    }catch(Exception e){
    ex.printStackTrace();
   }
  }
}

三种创建线程方式的对比

实现Runnable和实现Callable接口的方式基本相同,不过Callable执行call()方法有返回值,Runnable执行run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:

1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。

2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。

4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。

注:一般推荐采用实现接口的方式来创建多线程

参考:

https://www.cnblogs.com/3s540/p/7172146.html

https://www.cnblogs.com/yxt9322yxt/p/4804026.html

相关文章

网友评论

      本文标题:多线程基础

      本文链接:https://www.haomeiwen.com/subject/vianmxtx.html