Java 基础篇:多线程基本概念


前言

多线程技术概述: 线程与进程

进程:

  • 是指一个内存中运行的应用程序,每个程序都有一个独立的内存空间

线程:

  • 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。


一 、基本概念

1.1 程序 - 进程 - 线程

  1. 程序(Program) 是为完成 特定的任务、 用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  2. 进程(Progress)是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。

如:

  • 运行中的QQ,运行中的MP3播放器;
  • 程序是静态的,进程是动态的;

线程(thread):

进程可进一步细化为线程,是一个程序内部的一条执行路径。

若一个程序可同一时间执行多个线程,就是支持多线程的。

Q:何时需要多线程?

A:

程序需要同时执行两个或多个任务;

程序需要实现一些需要等待的任务时,如用户输入 、文件读写操作、网络操作、搜索等;

需要一些后台运行的程序时。

1.2 线程调度

分时调度:

  • 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度:

  • 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么随机选择一个(线程随机性),Java 使用的为抢占式调度。
  • CPU 使用抢占式调度模式在多个线程间进行着高速的切换。对于 CPU 的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程出现并不能提高程序的运行速度,但能够提高程序运行效率,让 CPU 的使用率更高。

1.3 同步与异步

同步:

排队执行,效率低但是安全。

异步:

同时执行,效率高但是数据不安全。

1.4 并发与并行

并发:

指的是两个或多个事件在 同一时间段内 发生。

并行:

指的是两个或多个事件在 同一时刻 发生(同时发生)。

二、多线程的创建和启动

Java 语言的 JVM 允许程序运行多个线程,它通过 java.lang.Thread 类来实现。

Thread 类的特性:

  1. 每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为 线程体
  2. 通过该 Thread 对象的 start() 方法来调用这个线程。

2.1 Thread 类

构造方法

  1. Thread():创建新的 Thread 对象;
  2. Thread(String threadname): 创建线程并指定线程实例名;
  3. Thread(Runable target):指定创建线程的目标对象,它实现了 Runable 接口中的 run 方法;
  4. Thread(Runable target, String name):创建新的 Thread 对象。

2.2 创建线程的两种方式

1、继承 Thread类

  1. 定义子类继承 Thread 类。
  2. 子类中重写 Thread类中的 run 方法。
  3. 创建 Thread 子类对象,即创建了线程对象。
  4. 调用线程对象 start 方法:启动线程,调用 run 方法。

2、实现 Runnable 接口

  1. 定义子类,实现 Runnale 接口。
  2. 子类中重写 Runnable 接口中的 run 方法。
  3. 通过 Thread 类含参构造器创建线程对象。
  4. 将 Runnale 接口的子类对象作为实际 参数传递给 Thread类的构造方法中。
  5. 调用 Thread 类的 start 方法:开启线程,调用 Runnable 子类接口的 run 方法。

3、Runnale 与 Callable

接口定义
//Callable 接口

public interface Callable<V>{
	V call() throws Exception;
}

//Runnable 接口

public interface Runnable{
	public abstrast void run();
}

4、Callable 使用步骤

 - 编写类实现 Callable 接口,实现 call 方法
	class xxx implements Callable<T>{
		@Override
		public <T> call() throws Exception{
			return T;
	}
}
 - 创建 FutureTask 对象,并传入第一步编写的 Callable 类对象
	FutureTask<Integer> future = new FutureTask<>(Callable)
 - 通过 Thread,启动线程
	new Thread(future).start();

5、Runnable 与 Callable 的相同点

  • 都是接口
  • 都可以编写多线程程序
  • 都采用 Thread.start() 启动线程

Runnable 与 Callable 的不同点

  • Runnable 没有返回值;Callable 可以返回执行结果
  • Callable 接口的 call() 允许抛出异常;Runnable 的 run() 不能抛出

6、Callable 获取返回值

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

例(源码):

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * java中的第三种线程执行方式:Callable
 */
public class D6_3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> c = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(c);
        //task.isDone();//判断子线程是否执行完毕
        //task.cancel(true);//取消线程执行
        new Thread(task).start();
        Integer j = task.get();
        System.out.println("返回值为:"+j);
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }

    static class MyCallable implements Callable<Integer>{

        @Override
        public Integer call() throws Exception {
            //Thread.sleep(3000);
            for (int i=0;i<10;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
            return 100;
        }
    }
}

运行结果:
在这里插入图片描述


【注意】 继承方式和实现方式的联系与区别

public class Thread extends Object implements Runnable

区别:

  1. 继承 Thread:线程代码存放 Thread 子类 run 方法中,重写 run 方法。
  2. 实现 Runnable:线程代码存在接口的子类的 run 方法,实现 run 方法。

实现接口方式的好处:

  1. 避免了单继承的局限性;
  2. 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

一般使用实现接口方式来实现多线程。

2.3 多线程的优点

多线程程序的优点

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统 CPU的利用率。
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

2.4 线程的优先级

线程的优先级控制:

  • MAX_PRIORITY(10):
  • MIN_PRIORITY(1):
  • NORM_PRIORITY(5):

涉及的方法:

getPriority():返回线程优先值

setPriority(int newPriority):改变线程的优先级

线程创建时继承父线程的优先级

2.5 Thread 类的有关方法(1)

void start():启动线程,并执行对象的 run() 方法

run():线程在被调度时执行的操作

String getName():返回线程的名称

void setName(String name):设置该线程名称

static currentThread():返回当前线程

2.6 Thread 类的有关方法(2)

static void yield(): 线程让步

暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程

若队列中没有同优先级的线程,忽略此方法

join(): 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。

低优先级的线程也可以获得执行

static void sleep(long mills):(指定时间:毫秒)

令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重排队。

抛出 InterruptedException 异常

stop(): 强制线程生命期结束

boolean isAlive(): 返回 blooean,判断线程是否还活着。

三、线程的生命周期

3.1 JDK 中用 Thread State 枚举表示了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象,Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态

  • 新建:当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
  • 就绪:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件;
  • 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run() 方法定义了线程的操作和功能;
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态;
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止。

3.2 图解

在这里插入图片描述

四、线程的同步

4.1 问题的提出

  1. 多个线程执行的不确定性引起执行结果不稳定;
  2. 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。

解释如下图:
在这里插入图片描述

4.2 多线程出现了安全问题

问题的原因:

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

解决办法:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

Synchronized 的使用方法

Java 对于多线程的安全问题提供了专业的解决方式(同步机制):

1、synchronized 还可以放在方法声明中,表示整个方法为同步方法。

例:

public synchronnized void show(String name){
	...
}
2、synchroized(对象){
	//需要被同步的代码:
	
}

源码:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class D6 {
    /**
     * 同步代码块 和 同步方法 都属于隐式锁
     * 线程同步:synchronized
     * @param args
     */
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全和安全问题
        //解决方案3、显示锁 Lock 子类 ReentrantLock
        //格式: synchronized(锁对象){
        //
        //      }
        Runnable run = new Ticket();
        //三个线程启动
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
        /*new Thread(new Ticket()).start();
        new Thread(new Ticket()).start();
        new Thread(new Ticket()).start();*/
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        //显式锁 l : fair参数为true 就表示是公平锁
        private Lock l = new ReentrantLock(true);
        //private Object o = new Object();
        @Override
        public void run() {//A  B  C
            while (true) {
                l.lock();
                if (count > 0) {
                    //卖票
                    System.out.println("正在准备卖票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                }else{
                    break;
                }
                l.unlock();
            }
        }
    }
}

运行结果:
在这里插入图片描述

线程的死锁问题

死锁:

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
  • 使用专门的算法、原则,比如加锁顺序一致;
  • 尽量减少同步资源的定义,尽量避免锁未释放的场景;

例(源码):

public class D6_1 {
    public static void main(String[] args) {
        //线程死锁
        Culprit c = new Culprit();
        Police p = new Police();
        new MyThread(c,p).start();
        c.say(p);
    }
    static class MyThread extends Thread{
        private Culprit c;
        private Police p;
        public MyThread(Culprit c, Police p){
            this.c = c;
            this.p = p;
        }

        @Override
        public void run() {
            p.say(c);

        }
    }
    //罪犯
    static class Culprit{
        public synchronized void say(Police p){
            System.out.println("罪犯:你放了我,我放人质");
            p.fun();

        }
        public synchronized void fun(){
            System.out.println("罪犯被放走了,罪犯也放了人质");
        }
    }

    //警察
    static class Police{
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人质,我放过你");
            c.fun();
        }
        public synchronized void fun(){
            System.out.println("警察救了人质,但是罪犯跑了");
        }
    }
}

运行结果:
在这里插入图片描述

五、线程通信

5.1 wait() 与 notify() 和 notefyAll()

  • wait():令当前线程挂起并放弃 CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等待再次对资源的访问;
  • notify():唤醒正在排队等待同步资源的线程中优先级最高者;
  • notifyAll():唤醒正在排队等待资源的所有线程结束等待,Java.lang.Object 提供的这三个方法只有在 synchronized 方法或 Java.lang.illegalMonitorStateExcception异常

wait() 方法

在当前线程中调用方法:对象名.wait()

使当前线程进入等待(某对象)状态,直到另一线程对该对象发出 notify(或 notifyAll)为止。

调用方法的必要条件:当前线程必须具有对象的监控权(加锁)

在当前线程被 notify 后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll() 方法

在当前线程中调用方法:对象名.notify()

功能:唤醒等待该对象监控权的一个线程。

调用方法的必要方法:当前线程必须具有该对象的监控权(加锁)

5.2 经典例题:生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如 20个产品),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

这里可能出现两个问题:

生产者比消费者快时,消费者会漏掉一些数据没有取到。
消费者比生产者快时,消费者会取相同的数据。

例(源码解释):

public class D6_2 {
    /**
     * 多线程通信问题,生产者与消费者问题
     * @param args
     */
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }

    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if (i%2==0){
                    f.setNameAndTaste("老干妈小米粥","香辣味");
                }else {
                    f.setNameAndTaste("煎饼果子","甜辣味");
                }
            }
        }
    }

    //服务生
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }

    //食物
    static class Food extends Thread{
        private String name;
        private String taste;

        private boolean flag = true;
        //true 表示可以生产

        public synchronized void setNameAndTaste(String name,String taste){
            if(flag){
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get(){
            if (!flag){
                System.out.println("服务员端走的菜的名称是:"+name+",味道:"+taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果:
在这里插入图片描述

六、线程池 Executors

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间,线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

6.1 线程池的好处

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的课管理性

6.2 Java 中的四种线程池 ExecutorService

1、缓存线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class D6_4 {
    /**
     * 缓存线程池
     * (长度无限制)
     * 任务加入后的执行流程:
     *      1、判断线程池是否存在空闲线程
     *      2、存在则使用
     *      3、不存在,则创建线程,并放入线程池,然后使用
     * @param args
     */
    public static void main(String[] args) {
        //创建线程
        //创建任务
        //执行任务
        //关闭线程
        ExecutorService service = Executors.newCachedThreadPool();
        //指挥线程池向线程池中加入新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
    }
}

2、定长线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class D6_5 {
    /**
     * 定长线程池
     * (长度是指定的数值)
     * 任务加入后的执行流程
     *      1、判断线程是否存在空闲线程
     *      2、存在则使用
     *      3、不存在空闲线程,且线程池未满的情况下则创建线程并放入线程池然后使用
     *      4、不存在空闲线程,且线程池已满的情况下则等待线程池存在空闲然后使用
     * @param args
     */
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
    }
}

3、单线程线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class D6_6 {
    /**
     * 单线程线程池
     * 执行流程:
     *          1、判断线程池的那个线程是否空闲
     *          2、空闲则使用
     *          3、不空闲,则等待,池中的单个线程空闲后 使用
     * @param args
     */
    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
            }
        });
    }
}

4、周期性任务定长线程池

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class D6_7 {
    /**
     * 周期任务:定长线程池
     * 执行流程:
     *          1、判断线程池是否存在空闲线程
     *          2、存在则使用
     *          3、不存在空闲线程,且线程池未满的情况下则创建线程并放入线程池然后使用
     *          4、不存在空闲线程,且线程池已满的情况下则等待线程池存在空闲然后使用
     *
     * 周期性任务执行时:
     *      定时执行,当某个时机触发时,自动执行某任务
     * @param args
     */
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
         *1、定时执行一次
         * 参数1、定时执行的任务
         * 参数2、时长数字
         * 参数3、 时长数字的时间单位,TimeUnit的常量指定
         *
         */

        /*service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("锄禾日耽误");
            }
        },5, TimeUnit.SECONDS);*/
        /**
         * 周期性执行任务
         * 参数1、任务
         * 参数2、延迟时长数字(第一次执行在什么时间以后)
         * 参数3、周期时长数字(每隔多久执行一次)
         * 参数4、时长数字的单位
         */
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("旱地和下入");
            }
        },5,1,TimeUnit.SECONDS);
    }
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值