19.多线程

目录

        • 1. 程序、进程、线程的概念
        • 2. 单核CPU和多核CPU的理解
        • 3. 任务执行_并行与并发
        • 4. 使用多线程的优点
        • 5. 何时需要多线程
        • 6. Thread类
        • 7. 创建多线程方式
        • 8. JDK5.0新增线程创建方式
        • 9. 线程的常用方法
        • 10. 线程优先级的设置
        • 11. 线程同步/线程安全
        • 12. 同步机制中的锁
        • 13. 线程的死锁问题
        • 14. Lock(锁)
        • 15. 生命周期
        • 16. 线程的通信

1. 程序、进程、线程的概念

  1. 程序:

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

  2. 进程:

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

    如:运行中的QQ,运行中的MP3播放器

    程序是静态的,进程是动态的

    进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

  3. 线程:

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

    1.若一个进程同一时间并行执行多个线程,就是支持多线程的

    2.线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小

    3.一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患

19.多线程_第1张图片

2. 单核CPU和多核CPU的理解

  1. 单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务,但是因为CPU时间单元特别短,因此感觉不出来。感觉是同时在进行。

  2. 多核CPU:能更好的发挥多线程的效率。(现在的服务器都是多核的)

  3. 一个Java应用程序java.exe,其实至少有三个线程:

    1. main()主线程。
    2. gc()垃圾回收线程。
    3. 异常处理线程。当然如果发生异常,会影响主线程。

3. 任务执行_并行与并发

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

  2. 并发:一个CPU(采用时间片)同时执行多个任务。比如:多个人做同一件事(购物-多人购买同一件商品)

4. 使用多线程的优点

背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),

肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。

  2. 提高计算机系统CPU的利用率

  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

5. 何时需要多线程

  1. 程序需要同时执行两个或多个任务。

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

  3. 需要一些后台运行的程序时(垃圾回收)

6. Thread类

  1. Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
  2. Thread类的特性
    1. 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
    2. 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

7. 创建多线程方式

​ 步骤:

  1. 创建一个Thread的子类

  2. 重写run()的方法

  3. 创建Thread类的子类对象

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

  • 方式一:创建一个 Thread的子类 这个子类应该重写run()的方法 ,将线程的操作写在run()方法中。 然后可以分配并启动子类的实例。
1.创建`Thread的子类。
	class PrimeThread extends Thread {
         long minPrime;
         PrimeThread(long minPrime) {
             this.minPrime = minPrime;
         }
        
         public void run() {
             // compute primes larger than minPrime  线程运行的代码
              . . .
         }
     }
--------------------------------------------------------------------------------

然后,以下代码将创建一个线程并启动它运行: 
	//启用线程,运行run()方法的内容
     PrimeThread p = new PrimeThread(143);
     p.start();
     PrimeThread p2 = new PrimeThread(143);
     p2.start();

--------------------------------简化版----------------------------------------------
    new Thread(){
    public void run(){
        //业务逻辑
        }
    }.start();
    
  • 方式二:方法来创建一个线程是声明 实现类Runnable接口 类然后实现了run方法。

​ 然后可以分配类的实例,在创建Thread对象时作为参数传递,并启动。

​ 可以天然的实现共享数据的效果

     class PrimeRun implements Runnable {
         long minPrime;
         PrimeRun(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
             //业务逻辑
              . . .
         }
     }
 
--------------------------------------------------------------------------------

然后,以下代码将创建一个线程并启动它运行: 
    PrimeRun p = new PrimeRun(143);
    Thread t1 = new Thread(p);
	Thread t1 = new Thread(p);
	t1.start()
    t1.start();

8. JDK5.0新增线程创建方式

  • 方式三: 实现Callable接口

与使用Runnable相比, Callable功能更强大些

call()方法,可以有返回值

方法可以抛出异常

支持泛型的返回值

需要借助FutureTask类,比如 加入线程中 或者是 获取返回结果

Future接口

1.FutrueTask是Futrue接口的唯一的实现类

2.可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

3.FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
         //业务逻辑
        return Object;
    }
}
----------------------------------------------------------------
public static void main(String[] args) {
    //3.创建Callable接口实现类的对象
    NumThread numThread = new NumThread();
    //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
    FutureTask futureTask = new FutureTask(numThread);
    //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
    new Thread(futureTask).start();
    
    //6.获取Callable中call方法的返回值 不需要返回值可以不写
    try {
        Object sum = futureTask.get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}
  • 方式四:使用线程池

背景:

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

思路:

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

好处:

提高响应速度(减少了创建新线程的时间)

降低资源消耗(重复利用线程池中线程,不需要每次都创建)

便于线程管理

线程池相关API

  • JDK 5.0起提供了线程池相关API:
    • ExecutorService 真正的线程池接口
    • Executors

ExecutorService:

方法 返回值 作用
execute(Runnable command) void 执行任务/命令,没有返回值,一般用来执行Runnable
submit(Callable task) 执行任务,有返回值,一般又来执行Callable
shutdown() void 关闭连接池

ThreadPoolExecutor: ExecutorService的实现类可以设置线程词的属性

方法 作用
corePoolSize 核心池的大小
maximumPoolSize 最大线程数
keepAliveTime 线程没有任务时最多保持多长时间后会终止

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

可以方法 作用
Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n) 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行

案例:

class RunThread implements Runnable{
    @Override
    public void run() {
        // 业务逻辑
    }
}

class CallThread implements Callable{
    @Override
    public Object call() throws Exception {
       
       //业务逻辑  和 需要return出去的结果
        return obj;
    }
}


public class ThreadPool {	
    public static void main(String[] args) throws Exception {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        /**ThreadPoolExecutor 可以设置线程词的属性  根据ExecutorService的实现类来设置属性
        *  	ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        * 	service1.setCorePoolSize(15);
        *	service1.setKeepAliveTime();
        */
        
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new RunThread());//适合适用于Runnable
        service.submit(new CallThread());//适合使用于Callable
        
        //3.关闭连接池
        service.shutdown();
    }
}

9. 线程的常用方法

常用方法 作用
start() 启用当前线程的run()
run() 通常需要重写Thread的run(),将线程操作放在里面
currentThread() Thread的静态方法,获取当前线程的信息
getName() 获取当前线程的名字
setName() 设置线程的名字
yield() 释放当前CPU的执行权
join() 在线程A中调用线程B的join(),此时A线程进入阻塞状态,
直到线程B完全执行,线程A才结束阻塞状态(线程插入)
stop() 强制结束当前线程
sleep(long millis) 线程休眠,休眠时间结束并不会立即运行,
还需要等待系统分配资源,单位毫秒
wait() 线程等待,等候其他线程调用notify()或notifyAll()方法唤醒
notify() 唤醒单个正在排队等待同步资源的线程
notifyAll() 唤醒所有正在排队等待同步资源的线程
isAlive() 判断当前是否存活,也就是线程是否执行完毕
getPriority() 获取线程优先级
setPriority(int newPriority) 设置线程优先级

10. 线程优先级的设置

说明

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

低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用,具体受到CPU的影响

线程的优先级等级

MAX_PRIORITY:10

MIN _PRIORITY:1

NORM_PRIORITY:5 (默认)

涉及的方法

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

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

11. 线程同步/线程安全

什么是线程安全

  • 有共享数据才有线程安全问题
  • 多个线程执行的不确定性引起执行结果的不稳定
  • 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。

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

如何解决:通过同步机制来解决 synchronized

创建线程安全的方式:

  1. 同步代码块
  2. 同步方法
  3. lock锁
1. 同步代码块:
synchronized (obj){
// 需要被同步的代码(操作共享数据的代码);
}

2. synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:
public synchronized void show (String name){.
}

3.lock锁
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();

public void xxx() {
    try{
        //2.调用锁定方法lock()
        lock.lock();
        
    }finally {
        //3.调用解锁方法:unlock()
        lock.unlock();
    }
}

12. 同步机制中的锁

第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,

而在其被解锁之时,另一个任务就可以锁定并使用它了

1. synchronized 的锁是什么?

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。

同步方法的锁:静态方法(类名.class)、非静态方法(this)

同步代码块:自己指定,很多时候也是指定为this或类名.class

2. 注意:

必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全

一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),

同步代码块(指定需谨慎)

同步代码块
class PrimeThread implements Runnable{};

synchronized (this){
// 需要被同步的代码(操作共享数据的代码); this代表同一个对象(唯一)
}

class PrimeThread extends Thread{};
synchronized (PrimeThread.class){
// 需要被同步的代码(操作共享数据的代码);  类只会加载一次,可以作为同步锁
}
同步方法
class PrimeThread implements Runnable{};
public synchronized void show (String name){ 
    //  默认的锁是this
}


class PrimeThread extends Thread{};
public synchronized void show (String name){ 
    //  默认的锁是this,实现不了线程安全,继承会创建多个对象
    //  解决办法,把当前方法改为静态的,默认的锁会改为当前类PrimeThread.class
}

3. 同步的范围

范围太小:没锁住所有有安全问题的代码

范围太大:没发挥多线程的功能。可能出现不符合逻辑的功能

4.线程的安全问题:

对于单例模式:在多线程过程中,首先线程A先获得系统资源但还未完全创建出单例对象,轮到线程B获得系统资源也进入方法创建单例对象,则结果会new两次单例对象。

解决方法:

在创建单例对象的方法加上 synchronized

或者在创建单例的代码中用synchronized { }包住

19.多线程_第2张图片

13. 线程的死锁问题

  • 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

案例:
public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();

结果可能是
    ab 12 / adcd 1234
    cd 34 / cdab 3412
    死锁,什么都不输出(假设线程0先握住锁s1,线程1先握住s2,休眠结束后,两个线程都没有释放锁就导致死锁产生)

  • 解决方法

专门的算法、原则

尽量减少同步资源的定义

尽量避免嵌套同步

14. Lock(锁)

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

	//1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();    
	public void xxx() {
        try{
            //2.调用锁定方法lock()
            lock.lock();
        }finally {
            //3.调用解锁方法:unlock()
            lock.unlock();
        }
    }

注:要使用try{}finally{},确定解锁,要不会占用资源

synchronized与Lock的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是

隐式锁,出了作用域自动释放

  1. Lock只有代码块锁,synchronized有代码块锁和方法锁

  2. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有

更好的扩展性(提供更多的子类)

15. 生命周期

生命周期中通常要经历如下的 五种状态

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

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

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

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

  5. 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
    19.多线程_第3张图片

16. 线程的通信

一、 wait() 与notify() 和notifyAll()

  1. wait()

令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。

wait()和sleep();

  1. 相同的:都可以使线程进入阻塞状态T
  2. 不同点:
    1. 声明位置不同,sleep()声明在Thread中,wait()声明在Object中
    2. sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
    3. 如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

wait()释放锁释放资源进入等待状态,sleep()进入阻塞状态,不释放锁

  1. notify()

唤醒正在排队等待同步资源的线程中优先级最高者结束等待

  1. notifyAll ()

唤醒正在排队等待资源的所有线程结束等待.

二、

  1. 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报

java.lang.IllegalMonitorStateException异常。

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

三、

  1. 因为这三个方法必须由锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明中。
  2. wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

你可能感兴趣的:(JavaSE,java,jvm,面试)