05-Java的多线程

    科技2022-07-17  103

    1、基本概念:程序、进程、线程

    程序(program)

    是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

    进程(process)

    是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程: 有它自身的产生、存在和消亡的过程。——生命周期

    如: 运行中的QQ,运行中的MP3播放器程序是静态的,进程是动态的进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

    线程(thread)

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

    若一个进程同一时间并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

    单核CPU与多核CPU

    单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。涉及到CPU处理线程的方式,CPU在单位时间(也就是说一个时间片内)内只能处理一个线程,于是就将其他的线程设置为阻塞状态,加入到阻塞队列中,等到处理完成当前线程后从就绪队列中取出新的线程进行处理,由于切换和处理时间很快用户感知不到于是用户便认为CPU在同一时间内处理多个线程。多核CPU,才能更好的发挥多线程的效率。(现在的服务器都是多核的)一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

    并行与并发

    并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

    并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事

    1.1、多线程的优点

    背景: 以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

    多线程的优点

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

    应用的场景

    程序需要同时执行两个或多个任务。程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等需要一些后台运行的程序时

    2、Thread类

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

    2.1、Thread类的特性

    每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

    2.2、多线程的创建,方式一: 继承与Thread类

    1.创建一个继承于Thread类的子类

    2.重写Thread类的run()方法

    3.创建Thread类的子类的对象

    4.通过此对象调用start()

    start()方法两个作用

    启动当前线程调用当前线程的run()方法

    注意: 一个对象只能启动一个线程,要想在启动新的线程需要重新创建对象启动线程。

    例子 Thread t1 = new Thread(); t1.start(); Thread t2 = new Thread(); T2.start();

    常用的Thread类中的方法 Thread.currentThread().getName(); 获取当前线程的名称

    public class ThreadTest { public static void main(String[] args) { //3.创建Thread类的子类的对象 Machine machine = new Machine(); //4.通过此对象调用start() machine.start(); for (int i=0; i<200; i++){ System.out.println(i + "******main主线程******"); } } } //1.创建一个继承于Thread类的子类 class Machine extends Thread{ //2.重写Thread类的run()方法 public void run(){ for (int i=0; i<200; i++){ System.out.println(i + "******线程一******"); } } }

    2.3、多线程的创建,方式二:实现Runnable接口

    1.创建一个实现了Runnable接口的类

    2.实现类去实现Runnable中的抽象方法: run()

    3.创建实现类的对象

    4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

    5.通过Thread类的对象调用start()

    demo

    public class RunnableTest { public static void main(String[] args) { //3.创建实现类的对象 Window window = new Window(); //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread thread = new Thread(window); //5.通过Thread类的对象调用start() thread.start(); } } //1.创建一个实现了Runnable接口的类 class Window implements Runnable{ @Override //2.实现类去实现Runnable中的抽象方法: run() public void run() { System.out.println("我是线程"); } }

    比较创建线程的两种方式

    开发中: 优先选择: 实现Runnable接口的方式

    原因: 1. 实现的方式没有类的单继承性的局限性

    ​ 2.实现的方式更适合来处理多个线程有共享数据的情况。

    联系: public class Thread implements Runnable

    相同点: 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中

    2.4、Thread类的常用方法

    start(): 启动当前线程,调用当前线程的run()方法run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中currentThread(): 静态方法,返回执行当前代码的线程getName(): 获取当前线程的名字setName(): 设置当前线程的名字yield(): 释放当前cpu的执行权。例如t1释放了执行权,然后t1和t2在重新抢夺这个线程的执行权。join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。sleep(long millitime): 让当前线程" 睡眠 "指定的毫秒。在指定的毫秒时间内,当前线程是阻塞状态。isAlive(): 判断当前线程是否存活

    2.5、线程的优先级

    线程的优先级等级

    MAX_PRIORITY: 10

    MIN_PRIORITY: 1

    NORM_PRIORITY: 5 --------------->默认优先级

    方法

    getPriority(): 返回线程的优先值

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

    说明

    线程创建时继承父线程的优先级高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被中,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

    demo

    public class ThreadTest2 { public static void main(String[] args) { Test test = new Test(); test.setPriority(10); test.start(); System.out.println(test.getPriority()); } }

    3、线程的生命周期

    JDK中用Thread.State类定义了线程的几种状态

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

    新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。

    例如: Thread t1 = new Thread()

    就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源

    例如: t1.start()

    运行: 当就绪的线程被调度并获得CPU资源时,便进入了运行状态,run()方法定义了线程的操作和功能

    阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态

    死亡: 线程完成了它的全部工作或线程被提前强制性终止或出现异常导致结束

    4、synchronized的三种使用方式

    背景

    例子:创建个窗口卖票,总票数为100张.使用实现Runnable接口的方式

    问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。在Java中,我们通过同步机制,来解决线程的安全问题

    4.1、方式一: 同步代码块

    synchronized(同步监视器){ //需要被同步的代码 }

    说明:

    操作共享数据的代码,即为需要被同步的代码

    共享数据: 多个线程共同操作的变量。

    同步监视器,俗称: 锁。任何一个类的对象,可以充当锁

    要求: 多个线程必须要共用同一把锁。例如 Dog dog = new Dog;synchronized(dog)

    在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

    在继承Thread类创建多线程的方式中,考虑使用当前类充当同步监视器。例如 window.class。

    总结:

    实现Runnable接口使用方式一 ,同步监视器可以写四种方式 第一种 放入任意个对象 Dog dog = new Dog;synchronized(dog)第二种 放入静态对象 static Object obj = new Object(); synchronized(obj)第三种 使用当前类;类名.class第四种 使用this充当同步监视器。 继承Thread类使用方式一,同步监视器可以写两种方式 第一种 放入静态对象 static Object obj = new Object(); synchronized(obj)第二种 使用当前类;类名.class //使用Runable接口来解决线程安全问题 public class RunnableTest2 { public static void main(String[] args) { Window2 window2 = new Window2(); Thread t1 = new Thread(window2); Thread t2 = new Thread(window2); Thread t3 = new Thread(window2); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } } class Window2 implements Runnable{ int ticket = 100; Object object = new Object(); @Override public void run() { while (true) { synchronized(object) { if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + ticket); ticket--; } } } } }

    4.2、同步方法

    说明:

    如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

    public synchronized void show(){ .... } //使用同步方法解决实现Runnable接口的线程安全问题 public class RunnableTest3 { public static void main(String[] args) { Window3 window3 = new Window3(); Thread t1 = new Thread(window3); Thread t2 = new Thread(window3); Thread t3 = new Thread(window3); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } } class Window3 implements Runnable{ int ticket = 100; @Override public void run() { while (true){ show(); } } public synchronized void show(){ if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + ticket); ticket--; } } } //使用同步方法解决继承Thread类的线程安全问题 public class ThreadTest3 { public static void main(String[] args) { Window4 w1 = new Window4(); Window4 w2 = new Window4(); Window4 w3 = new Window4(); w1.setName("窗口一"); w2.setName("窗口二"); w3.setName("窗口三"); w1.start(); w2.start(); w3.start(); } } class Window4 extends Thread{ static int ticket = 100; @Override public void run() { while (true){ show(); } } public static synchronized void show(){ //同步监视器 window4.class if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + ticket); ticket--; } } }

    同步方法的总结:

    同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。非静态的同步方法,同步监视器是: this静态的同步方法,同步监视器是: 当前类本身

    4.3、同步的优缺点

    同步的方式,解决了线程的安全问题---->好处

    操作的同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低 —>局限性

    4.4、方式三: Lock锁 —JDK5.0新增

    从JDK 5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock类实现了Lock,它拥有与 synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 Reentrantlock,可以显式加锁、释放锁。 //语法 class A implements Runnable{ private ReentrantLock lock = new ReentrantLock(); public void run(){ try{ lock.lock();//锁住 //同步代码块 }finally{ lock.unlock();//解锁 } } } //demo public class RunnableTest4 { public static void main(String[] args) { Window5 window5 = new Window5(); Thread t1 = new Thread(window5); Thread t2 = new Thread(window5); Thread t3 = new Thread(window5); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } } class Window5 implements Runnable{ int ticket = 100; //1.实例化ReentrantLock对象 private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true){ try{ //2.调用锁定方法lock() lock.lock(); if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + ticket); ticket--; } }finally { //3.调用解锁方法unlock() lock.unlock(); } } } }

    5、线程的通信

    wait(): 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

    notify(): 一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。

    notifyAll(): 一旦执行此方法,就会唤醒所有被wait的线程。

    说明:

    wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。

    wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。

    否则,会出现IllegalMonitorStateException异常

    wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

    demo:

    使用两个线程打印 1-100,线程1, 线程2 交替打印。

    public class RunnableTest5 { public static void main(String[] args) { Number number = new Number(); Thread t1 = new Thread(number); Thread t2 = new Thread(number); t1.setName("线程一"); t2.setName("线程二"); t1.start(); t2.start(); } } class Number implements Runnable{ private int number = 100; @Override public void run() { while (true){ show(); } } public synchronized void show(){ notify(); if (number>0){ System.out.println(Thread.currentThread().getName() + ":" + number); number--; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }

    6、JDK5.0新增线程创建方式

    6.1、多线程的创建方式三: 实现callable接口

    创建一个实现Callable的实现类实现call方法,将此线程需要执行的操作声明在call()中创建Callable接口实现类的对象将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。通过FutureTask对象的引用获取Callable中call方法的返回值内容(可写可不写) //demo public class CallableTest { public static void main(String[] args) { Window6 window6 = new Window6(); FutureTask futureTask = new FutureTask(window6); Thread thread = new Thread(futureTask); thread.start(); try { Object object = futureTask.get(); System.out.println(object); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class Window6 implements Callable{ int temp=0; @Override public Object call() throws Exception { for (int i=0; i<=100; i++){ temp+=i; } return temp; } }

    如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

    call()可以有返回值的。call()可以抛出异常,被外面的操作捕获,获取异常的信息。Callable是支持泛型的。

    6.2、多线程的创建方式方式四: 使用线程池

    背景

    经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响大。

    解决思路

    提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

    好处

    提高响应速度(减少了创建新线程的时间)降低资源消耗(重复利用线程池中线程,不需要每次都创建)便于线程管理

    实现方法

    提供指定线程数量的线程池执行指定的线程的操作。需要提供Runnable接口或Callable接口实现类的对象关闭连接池

    相关API

    JDK 5.0起提供了线程池相关AP|: Executor Service和 Executors Executor Service:真正的线程池接口。常见子类 Thread Poolexecutor void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable <T> Future<T> submit(Callable<T>task):执行任务,有返回值,一般又来执行Callable void shutdown():关闭连接池 Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池 Executors. newCachedThreadPool():创建一个可根据需要创建新线程的线程池 Executors.newFixedthreadPool(n);创建一个可重用固定线程数的线程池 EXecutors. newSingleThreadEXecutor():创建一个只有一个线程的线程池 Executors. new thread Poo(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 //demo public class Test10 { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(10); service.execute(new Window10()); service.shutdown(); } } class Window10 implements Runnable{ int temp=0; @Override public void run() { for (int i=0; i<100; i++){ temp += i; System.out.println(temp); } } }
    Processed: 0.010, SQL: 10