6. 多线程基础

简介: 对一个程序的运行状态, 以及在运行中所占用的资源(内存, CPU)的描述;一个进程可以理解为一个程序; 但是反之, 一个程序就是一个进程, 这句话是错的。

6.多线程


6.1 多线程概念


进程是什么(process)


对一个程序的运行状态, 以及在运行中所占用的资源(内存, CPU)的描述;


一个进程可以理解为一个程序; 但是反之, 一个程序就是一个进程, 这句话是错的。


进程的特点:


  • 独立性: 不同的进程之间是相互独立的, 相互之间资源不共享;
  • 动态性: 进程在程序中不是静止不动的, 而是一直是活动状态;
  • 并发性: 多个进程可以在一个处理器上同时运行, 互不影响;


线程是什么(thread)


是进程的一个组成部分, 一个进程中可以包含多个线程, 每一个线程都可以去处理一项任务;


进程在开辟的时候, 会自动的创建一个线程, 这个线程叫做 主线程;


一个进程包含多个线程, 且至少是一个, 如果一个进程中没有线程了, 这个进程会被终止;


多线程的执行是抢占式的, 多个线程在同一个进程中并发执行任务, 其实就是CPU快速的在不同的线程之间进行切换,


进程与线程的关系和区别


  • 一个程序运行后, 至少有一个进程
  • 一个进程包含多个线程, 至少一个线程
  • 进程之间是资源不共享的, 但是线程之间是资源共享的
  • 系统创建进程的时候, 需要为进程重新分配系统资源, 而创建线程则容易很多, 因此使用多线程在进行并发任务的时候, 效率比多进程高


区别并行和并发


并行:指在同一时刻,有多条指令在多个处理器上同时执行 ;


并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行;


6.2 创建线程的方式


继承Thread类


Thread是所有线程类的父类, 实现了对线程的抽取和封装;


使用继承Thread类实现多线程的步骤:


  • 继承Thread类, 写一个Thread的子类
  • 在子类中, 重写父类中的run方法, run方法就代表了这个线程需要处理的任务(希望这个线程处理什么任务, 就把这个任务写到run方法中), 因此, run方法也被称为 线程执行体
  • 实例化这个子类对象, 即是开辟了一个线程
  • 调用start方法, 来执行这个线程需要处理的任务(启动线程)
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
  System.out.println("main线程!"+i);
}
}
}
//自定义线程类
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
  System.out.println(getName()+":正在执行!"+i);
}
}
}


实现Runnable接口


实现Runnable接口, 并创建线程的步骤:


  • 设计一个类, 实现Runnable接口, 并重写接口中的run方法, 在run方法中, 写上这个线程要处理的任务
  • 创建Runnable实现类的对象, 并把这个对象作为Thread的target进行Thread对象的实例化, 这个Thread对象才是真正的线程对象
  • 调用start方法, 来启动线程
public class Demo02 {
  public static void main(String[] args) {
    //创建线程执行目标类对象
    Runnable runn = new MyRunnable();
    //将Runnable接口的子类对象作为参数传递给Thread类的构造函数
    Thread thread = new Thread(runn);
    Thread thread2 = new Thread(runn);
    //开启线程
    thread.start();
    thread2.start();
    for (int i = 0; i < 10; i++) {
      System.out.println("main线程:正在执行!"+i);
    }
  }
}
//自定义线程执行任务类
public class MyRunnable implements Runnable{
  //定义线程要执行的run方法逻辑
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println("我的线程:正在执行!"+i);
    }
  }
}

Callable


 class CallableThreadDemo implements Callable<Integer> {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask<Integer> ft = new FutureTask<>(ctt);
        new Thread(ft, "有返回值的线程").start();
        System.out.println("子线程的返回值" + ft.get());
    }
    @Override
    public Integer call() {
        int i;
        for (i = 0; i < 10; i += 2) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return i;
    }
}

三种方式的不同点


实现Runnable接口的方式:


  • 线程对应的任务类, 只是实现了Runnable接口, 不会对原有的继承关系产生影响
  • 多个线程可以使用同一个Runnable接口实现类对象来实例化, 非常适合多个线程访问相同的资源情况
  • 弊端:
    1. 编程稍微复杂, 不直观
    2. 如果需要获取到当前线程, 只能使用 Thread.currentThread() 来获取


继承Thread的方式:


  • 编程简单, 直观,
  • 如果需要访问当前线程, 直接用this即可, 也可以用 Thread.currentThread()
  • 弊端:因为线程类继承自Thread类, 因此这个类不能再继承自其他类


实际上, 大多数需要用到多线程的情况, 都是使用的Runnable接口的方式来实现的 [推荐使用匿名内部类]


strart() 和 run() 的区别


start方法会开辟一个线程, 然后在这个新的线程中执行run中的逻辑


但是如果直接调用run(), 则表示需要在当前的线程中执行逻辑。


Runnable与Callable


相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程


不同点:Runnable没有返回值;Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果


Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛


:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。


6.3 线程常用的方法


线程的名字


Thread thread = new Thread();
// 设置线程的名字
// 需要放到线程启动之前
thread.setName(name);
// 获取线程名字
thread.getName();

线程休眠 sleep


让一个指定的线程, 释放CPU时间片, 进入阻塞状态


休眠时间结束后, 线程进入就绪状态, 开始争抢时间片


Thread.sleep(500): 时间单位是毫秒


会有一个InterruptedException异常


设置线程优先级 priority


可以通过设置线程的优先级, 来修改某一个线程抢到时间片的概率, 优先级越高的线程, 抢到CPU时间片的概率越高, 但是这不意味者, 优先级高的线程一定能抢到时间片


默认情况下, 每一个线程的优先级, 都与创建他的线程优先级相同


优先级的范围 [0,10], 新建的线程, 优先级默认是5.


设置线程的优先级, 需要在线程启动(start)之前


thread.setPriority(2);

合并线程 join


在一个线程执行的过程中, 如果遇到了其他线程需要合并进来, 此时这个线程会释放CPU时间片, 去执行合并进来的线程. 合并进来的线程执行结束后, 再执行原来的逻辑.


特点:


  • 线程合并, 当前线程一定会释放CPU时间片, 优先执行合并进来的线程
  • 哪个线程需要合并到当前线程中, 就在当前线程中, 添加要合并的线程
  • 合并之前, 线程一定要处于start


后台线程


一个隐藏起来的, 一直默默地在后面执行的线程, 直到程序结束


又叫做 守护线程, 或者精灵线程,


JVM中的垃圾回收机制就是一个很典型的守护线程


如果所有的前台线程都死了, 后台线程会自动死亡


线程让步 yield


可以让当前的线程暂停, 但是不会进入阻塞, 只是从运行状态切换到了就绪状态, 完全可能出现的情况: 某一个线程yield暂停, 线程调度器又将其调度出来继续执行


实际上, 当某个线程使用yield让步后, 只有优先级与当前线程相同, 或者大于当前线程优先级的线程, 才有执行的机会


线程的生命周期


对于一个线程, 在被创建后, 不是立即就进入到了运行状态, 也不是一直处于运行状态, 在线程的声明周期中, 一个线程会在多种状态之间进行切换


new :创建 状态, 线程被实例化, 但是还没有开始执行(start)


runnable: 就绪状态, 已经执行过start, 线程已经启动了, 只是没有抢到CPU时间片


running: 运行状态, 抢到了CPU时间片


blocked: 阻塞状态, 线程执行的过程中, 遇到一些特殊情况, 会进入阻塞状态. 阻塞中的线程, 是不能参数时间片的抢夺的 (不能被线程调度器调度)


dead: 死亡状态, 线程终止


正常死亡 : run方法中的代码执行结束


非正常死亡 : 强制使用stop方法停止这个线程


1.png


6.4 线程安全


临界资源问题:由于线程之间是资源共享的。如果有多个线程,同时对一个数据进行操作,此时这个数据会出现问题。


如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。


public class ThreadDemo {
  public static void main(String[] args) {
    //创建票对象
    Ticket ticket = new Ticket();
    //创建3个窗口
    Thread t1  = new Thread(ticket, "窗口1");
    Thread t2  = new Thread(ticket, "窗口2");
    Thread t3  = new Thread(ticket, "窗口3");
    t1.start();
    t2.start();
    t3.start();
  }
}
//模拟票
class Ticket implements Runnable {
  //共5票
  int ticket = 5;
  @Override
  public void run() {
    //模拟卖票
    while(true){
      if (ticket > 0) {
        //模拟选坐的操作
        try {
          Thread.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
      }
    }
  }


如果有一个线程在访问一个临界资源,在访问之前,先对这个资源“上锁”,此时如果有其他的线程也需要访问这个临界资源,需要先查这个资源有没有被上锁,如果没有被上锁,此时这个线程可以访问这个资源;如果上锁了,则此时这个线程进入阻塞状态,等待解锁。


6.5 线程同步


同步代码段

// 同步代码段
// 小括号:就是锁
// 大括号:同步代码段,一般情况下,写需要对临界资源进行的操作
synchronized () {
}
// 关于同步锁:可以分成两种:对象锁、类锁
// 同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
public class Ticket implements Runnable {
  //共10张票
  int ticket = 10;
  //定义锁对象
  Object lock = new Object();
  @Override
  public void run() {
    //模拟卖票
    while(true){
      //同步代码块
      synchronized (lock){
        if (ticket > 0) {
          //模拟电影选坐的操作
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
        }
      }
    }
  }
}


同步方法

// 使用synchronized关键字修饰的方法就是同步方法
// 将一个方法中所有的代码进行一个同步
// 相当于将一个方法中所有的代码都放到一个synchronized代码段中
// 同步方法的锁:
// 1. 如果这个方法是一个非静态方法:锁是this
// 2. 如果这个方法是一个静态方法:锁是类锁(当前类.class)
private synchronized void sellTicket() {
}



同步方法解决安全问题


public class Ticket implements Runnable {
  //共100票
  int ticket = 100;
  //定义锁对象
  Object lock = new Object();
  @Override
  public void run() {
    //模拟卖票
    while(true){
      //同步方法
      method();
    }
  }
//同步方法,锁对象this
  public synchronized void method(){
    if (ticket > 0) {
      //模拟选坐的操作
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
    }
  }
}


lock与unlock


就是一个类RenntrantLock


public class Ticket implements Runnable {
  //共10票
  int ticket = 10;
  //创建Lock锁对象
  Lock ck = new ReentrantLock();
  @Override
  public void run() {
    //模拟卖票
    while(true){
      //synchronized (lock){
      ck.lock();
        if (ticket > 0) {
          //模拟选坐的操作
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
        }
      ck.unlock();
      //}
    }
  }
}


公平锁:谁等的时间最长的线程,优先获取锁


public class Ticket implements Runnable {
  //共10票
  int ticket = 10;
  //创建Lock锁对象
  Lock ck = new ReentrantLock(true);
  @Override
  public void run() {
    //模拟卖票
    while(true){
      //synchronized (lock){
      ck.lock();
        if (ticket > 0) {
          //模拟选坐的操作
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
        }
      ck.unlock();
      //}
    }
  }
}

等待唤醒机制


多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。


wait() 、notify() 、notifyAll()


wait(): 等待。使得当前的线程释放锁标记,进入等待队列。可以使当前的线程进入阻塞状态。


notify(): 唤醒,唤醒等待队列中的一个线程。


notifyAll(): 唤醒,唤醒等待队列中所有的线程。


wait和sleep的区别:


  • 两个方法都可以使一个线程进入阻塞。
  • 区别:wait方法会释放锁标记,sleep则不会释放锁标记。


package kaikeba;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadDemo{
    public static void main(String[] args) {
        ResourceUnit r = new ResourceUnit();
        Producer p1 = new Producer(r, 1);
        Consumer c1 = new Consumer(r, 1);
        p1.start();
        c1.start();
    }
}
class ResourceUnit {
    private int contents;
    private boolean available = false;
    public synchronized int get() {
        while (available == false) {
            try {
                wait();
            }
            catch (InterruptedException e) {
            }
        }
        available = false;
        notifyAll();
        return contents;
    }
    public synchronized void put(int value) {
        while (available == true) {
            try {
                wait();
            }
            catch (InterruptedException e) {
            }
        }
        contents = value;
        available = true;
        notifyAll();
    }
}
class Consumer extends Thread {
    private ResourceUnit resourceUnit;
    private int number;
    public Consumer(ResourceUnit r, int number) {
        resourceUnit = r;
        this.number = number;
    }
    public void run() {
        int value = 0;
        for (int i = 0; i < 10; i++) {
            value = resourceUnit.get();
            System.out.println("消费者 #" + this.number+ " got: " + value);
        }
    }
}
class Producer extends Thread {
    private ResourceUnit resourceUnit;
    private int number;
    public Producer(ResourceUnit r, int number) {
        resourceUnit = r;
        this.number = number;
    }
    public void run() {
        for (int i = 0; i < 10; i++) {
            resourceUnit.put(i);
            System.out.println("生产者 #" + this.number + " put: " + i);
            try {
                sleep((int)(Math.random() * 100));
            } catch (InterruptedException e) { }
        }
    }
}


线程死锁(了解)


在解决临界资源问题的时候,我们引入了一个"锁"的概念。我们可以用锁对一个资源进行保护。实际,在多线程的环境下,有可能会出现一种情况:


假设有A和B两个线程,其中线程A持有锁标记a,线程B持有锁标记b,而此时,线程A等待锁标记b的释放,线程B等待锁标记a的释放。这种情况,叫做死锁。


6.6 线程池


线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。


Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。


创建并使用线程池


通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。


Executors:线程池创建工厂类


public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象


ExecutorService:线程池类


Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行


Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用


 public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(index);
                        Thread.sleep(2000);
                        System.out.println(Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            });
        }
    }
目录
相关文章
|
6月前
|
存储 安全 Java
10分钟巩固多线程基础
10分钟巩固多线程基础
|
Java API 调度
并发编程系列教程(01) - 多线程基础
并发编程系列教程(01) - 多线程基础
69 0
|
6月前
|
Java API 调度
[Java并发基础]多进程编程
[Java并发基础]多进程编程
127 0
|
Java 程序员 调度
多线程(初阶)——多线程基础
多线程(初阶)——多线程基础
88 0
|
Java API 调度
并发编程之多线程基础
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
并发编程之多线程基础
|
Java 编译器 程序员
多线程基础(下)
多线程基础(下)
102 0
多线程基础(下)
|
安全 Java 编译器
多线程基础(上)
多线程基础(上)
67 0
多线程基础(上)
|
SpringCloudAlibaba 安全 前端开发
JUC系列(一) 多线程基础复习
问:如何学习JUC? 答: 源码 + Java帮助文档 面试高频, juc 其实就是 Java.util 包下的线程分类的工具
JUC系列(一) 多线程基础复习
|
安全 Java 调度
第8章 多线程基础
建立多线程基础,了解基本知识。
109 0
|
设计模式 缓存 安全
Java并发多线程基础总结
Java并发多线程基础总结
143 0
Java并发多线程基础总结