Java教程

6、多线程

本文主要是介绍6、多线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

6、多线程

6.1 基本概念

程序,进程,线程

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

  • 进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
     如:运行中的QQ,运行中的MP3播放器
     程序是静态的,进程是动态
    进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

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

image-20220326141126912


  • 一个Java应用程序java.exe 至少有三个线程main()主线程,gc() 垃圾回收线程,异常处理线程;

  • 并行与并发:

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


对于多线程:

  • 以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法) 反而比用多个线程来完成用的时间更短。 ---> 线程之间的切换需要时间

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

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

6.2 线程的创建和使用*

多线程的创建

方式一:继承Thread类

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

  2. 重写Thread类的run( ) --> 将此线程执行的操作声明在run( )中

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

  4. 通过此对象调用start( )---> a.启动当前线程 b.调用当前线程的run( )

    //1.创建一个继承于Thread类的类
    class MyThread extends Thread{
        //2.重写Thread类的run()
        pbulic void run(){ sout("这个线程要执行的操作"); }
    }
    //main--主线程
    public void main(){
        //3.创建Thread类的子类的对象
        MyThread t1 = new MyThread();
        //4.通过此对象调用start()
        t1.start();
                                                                 
        new Thread(){
            public void run(){ sout("多线程--Thread匿名子类"); }
        }.start();
    }
    

注意:

  • 要想启动多线程,必须调用start( ),调用run()并不会启动多线程
  • 一个线程对象只能调用一次start()方法启动
image-20220326150906791

方式二:实现Runnable接口

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

  2. 实现类实现接口中的 抽象方法:run( )

  3. 创建实现类的对象

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

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

    //1.创建一个实现了 Runnable接口的类,并实现run()方法
    class MyThread implements Runnable{
        pbulic void run(){ sout("此线程要执行的操作") };
    }
                                                             
    public void main(){
        //2.创建实现类对象
        MyThread w = new MyThread(); 
        //3.将实现类对象作为参数 传递到Thread类的构造器中,创建Thread类对象
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        //4.通过Thread类对象 掉哟start()
        t1.start();
        t2.start();
    }
    

小结

开发中,优先选择 实现Runnable接口的方式:
 没有单继承的局限性
多个线程可以共享实现类中的数据

联系:public class Thread implements Runnable

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

线程方法

Thread中的常用方法

start( ) : 启动当前线程,调用当前线程的run( )

run( ) : 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在run( )中

currentThread( ):静态方法,返回当前执行代码的线程

getName( ):获取当前线程的名字

setName( ):设置当前线程的名字 【也可以在 构造器中设置线程名字 new MyThread("线程1")

yield( ):释放当前cpu的执行权

join( ):在线程a中调用线程b的join( ),此时线程a进入阻塞,直到b执行完毕。

stop( ):强制结束当前线程 (已过时)

sleep(long millitime):让当前线程“睡眠”指定 毫秒,睡眠期间此线程 阻塞。

isAlive( ):判断当前线程是否存活。

线程的优先级

MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY:5 -->默认优先级

getPriority( ):获取当前线程优先级

setPriority( ):设置线程的优先级

--- 优先级只是提高被cpu执行的概率

线程通信

wait( ),notify( ),notifyAll( ):此三个方法定义在Object类中

线程的分类

Java中的线程分为两类:守护线程用户线程

虚拟机JVM 必须确保 用户线程执行完毕才能离开 ,若JVM中都是守护线程,当前JVM退出。用户线程完毕--JVM关闭--守护线程完毕

守护线程是用来服务用户线程的,通过在start( )前调用thread.setDaemon(true)可以设置为守护线程

守护线程: 后台记录操作日志,监控内存,垃圾回收等待

6.3 线程的生命周期

  • JDK 中用Thread.State 类定义了 线程的5种状态
  • Thread.State (返回枚举类型值) : ( 新建NEW,就绪WAITING,运行RUNNABLE,阻塞BLOCKED,死亡TERMINATED
image-20220326170449209

6.4 线程同步*

 线程同步的问题:当线程a操作某共享数据的过程中(还没操作完),进程b参与进来也操作该共享数据,就会造成共享数据的错误。

 解决: 当一个线程a在操作共享数据时,其他线程不能参与进来,直到线程a操作完毕。

 问题:同步后,该同步片段变为 单线程;同步片段过多可能使程序出现错误 --被某一线程全部占用。

synchronized

方式一:同步代码块

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

说明:a.操作共享数据的代码,即为需要被同步的代码。

​ b.共享数据:多个线程共同操作的变量

​ c.同步监视器,俗称 任何一个类的对象 都可以充当锁
​ 要求:多个线程必须要共用同一把锁。--Runnable方式中可以用 this
​ --Thread方式中可以用 xx.class【类对象】

方式二:同步方法

若操作共享数据的代码完整的声明在一个方法中,可以考虑使用同步方法

public synchronized void method(){
    //被同步的代码(实现Runnabe方式)-- this
}
public static synchronized void method(){
    //继承Thread方式-- 类.class
}

总结:同步方法仍然涉及到同步监视器,只是不再需要显示声明。(默认)
​  非静态的同步方法,同步监视器是:this
​  静态的同步方法,同步监视器是:当前类本身

Lock锁

方式三:Lock锁

JDK5.0新增,手动上锁,解锁

//1.实例化ReentrantLock
privete ReentrantLock lock = new ReentrantLock(); //ReentrantLock(true)-公平锁
//2.调用上锁方法 lock()
lock.lock();
 ---------------------要同步的代码片段
//3.调用解锁方法 unlock()
lock.unlock();

synchronized 与 Lock 的对比

面试题:synchronized 与 Lock的异同

  • 相同:二者都可以解决线程安全问题

  • 不同:synchronized机制在执行完相应的代码片段后,自动释放同步监视器

    ​ Lock需要手动的启动同步lock(),手动的结束同步 unlock()

优先顺序:Lock---> 同步代码块 ---> 同步方法

线程死锁

死锁
 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

解决方法
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步

6.5 线程通信

synchronized(this){
    notify();  //线程a 释放(一个)被阻塞的线程b
    /*
    同步代码块
    */
    wait();   //当前线程(a)进入阻塞,并释放同步监视器 --- 交替到线程b
}

线程通信涉及到的三个方法:
 wait( ): 当前线程进入阻塞状态,并释放同步监视器
 notify( ): 唤醒被wait的一个线程 (若有多个线程被wait 则唤醒优先级高的那个)
 notifyAll( ):唤醒所有被wait的线程

说明(对于wait, notify, notifyAll三个方法):
 必须在 同步代码块或同步方法中使用
调用者必须是同步代码块或同步方法中的 同步监视器
 都是 定义在java.lang.Object类中的。

面试题:sleep( ) 和 wait( ) 的异同?

相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态

不同点: 1.两个方法声明的位置不同:Thread类中声明sleep()Object类中声明wait()
2.调用的要求不同:sleep()可以在任何场景下调用,wait()必须使用在同步代码块/同步方法中
3.是否释放锁:若两个方法都在同步代码块/同步方法中,sleep()不会释放锁,wait()会释放锁。

生产者消费者模式:

  • 管程法 (生产者---缓冲区---消费者)

  • 信号灯法 (标志位)

  • 生产者消费者 共用同一把锁

    public synchronized void produce(){
        if(goods < 20){
            goods++; 
            sout("生产产品");
            nofity();   //唤醒消费者,通知消费
        }else{
            wait();    //等待消费
        }
    }
    public synchronized void consum(){
        if(goods > 0){
            goods--;
            sout("消费产品");
            nofity();   //唤醒生产者,通知生产
        }else{
            wait();  //等待生产
        }
    }
    

6.6 新增线程创建方式

JDK5.0新增了两种创建线程的方式 ---(共四种创建线程的方式)

线程创建方式三:实现Callable接口

1.创建一个实现 Callable接口 的实现类
2.实现接口中的call( ) --> 将此线程执行的操作声明在call( )中,可以有返回值
3.创建实现类对象
4.创建FutureTask对象,将实现类对象 传入 FutureTask构造器中 ---(FutureTask 实现了Runnale接口)
5.创建Thread对象,将FutureTask对象 传入 Thread构造器中
6.通过Thread对象 调用 start( )
---> futureTask.get( )可以获得 call( )的返回值

//1.创建一个实现 Callable接口 的实现类,实现call()
class myThread implements Callable{
     public Object call() throws Exception{ 
         sout("线程要执行的操作");
         return null;  //线程的返回值
     }
}

public void main(){
 //2.创建实现类对象
 myThread w = new myThread();
 //3.创建FutureTask对象,将实现类对象 传入 FutureTask构造器中 
 FutureTask futureTask = new FutureTask(w);
 //4.创建Thread对象,将FutureTask对象 传入 Thread构造器中,调用start()
 new Thread(futureTask).start();

 Object ob = futureTask.get();//5.futureTask.get()可以获得 call()的返回值
}

实现Callable接口的方式 比 实现Runnable接口方式 创建多线程更强大?
 call( )可以有返回值
 call( )可以抛出异常,被外面的操作捕获,获取异常的信息
 Callable是支持泛型

线程创建方式四:线程池

提前创建好多个线程,放入线程池中,使用时直接获取,用完放回。

  • 提高相应速度,降低资源消耗(避免重复的创建销毁,实现重复利用)
  • 便于线程管理:
     corePoolSize:核心池的大小
     maximumPoolSize:最大线程数
     keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池的创建

1.提供指定线程数量的线程池

2.执行指定的线程的操作,需要提供 实现Runnable接口/Callable接口的实现类对象
execute(Runnable实现类对象) / submit(Callable实现类对象)

3.关闭连接池 shutdown()

class MyRunThread implements Runnable{
    public void run(){};
}
class MyCalThread implements Callable{
    public Object cal(){};
}
public void main(){
    //1.提供指定数量的线程池
    ExecutorService service = Executors.newFixedThreadPool(8);
    //2.执行指定线程的操作--管理线程
    service.execute(new MyRunThread() ); //适合 Runnable
    service.submit(new MyCalThread() );  //适合 Callable
    service.setXXX;  //线程管理
    //3.关闭连接池
    service.shutdown();
}

image-20220326214052866

这篇关于6、多线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!