多线程(常用概念、创建线程的方式、线程的五种状态、线程同步、死锁、生产者/消费者模式)

一、常用概念

1.程序

Java源程序和字节码文件被称为“程序” ( Program ),是一个静态的概念

2.程序

执行中的程序叫做进程(Process),是一个动态的概念。为了使计算机程序得以运行,计算机需要加载代码,同时也要加载数据。

  • 进程是程序的一次动态执行过程, 占用特定的地址空间。
  • 每个进程由3部分组成:cpu,data,code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西。
  • 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占Cpu的使用权

3.进程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
程序是指令的集合,代码的集合 ; 而进程是动态的概念,当程序在执行时,系统分配进程 ; 多线程是在同一进程下,充分利用资源 ,多条执行路径,共享资源 (cpu data code) 。

二、创建线程

1.继承Thread

  1. 创建线程类: 继承 Thread类 +重写 run() 方法
  2. 构造线程类对象: 创建 子类的对象
  3. 启动线程: 通过子类对象调用 start() 方法
    创建 Thread 子类的一个实例并重写 run 方法, run 方法会在调用 start() 方法之后自动被执行
/*
1、创建一个线程类    继承 Thread + 重写 run方法
2、创建线程对象
3、调用线程对象的 start方法启动线程
 */
public class Demo001 {
    public static void main(String[] args) {
        //创建多线程对象
        MyThread thread=new MyThread();

        for (int i=0;i<100;i++){
            System.out.println(i);
        }
        thread.run();
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("子线程的方法");
    }
}

这种方式的特点:由于java中类只能单继承,因此继承Thread后就不能再继承其他类,由于父类的run方法中没有抛出异常,因此异常只能捕获

2.实现Runnable接口实现

  1. 创建实现 Runnable 接口的实现类 + 重写 run() 方法
  2. 创建一个实现类对象
  3. 利用实现类对象创建Thread类对象
  4. 启动线程
/*
通过实现Runnable接口创建线程
1、创建线程类    实现Runnable接口, 重写run方法
2、创建线程类对象  -> 提供真实的线程操作
3、创建Thread类对象,需要持有 线程对象的引用 -> 提供代理方法(启动线程的方法)
4、通过Thread对象启动线程
*/
public class Demo003 {
    public static void main(String[] args) {
        // 创建线程类对象
        MyThreadRunnable t = new MyThreadRunnable();
        // t提供线程体, Thread 提供启动
        Thread thread = new Thread(t);
        thread.start();
        for(int i=0; i<200; i++){
            System.out.println("。。。。");
        }
    }
}
class MyThreadRunnable implements Runnable{
    // 线程体方法
    @Override
    public void run() {
        System.out.println("---------------------");
    }
}

3. 实现Callable接口实现(了解)

  1. 创建实现 Callable 接口的实现类 + 重写 call() 方法
  2. 创建一个实现类对象
  3. 由 Callable 创建一个 FutureTask 对象
  4. 由 FutureTask 创建一个 Thread 对象
/*
 *通过Callable接口实现多线程
 * 1、创建线程类  实现Callable接口 + 重写 call方法
 * 2、创建线程类对象
 * 3、创建FutureTask对象
 * 4、创建Thread对象
 */
public class Demo002 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable=new MyCallable();
        // 将Callable包装成FutureTask,FutureTask也是一种Runnable
        FutureTask futureTask=new FutureTask(callable);
        // 将FutureTask包装成Thread
        new Thread(futureTask).start();
        System.out.println(futureTask.isDone());
        System.out.println(futureTask.get());
    }
}

class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i=0;i<100;i++){
            sum+=i;
        }
        return sum;
    }
}

4.通过线程池创建

三、线程的五种状态

在这里插入图片描述

  1. 新建状态 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  2. 就绪状态线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  3. 运行状态 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  4. 阻塞状态如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
    其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O处理完毕,线程重新转入就绪状态。
  5. 死亡状态 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

1.停止线程

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。

  1. 正常运行的线程完成了它的全部工作
  2. 线程被强制终止,如通过执行 stop 或 destroy 方法来终止一个线程。但是,不要调用 stop,destory 方法 ,太暴力,一盆冷水让其停止。
/**
 * 1、正常执行完毕,循环 次数已经到达
 * 2、外部干涉
 * 线程中加入标识 -->属性
 * 线程体中 使用改标识 -->死循环
 * 对外提供改变改标识的方法 setXxx() terminate() a()...
 * 外部根据适当的时机调用该方法
 */
public class ThreadStop {

    public static void main(String[] args) throws InterruptedException {
        MyThread mt=new MyThread();
        Thread t=new Thread(mt);
        t.start();
        Thread.sleep(2000);
        mt.setFlag(false);
        System.out.println("主线程方法");
    }

}
class MyThread extends Thread{

    private boolean flag=true;
    @Override
    public void run() {

        int i=0;
        while (flag){
            System.out.println("我是子线程"+i++);

            try {
                //暂停当前的线程,时间为100ms
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //设置线程运行状态的方法
    public void setFlag(boolean flg){
        this.flag=flg;
    }
}

2.阻塞状态

处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
有三种方法可以让我们暂停Thread执行:

  • sleep方法:sleep() 方法可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态。但是sleep() 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。
  • yield方法: yield() 方法和 sleep() 方法类似,也不会释放“锁标志”,区别在于yield()方法是“礼让”一次,即yield() 方法让当前线程在被调用时进入就绪状态,系统分配资源时会参与线程的竞争
  • join方法: 方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行
[1]sleep()
public class PauseThread {
    public static void main(String[] args) {
        MyThread02Runnable m2=new MyThread02Runnable();
        MyThread03Runnable m3=new MyThread03Runnable();
        new Thread(m2).start();
        new Thread(m3).start();
    }
}
class MyThread02Runnable implements Runnable{

    @Override
    public void run() {
        for(int i=0; i<10; i++){
            System.out.println("你打我一下");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyThread03Runnable implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<10; i++){
            System.out.println("我打你一下");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
[2]yield()
public class PauseThread2 {
    public static void main(String[] args) {
        MyThread04Runnable m4=new MyThread04Runnable();
        MyThread05Runnable m5=new MyThread05Runnable();
        new Thread(m4).start();
        new Thread(m5).start();
    }
}
class MyThread04Runnable implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<10; i++){
            System.out.println("你打我一下");
        }

    }
}
class MyThread05Runnable implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<10; i++){
            //当被调用时,礼让以下。进入到就绪状态,系统分配资源时会参与线程的竞争
            Thread.yield();
            System.out.println("我打你一下");
        }
    }
}
[3]join()
public class PauseThread3 {
    public static void main(String[] args) throws InterruptedException {
        MyThread06Runnable m6 = new MyThread06Runnable();
        Thread t=new Thread(m6);
        t.start();
        // 当主线程 输出前5条语句,前 5条我们都是公平,但是到5条以后,我希望 我自己的线程
        // 加入到主线程,要主线程等待我的线程执行结束后再执行
        for (int i=0;i<10;i++){
            System.out.println("main========");
            if (i==3){
                //谁调用加入到谁的线程中,执行完毕后会继续执行原线程
                t.join();
            }
        }
    }
}
class MyThread06Runnable implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<10; i++){
            System.out.println("我打你一下");
        }
    }
}

3.线程的基本信息

在这里插入图片描述
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照 线程的优先级 决定应调度哪个线程来执行。
线程的优先级用数字表示,范围从 1 到 10
一线程的默认优先级是5

Thread.MIN_PRIORITY = 1 
Thread.MAX_PRIORITY = 10 
Thread.NORM_PRIORITY = 5

使用下述线程方法获得或设置线程对象的优先级

void setPriority(int newPriority);
 void setPriority(int newPriority);

注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程

public class ThreadTest4 { 
    public static void main(String[] args) { 
        Thread t1 = new Thread(new Thread7(), "t1"); 
        Thread t2 = new Thread(new Thread7(), "t2"); 
        t1.setPriority(1); 
        t2.setPriority(10);
        t1.start(); 
        t2.start(); 
    } 
}
class Thread7 extends Thread { 
    public void run() {
    for (int i = 0; i < 20; i++) { 
        System.out.println(Thread.currentThread().getName() + ": " + i); // yield();
         } 
    } 
}

四、线程同步

1. 线程安全

在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就有可能造成数据的不准确。

public class TestSync {

    public static void main(String[] args) {
        Account a=new Account(100,"a");
        Drawing dra1=new Drawing(60,a);//你取钱
        Drawing dra2=new Drawing(70,a);//你家人取钱
        dra1.start();
        dra2.start();
    }
}

/**
 * 简单表示银行账户
 */
class Account{

    int monery;
    String name;

    public Account(int monery, String name) {
        this.monery = monery;
        this.name = name;
    }
}
/**
 * 简单表示取钱操作
 */
class Drawing extends Thread{

    int drawNum; //要取的钱数
    Account account;//要取钱的账户
    int expenseTotal;//总共取的钱数

    public Drawing(int drawNum, Account account) {
        super();
        this.drawNum = drawNum;
        this.account = account;
    }
    @Override
    public void run() {
        draw();
    }
      void draw() {
       
              if (account.monery - drawNum < 0) {
                  return;
              }

              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              account.monery = account.monery - drawNum;

              expenseTotal = expenseTotal + drawNum;
              System.out.println(this.getName() + " 共取了" + expenseTotal);
              System.out.println(this.getName() + " 账户余额" + account.monery);

          }
}

在这里插入图片描述
执行结果为两人都在账户中取到钱了,而且账户余额还剩40,但是我们代码定义的逻辑是余额不足则取不出钱。很显然,使用多线程但未处理共用资源时,线程是不安全的。

2. 线程同步 synchronized

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。“同”字从字面上容易理解为一起动作其实不是,“同”字应是指协同、协助、互相配合。
在Java里面,通过 synchronized 进行同步的保证。它包括两种用法:synchronized 方法和 synchronized 块

[1]synchronized 方法

通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个对象对应一把锁,每个synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得 该锁,重新进入可执行状态
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。

[2]synchronized 块
public class TestSync {

    public static void main(String[] args) {
		//创建账户对象
        Account a=new Account(100,"a");
        //创建两个线程对象
        Drawing dra1=new Drawing(60,a);//你取钱
        Drawing dra2=new Drawing(50,a);//你家人取钱
        dra1.start();
        dra2.start();
    }
}

/**
 * 简单表示银行账户
 */
class Account{

    int monery;
    String name;
    public Account(int monery, String name) {
        this.monery = monery;
        this.name = name;
    }
}

/**
 * 简单表示取钱操作
 */
class Drawing extends Thread{

    int drawNum; //要取的钱数
    Account account;//要取钱的账户
    int expenseTotal;//总共取的钱数


    public Drawing(int drawNum, Account account) {
        super();
        this.drawNum = drawNum;
        this.account = account;
    }

    @Override
    public void run() {
        draw();
    }
		//取钱的方法
      void draw() {
      		//如果账户余额小于要取的钱数,则结束方法	(双重检测来提高效率)
          if (account.monery - drawNum < 0) {
              return;
          }
          //锁住账户对象
          synchronized (account) {
          		//若余额比将取出的钱少则返回(逻辑判断)
              if (account.monery - drawNum < 0) {
                  return;
              }
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              //取钱,账户扣款
              account.monery = account.monery - drawNum;
              //取出的钱数
              expenseTotal = expenseTotal + drawNum;
              System.out.println(this.getName() + " 共取了" + expenseTotal);
              System.out.println(this.getName() + " 账户余额" + account.monery);

          }
      }
}

在这里插入图片描述
使用了synchronized块后,第一个人取完钱后余额还剩40,第二个人取50发现钱不够,就不再进行取款。此时线程变得安全了。
上面这种方式叫做:互斥锁原理。利用互斥锁解决临界资源问题。

五、死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
下面举个小例子
如:A去B公司面试:
A:你把死锁说清楚我就让你通过面试
B: 你让我过通过面试我就把死锁给你说清楚

/**
 * 测试死锁
 */
public class ThreadDeadLock {
    public static void main(String[] args) {
        Makeup m1=new Makeup(true,"张三");
        Makeup m2=new Makeup(false,"李四");
        m1.start();
        m2.start();
    }
}
/**
 * 口红
 */
class Lipstick{
}

/**
 * 镜子
 */
class Mirror{
}

class Makeup extends Thread{
    //确定获取资源的顺序
    boolean flag;
    //名称
    String girl;
    // 静态属性是所有对象共享
    static Lipstick lipstick=new Lipstick();
    static Mirror mirror= new Mirror();
    // 创建对象的时候, 就确定先获取什么资源
    public Makeup(boolean flag,String girl) {
        this.flag = flag;
        this.girl = girl;
    }
    @Override
    public void run() {
        doMakeUp();
    }
    void doMakeUp(){
        if (flag){
            synchronized (lipstick){
                System.out.println(girl+" 拿着口红");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror){
                    System.out.println(girl+ "拿着镜子");
                }
            }
        }else {
            synchronized (mirror){
                System.out.println(girl+" 拿着镜子");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick){
                    System.out.println(girl+"拿着口红");
                }
            }
        }
    }
}

在这里插入图片描述
如果不强行停止程序,就会一直处于阻塞状态。张三一直拿着镜子,但是需要拿口红才能完成化妆,但是口红在李四那,而李四需要拿着镜子才能化妆。他们谁都不相让,因此程序一直阻塞。
如何解决死锁问题:

  1. 往往是程序逻辑的问题。需要修改程序逻辑。
  2. 尽量不要同时持有两个对象锁。

六、生产者/消费者模式

在常见的多线程问题解决中,同步问题的典型示例是“生产者-消费者”模型,也就是生产者线程只负责生产,消费者线程只负责消费,在消费者发现无内容可消费时则调用则唤醒生产者,自身则调用wait()进入阻塞状态。
而生产者发现生产量到达一定程度之后,则唤醒消费者,自身进入休眠状态。

/**
 * 生产者,消费者
 */
public class Synchronized {
    public static void main(String[] args) {
        //有生产、消费功能的工厂
        SyncStack stack=new SyncStack();
        //生产馒头的工厂类
        ShengChan sc=new ShengChan(stack);
        //有消费功能的工厂
        Chi chi=new Chi(stack);
        sc.start();
        chi.start();
    }
}
/**
 * 商品类
 */
class Mantou{

}

/**
 * 有生产、消费功能的工厂
 */
class SyncStack{
    List<Mantou> list=new ArrayList<>();//盛馒头的容器。10个就满了

    //生产馒头的方法
    public synchronized void push(Mantou mantou){

        //若list已经装了10个元素,则认为已经放满了
        if (list.size()==10){
            try {
                //唤醒消费者的线程,通知来买馒头
                this.notify();
                //自身进入阻塞状态,等待被唤醒
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            list.add(mantou);
            System.out.println("生产第"+list.size()+"个馒头");
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 吃馒头的方法
     */
    public synchronized void pop(){
        //如果吃完了
        if (list.size()==0){
            try {
                //唤醒生产者
                this.notify();
                //自身进入睡眠
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            //如果还有馒头,则移除list中最上面的元素
            list.remove(list.size()-1);
            System.out.println("还剩"+list.size()+"个馒头");
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//生产馒头的线程
class ShengChan extends Thread{
    SyncStack stack;
   public ShengChan(SyncStack stack) {
        this.stack = stack;
    }
    @Override
    public void run() {
        while (true){
            //一直生产馒头
            Mantou mantou=new Mantou();
            this.stack.push(mantou);
        }
    }
}
//消费馒头的线程
class Chi extends Thread{
    SyncStack stack;

    public Chi(SyncStack stack) {
        this.stack = stack;
    }
    @Override
    public void run() {
       while (true){
           //一直吃馒头
           this.stack.pop();
       }
    }
}

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值