多线程(如何创建+解决安全问题)

发布时间:2023年12月30日

目录

程序、进程和线程与并行、并发的概念

创建和启动线程(重点)

创建方式1:继承Thread类

创建方式2:实现Runnable接口

?对比两种方式:

Thread类的常用方法和生命周期

线程中的构造器

线程中常用的方法

过时方法:

线程的优先级

生命周期

(jdk5之前)

jak5之后(阻塞细分了)

同步代码块解决两种线程创建方式的线程安全问题(重点)

生活中的例子:

使用线程同步机制解决

方式1:同步代码块

方式2:同步方法

优缺点

解决单例模式中的懒汉式的线程安全问题

死锁

理解:

诱发死锁的原因?以下四个条件同时出现就会出现死锁

如何解决?

ReentrantLock的使用(另一种解决线程安全问题的方式)

?步骤:

面试题synchronized同步的方式于Lock的对比?

线程的通信机制与生产者消费案列(不是重点)

线程间通信的理解

涉及到三个方面:

wait和sleep的区别

消费者&生产者

clerk

生产者

?消费者

?测试

线程的创建方式(5.0新增特性)(2个)

Callable(相较于Runnable)

线程池(juc学)


程序、进程和线程与并行、并发的概念

????????程序(program):为了完成特定的任务,用某种语言编写的一组指令,即指一段静态的代码

????????进程(process):程序的一次执行过程。程序是静态的,进程是动态的。进程作为操作系统调度和分配资源的最小单位

????????线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。线程作为CPU调度和执行的最小单位。

????????并行(parallel):指两个或多个事件在同一时刻发生(同时发生),有多条指令在多个CPU上同时执行。

????????并发(concurrency):指两个或多个事件在同一时间段发生。即在一段时间内,有多条指令在单个CPU上快速轮换、交替执行,使得宏观上具有多个进程同时执行的效果

创建和启动线程(重点)

创建方式1:继承Thread类

  • 1.1创建一个继承于Thread类的子类
  • 1.2重写Thread类的run( )--->将此线程要执行的操作,声明在此方法体中。
  • 1.3创建当前Tread的子类的对象
  • 1.4通过对象调用start( ):
    • 1.启动线程
    • 2.调用当前线程的run()方法
  • 注意:
    • 不能让已经start( )的线程,再次执行start( ),否则会报异常IllegalThreadStateException,每个创建线程都会有一个status变量,第一次调用start方法时此staus会改变,当再次调用时,会先判断这个状态变没变,如果变了就会报错。
    • 不能调用用对象调用run()方法,如果这样做,实际只是在一个线程中,先调用了run方法里面的内容而已,并没有启动线程;所以start也有启动线程的重要功能。

//创建继承于Tread类的子类
class PrintNumber extends Thread{

//重写run方法
    public void run(){
        for(int i=1;i<=100;i++){
            if(i%2==0)
            System.out.println(i);
        }
    }
        

}
public class EvenNumberTest{
    public static void main(String[] args){
    //创建当前Thread的子类对象
        PrinterNumber t1 = new PrinterNumber();

    //通过对象调用start方法
        t1.start();
    
    }
}

创建方式2:实现Runnable接口

  • 2.1创建一个实现Runnable接口的类
  • 2.2实现接口中的run( )方法
  • 2.3创建当前实现类的对象
  • 2.4将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
  • 2.5Thread类的实例调用start( ):1.启动线程2.调用当前线程的run()方法
//创建实现Runnable接口的类
class PrintNumber implements Runnable{

//重写run方法
    public void run(){
        for(int i=1;i<=100;i++){
            if(i%2==0)
            System.out.println(i);
        }
    }
        

}
public class EvenNumberTest{
    public static void main(String[] args){

    //创建对象
        PrinterNumber p = new PrinterNumber();

    //作为参数传递到Thread类的构造器中
        Thread t1 = new Thread(p);
        
    //通过对象调用start方法
        t1.start();
    
    }
}

?对比两种方式:

  • 共同点:①启动线程,使用的是Thread类中定义的start( ) ②创建的线程对象都是Thread类或其子类的对象
  • 不同点:一个是类的继承,一个是接口的实现
  • 建议使用使用Runnable接口的方式
    • Runnable方式的好处:
      • 实现的方式,避免了类的单继承性
      • 更适合处理有共享数据的问题
      • 实现了代码和数据的分离

????????两个方法的联系:在源码中,Thread类是这样定义的:public class Thread implements Runnable(代理模式),所以其实Thread也是实现Runnable接口的类

Thread类的常用方法和生命周期

线程中的构造器

  • public Thread( ):分配一个新的线程对象
  • public Thread( String name):分配一个指定名字的新的线程对象
  • public Thread( Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run()方法。
  • public Thread( Runnable target ,String nam):分配一个带有指定目标新的线程对象并指定名字。

class PrintNumber extends Thread{
    public PrintNumber(){
    
    }
    public PrintNumber(String name){
        super(name);
    }
    //……………………


    public void run(){
        for(int i=1;i<=100;i++){
            if(i%2==0)
            System.out.println(i);
        }
    }
        

}

线程中常用的方法

  • start( ):1.启动线程;2.调用线程中的run( )。
  • run( ):将线程要执行的操作,声明在run( )方法中。
  • currentThread( ):获取当前执行代码对应的线程。
  • getName( ):获取线程名。
  • setName( ):设置线程名字(会抛出来非运行异常,记得处理)。
  • sleep(long millis ):静态方法,调用时,可以使得当前线程睡眠指定毫秒数。
  • yield() :静态方法,一旦执行此方法,就要释放CPU的执行权。(可能会被别的线程抢到,也可能抢不到)。
  • join( ):在线程a中通过线程b调用join( ),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。
  • isAlive( ):判断当前线程是否存活。

过时方法:

  • stop( ):强行结束一个线程的执行,直接进入死亡状态。不建议使用(可能造成资源不能被关闭,造成内存泄漏)
  • void suspend( )/void resume( ):可能造成死锁,不建议使用

线程的优先级

  • getPriority( ):获取线程优先级
    • 三个全局变量
      • MAX_PRIORITY(10)最高优先级
      • NORM_PRIORITY(5)普通优先级
      • MIN_PRIORITY(1)最低优先级·
  • setPriority():(范围1到10),设置优先级。

????????还是会有交互的,不过高优先级分配CPU的概率会更高,并不是一定先执行完。

生命周期

(jdk5之前)

新建、就绪、运行、死亡、阻塞(临时状态)5种状态

jak5之后(阻塞细分了)

阻塞细分为锁阻塞、计时等待、无限等待

同步代码块解决两种线程创建方式的线程安全问题(重点)

生活中的例子:

????????卖火车票,

  • 多线程卖票,出现的问题:重票和错票
  • 什么原因导致的?
    • 线程操作ticket过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作
  • 如何解决?必须保证一个线程在操作ticket的过程中,其他线程必须等待,直到该线程操作ticket结束之后,其它线程才可以进来继续操作ticket。

使用线程同步机制解决

方式1:同步代码块

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

说明

  • 需要被同步的代码:即为操作共享数据的代码
  • 共享数据:即多个线程都需要操作的数据。比如ticket
  • 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
  • 同步监视器(锁):哪个线程获取了锁,哪个线程就能执行需要被同步的代码
  • 同步监视器,可以使用任何一个类的对象充当。但是多个线程必须公用同一个同步监视器。
  • 注意:实现接口的方式中锁可以用this,继承的方式中锁慎用this,可以用当前类.class来代换。

方式2:同步方法

  • 如果操作的共享数据的代码完整的声明在了一个方法中,那么就可以将此方法声明为同步方法即可
  • 非静态的同步方法,默认同步监视器是this
  • 静态的同步方法,默认同步监视器是当前类本身(当前类.class)

优缺点

  • 好处:解决了线程的安全问题
  • 弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低一些

解决单例模式中的懒汉式的线程安全问题

代码演示


class Bank{
    private Bank(){}

    private static volatile Bank instance = null;//volatile避免指令重排,下面的两种方式其实都应该加上。主要是防止new Bank(),操作后,虽创建了对象,但并没有执行init方法,就return返回了,这时instance还是null,可能会造成线程的不安全问题。,

    //方式1:同步方法
    public static synchronized Bank getinstance(){
        if(instance == null){
            instance = new Bank();
        }
        return instance;
    }
}

class Bank{
    private Bank(){}

    private static Bank instance = null;

    //方式2:同步代码块
    public static Bank getinstance(){
       synchronized(Bank.class){
            if(instance == null){
                instance = new Bank();
            }
            return instance;
        }
    }
}

class Bank{
    private Bank(){}

    private static Bank instance = null;

    //方式3:效率更高
    public static Bank getinstance(){
      if(instance == null){//这次判断是优化,使创建单例之后,其他线程直接不执行以下操作
       synchronized(Bank.class){
            if(instance == null){
                instance = new Bank();
            }
            return instance;
       }
      }
    }
}

死锁

线程的同步机制带来的问题

理解:

????????不同的线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源,就形成了线程的死锁。我们编写程序时,要避免出现死锁。

诱发死锁的原因?以下四个条件同时出现就会出现死锁

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

如何解决?

以下条件,破坏一个条件就解决了

  • 针对条件1:基本改变不了
  • 针对条件2:可以考虑一次性申请所有所需资源,这样就不存在等待的问题了
  • 针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已占用的资源
  • 针对条件4:将资源改为线性排序。申请资源时,先申请序号小的,避免循环等待的问题。

ReentrantLock的使用(另一种解决线程安全问题的方式)

?步骤:

  • 1.创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
  • 2.执行lock()方法,锁定对共享资源的调用
  • 3.unlock()的调用,释放对共享数据的锁定

面试题synchronized同步的方式于Lock的对比?

  • synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用
  • lock是通过两个方法控制需要被同步的代码块,更灵活
  • lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高

线程的通信机制与生产者消费案列(不是重点)

想看了看一下

线程间通信的理解

????????当我们“需要多个线程”来完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调他们的工作,以此实现多线程共同操作一份数据

涉及到三个方面:

  • wait( ):线程一旦执行此方法,就会进入等待状态,同时会释放对同步监视器的调用
  • notify():一旦执行此方法,就会唤醒被wait( )的线程中优先级最高的那个,(如果优先级一样,则随机唤醒),被唤醒的线程从当初被wait()的位置继续执行
  • notifyAll():一旦执行此方法,就会唤醒所有被wait()的线程。
  • 注意点:
    • 此三个方法的使用必须是在同步代码块或同步方法中(lock需要配合Condition实现线程的)
    • 此三个方法的调用者必须是同步监视器。否则会报异常。
    • 此三个方法声明在Object类中。

wait和sleep的区别

  • 相同点:一旦执行,当前线程都会进入阻塞状态
  • 不同点:
    • 声明的位置:wait声明在Object中,sleep声明在Thread中、静态方法
    • 使用场景:wait只能使用在同步代码块和同步方法中,sleep可以在任何需要的场景使用
    • 使用在同步代码块和同步方法中,wait会释放同步监视器,sleep不会释放
    • 结束阻塞的方式:wait到达指定时间自动结束阻塞,或者通过被notify唤醒结束阻塞。sleep到达指定时间主动结束阻塞

消费者&生产者

clerk

生产者

?消费者

?测试

线程的创建方式(5.0新增特性)(2个)

Callable(相较于Runnable)

  • call() 可以有返回值,更灵活
  • call() 可以使用throws的方式解决异常
  • Callable使用了泛型参数,可以指明call() 的返回值类型
  • 如果需要在主线程当中获取分线程call( )的返回值,则此时的主线程是阻塞状态的

线程池(juc学)

  • 好处:
    • 提高了程序执行的效率。(因为线程已经提前创建好了)
    • 提高了资源的复用率,因为执行完的线程并未销毁,而可以执行其他任务
    • 可以设置相关参数,对线程池中的线程的使用进行管理

文章来源:https://blog.csdn.net/yiyiyu66/article/details/135245878
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。