Java 复习 线程

线程

进程与线程的概述

  • 进程:是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
  • 进程特点:独立性 动态性 并发性
  • 线程:线程就是进程中的一个独立控制单元,线程在控制着进程的执行,一个进程中至少有一个线程,线程是操作系统能够运算调度最小单位,它被包含在进程之中,是进程的实际运作单位
  • 多线程:一个进程中有多个线程,其中有一个主线程来调用本进程中的其他线程。我们看到的进程的切换,切换的就是不同进程的主线程
  • 多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能

进程与线程的关系

  • 一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)
  • 每个线程在共享同一个进程中内存的同时,又有自己独立的内存空间,所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的

多线程的优点

  • 为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用了多线程则在主线程序执行任务的同时可以执行其他任务,而不需要等待
  • 进程之间不能共享数据,但是线程之间可以共享数据
  • 系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小
  • Java语言内置了多线程功能支持,简化了java多线程编程

多线程的特性

  • 随机性

串行:同一时刻一个cpu只能处理一件事
并行:同一时刻多个cpu可以处理多件事

  • CPU分时调度

FCFS(First Come First Service 未来先服务算法) 时间片策略:线程的调度采用时间片轮转的方式
SJS(Short Job Service) 短服务算法 抢占式策略:高优先级的线程抢占CPU

  • 线程的状态

创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列
就绪(可运行)状态:线程已经准备运行,只要获得CPU,就可立即执行
执行(运行)状态:线程已获得CPU,其程序正在运行的状态
终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
阻塞状态:由于运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞

  • 线程生命周期
  1. 新建状态:当线程对象创建后就进入了新建状态
  2. 就绪状态:当调用线程对象的start()方法,线程即为进入就绪状态

处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行

  1. 运行状态:当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态

就绪状态时进入运行状态的唯一出口,也就是线程想要进入运行状态执行,先得处于就绪状态

  1. 阻塞状态:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到进入就绪状态才有机会被CPU选中再次执行

根据阻塞状态产生原因不同,阻塞状态分为三种:

  • 等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
  • 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
  • 其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时,join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
  1. 死亡状态:线程执行完或者因异常退出了run()方法,该线程结束生命周期
  • 线程的优先级

等级:
线程优先级分为1 - 10,优先级越高,数量越大,默认优先级为5
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5

高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行

多线程的创建方式

继承于Thread

概述

Thread类本质上时实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。

创建方式
  1. 创建一个继承于 Thread 类的子类
  2. 重写 Thread 类的run()方法
  3. 创建Thread子类对象
启动方式

使用start()方法来启动多线程。start()方法是一 native 方法,它将通知底层操作系统,最终由操作系统启动一个新线程,操作系统将执行 run() 。这种方式实现的线程很简单,通过自己的类extend Thread,并重写 run() 方法,就可以自动启动新线程并执行自己定义的run()方法

start() 与 run() 的区别

  • start()作用:启动当前线程 调用当前线程的重写的 run() 方法(在主线程中生成子线程,有两条线程),调用start()方法以后,一条路径 代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start()方法
  • run()作用:在主线程中调用之后,直接在主线程一条线程中执行了该线程中run()方法(调用线程中的run()方法,只调用方法,并不会新开线程)
  • 我们不能通过run()方法新开线程,run()方法中只是写入我们需要执行的代码,使用start()方法开启线程,再调用run(0方法进行执行其中的代码

代码:

 public static void main(String[] args) {
        //4.创建线程对象进行测试
        /*4.new对应的是线程的新建状态
        * 5.要想模拟多线程,至少得启动2个线程,如果只启动1个,是单线程程序*/
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        MyThread t4 = new MyThread();
        /*6.这个run()如果直接这样调用,是没有多线程抢占执行的效果的
        * 只是把这两句话看作普通方法的调用,谁先写,就先执行谁*/
        //t1.run();
        //t2.run();
        /*7.start()对应的状态就是就绪状态,会把刚刚新建好的线程加入到就绪队列之中
        * 至于什么时候执行,就是多线程执行的效果,需要等待OS选中分配CPU
        * 8.执行的时候start()底层会自动调用我们重写的run()种的业务
        * 9.线程的执行具有随机性,也就是说t1-t4具体怎么执行
        * 取决于CPU的调度时间片的分配,我们是决定不了的*/
        t1.start();//以多线程的方式启动线程1,将当前线程变为就绪状态
        t2.start();//以多线程的方式启动线程2,将当前线程变为就绪状态
        t3.start();//以多线程的方式启动线程3,将当前线程变为就绪状态
        t4.start();//以多线程的方式启动线程4,将当前线程变为就绪状态
    }
}

//1.自定义一个多线程类,然后让这个类继承Thread
class MyThread extends Thread{
    /*1.多线程编程实现的方案1:通过继承Thread类并重写run()来完成的 */
    //2.重写run(),run()里是我们自己的业务
    @Override
    public void run() {
        /*2.super.run()表示的是调用父类的业务,我们现在要用自己的业务,所以注释掉*/
        //super.run();
        //3.完成业务:打印10次当前正在执行的线程的名称
        for (int i = 0; i < 10; i++) {
            /*3.getName()表示可以获取当前正在执行的线程名称
            * 由于本类继承了Thread类,所以可以直接使用这个方法*/
            System.out.println(i+"="+getName());
        }
    }

实现Runnable接口

概述

如果自己的类已经继承了另一个类,就无法多继承,所以我们可以通过实现 Runnable 接口来创建多线程

创建方式
  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法:run()
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类中的构造器,创建 Thread 类的对象
启动方式

启动方式与extend Thread 一样
代码:

 public static void main(String[] args) {
        //5.创建自定义类的对象--目标业务类对象
        MyRunnable target = new MyRunnable();
        //6.如何启动线程?自己没有,需要与Thread建立关系
        Thread t1 = new Thread(target);
        Thread t2 = new Thread(target);
        Thread t3 = new Thread(target);
        Thread t4 = new Thread(target);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

//1.自定义多线程类
class MyRunnable implements Runnable{
    //2.添加父接口中的抽象方法run(),里面是自己的业务
    @Override
    public void run() {
        //3.写业务,打印10次当前正在执行的线程名称
        for (int i = 0; i < 10; i++) {
            /*问题:自定义类与父接口Runnable中都没有获取名字的方法
            * 所以还需要从Thread中找:
            * currentThread():静态方法,获取当前正在执行的线程对象
            * getName():获取当前线程的名称*/
            System.out.println(i+"="+Thread.currentThread().getName());
        }
    }

两种方式的比较

两种方式都需要重写 run() 方法,将线程要执行的逻辑代码声明在run()方法中
继承方式:

  • 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
  • 缺点:自定义的线程类已继承了Thread类,所以后续无法再继承其他类
    实现方式:
  • 优点:自定义的线程类只是实现了 Runnable接口或 Callable 接口,后续还可以继承其他类,在这种方式下,多个线程可以共享同一个 target 对象,非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想
  • 缺点:编程稍微复杂,如想访问当前线程,则需要使用Thread.currentThread()方法
    开发中,优先使用实现 Runnable接口的方式
  1. 实现的方式没有类的单继承性的局限性
  2. 实现的方式更适合用来处理多个线程有共享数据的情况

线程池方式

概述
  • 背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。
  • 好处:
  1. 提高响应速度(减少了创建新线程的时间)。
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理
  • ExecutorService:用来存储线程的池子,把新建的线程/启动线程/关闭线程的任务都交给池来管理

execute(Runnable任务对象) 把任务丢到线程池

  • Executors:辅助创建线程池的工具类

newFixedThreadPool(int nThreads) 最多n个线程的线程池
newCachedThreadPool() 足够多的线程,使任务不必等待
newSingleThreadExecutor() 只有一个线程的线程池

  • 线程池代码:
public static void main(String[] args) {
        //3.创建对象进行测试
        MyRunnable target = new MyRunnable();

        //创建线程池
        /**创建线程池的工具:Executors使用
         * newFixedThreadPool(线程数)方法来创建出指定线程数的线程池
         * 创建出来的线程池类型是:ExecutorService*/
        ExecutorService pool =
                Executors.newFixedThreadPool(5);
        for(int i = 0;i<5;i++){
            //让池对象来完成任务,任务就是参数target
            pool.execute(target);
        }

    }
}

class MyRunnable implements Runnable{
    //2.把业务放入run(),实现的是Runnable接口中的run()
    @Override
    public void run() {
        //需求:打印10次当前正在执行的线程名称
        for(int i = 1; i<= 10;i++){
            //线程对象.getName()获取当前正在执行的线程对象的名称
            System.out.println(i+"="+Thread.currentThread().getName());
        }
    }

线程通信方式

wait() / notify() / notifyAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁时任意对象都能充当的,所以这三个方法定义在Object类中

  • wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先操作) – Object.wait 进入Obj这个锁住的区域的线程把锁交出来原地等待通知
  • notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) – Object.notify 新线程进入Obj这个区域进行操作并唤醒wait的线程

线程的分类

Java中的线程分为两类:守护线程(如垃圾回收线程,异常处理线程),用户线程(如主线程)。若JVM中都是守护线程,当前JVM退出(形象理解,唇亡齿寒)

同步锁

当线程中出现线程安全问题时,我们需要解决,这就用到了同步锁,判断程序的线程有没有可能出现线程安全问题,主要有以下三个条件:

在多线程程序中 + 有共享数据 + 多条语句操作共享数据

多线程场景和共享数据的条件是改变不了的,所以思路可以从 “多条语句操作共享数据” 入手,既然是在这多条语句操作数据过程中出现了问题那我们可以把有可能出现问题的代码都包裹起来,一次只让一个线程来执行

同步与异步
  • 同步:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权力的线程排队。
    坏处就是效率会降低,不过保证了安全
  • 异步:体现了多线程抢占资源的效果,线程间互不等待,互相抢占资源。
    坏处就是有安全隐患,效率要高一些
解决

要解决线程安全问题,我们可以给程序加一个同步锁,即synchronized 同步关键字

  1. 方式一:
    使用同步代码块

写法:
synchronized (锁对象){
需要同步的代码(也就是可能出现问题的操作共享数据的多条语句)
}

说明:

  • 操作共享数据的代码(所有线程共享的数据的操作的代码(视为共享区域)),即为需要共享的代码(同步代码块,相当于单线程,效率低)
  • 共享数据:多个线程共同操作的数据
  • 同步监视器(俗称:锁):任何一个对象都可以充当锁。当锁住以后只能有一个线程能进去
  • Runnable 天生带锁,而 Thread中需要用 static 对象或者 this 关键字或者当前类来充当唯一锁
  1. 方式二:
    使用同步方法,对方法进行 synchronized 关键字修饰
  • 对于实现 Runnable 接口方式的多线程,只需要将同步方法用 synchronized 修饰

写法:
public void synchronized method(){
需要同步的代码(也就是可能出现问题的操作共享数据的多条语句)
}

  • 对于继承自 Thread 方式,需要将同步方法用 static 和 synchronized 修饰,因为对象不唯一(即锁不唯一)

写法:
pblic static void synchronized method(){
需要同步的代码(也就是可能出现问题的操作共享数据的多条语句)
}
说明:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
  2. 非静态的同步方法,同步监视器是this
  3. 静态的同步方法,同步监视器是当前类本身,继承自 Thread.class
前提

同步效果的使用前提有两个前提:

  • 前提一:同步需要两个或者两个以上的线程(单线程无需考虑多线程问题)
  • 前提二:多个线程必须使用同一个锁(我上锁后其他人也能看到这个锁,不然我的锁锁不住其他人,就没有了上锁的效果)
特点
  1. synchronized 同步关键字可以用来修饰代码块,称为同步代码块,使用的锁对象类型任意,但注意:必须唯一
  2. synchronized 同步关键字可以用来修饰方法,称为同步方法
  3. 同步的缺点是会降低程序的执行效率,但我们为了保证程序的安全,有些性能是必须要牺牲的
  4. 但是为了性能,加锁的范围需要控制好

同步代码快可以保证同一时刻只有一个线程进入
同步方法不可以保证同一时刻只能有一个线程调用,所以使用本类代指对象this来确保同步

悲观锁和乐观锁

  • 悲观锁:对于并发间操作产生的线程安全问题持悲观状态
    悲观锁认为竞争总会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized ,直接上锁
  • 乐观锁:对于并发间的操作产生的线程安全问题持乐观状态
    乐观锁认为竞争不总会发生,因此它不需要持有锁,将"比较-替换"这两个动作作为一个原子尝试其修改内存中的变量,如果失败则表示发生冲突,那么就应该由相应的重试逻辑
悲观锁
  • synchronized 互斥锁(悲观锁,有罪假设)
    采用 synchronized 修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。
    每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁
  • ReentrantLock 排他锁(悲观锁,有罪假说)
    排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何"读 / 读"、"读 / 写"操作都不能同时发生,在这一定程度上降低了吞吐量。然而读写操作之间不存在数据竞争问题,如果"读 / 读"操作能够以共享锁的方式进行,那会进一步提升性能
乐观锁
  • ReentrantReadWriteLoca 读写锁(乐观锁,无罪假说)
    ReentrantReadWriteLoca 是 Reentrant(可重入) Read(读) Write(写) Lock(锁)
    读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
    读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景

线程死锁问题

概述

线程死锁:僵持,谁也不放手。两者都进入阻塞,都不执行下面的代码
出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续

解决方法
  1. 减少同步共享变量
  2. 采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
  3. 减少锁的嵌套

单例设计模式

概述

常见的 spring 默认创建的 bean 就是单例模式
单例模式好处:

  • 可节约系统内存空间
  • 控制资源的使用
    单例模式最重要的是确保对象只有一个

单例设计模式之饿汉式

代码:

/*本类用于实现单例设计模式实现方案1:饿汉式*/
//饿汉式:不管你用不用这个类的对象,都会直接先创建一个
public class Singleton1 {
    public static void main(String[] args) {
        //5.在main()中,不通过对象,直接通过类名,调用静态方法
        MySingle single1 = MySingle.getSingle();
        MySingle single2 = MySingle.getSingle();
        //6.用==检验是否是同一个对象
        System.out.println(single1 == single2);//true
        System.out.println(single1);
        System.out.println(single2);
    }
}
//0.创建自己的单例程序
class MySingle{
    //1.提供构造方法,并将构造方法私有化
    /*1.构造方法私有化的目的:为了防止外界随意创建本类对象*/
    private MySingle(){ }

    //2.创建本类对象,并将对象也私有化
    //4.2由于静态资源只能调用静态资源,所以single对象也需要设置成静态
    private static MySingle single = new MySingle();

    //3.提供公共的访问方式,返回创建好的对象
    //4.1为了不通过对象,直接调用本方法,需要将本方法设置为静态
    public static MySingle getSingle(){
        return single;
    }

懒汉式代码:

/*本类用于实现单例设计模式优化实现方案2:懒汉式
* 关于单例设计模式的两种实现方式:
* 1.饿汉式:不管你用不用这个类的对象,都会直接先创建一个
* 2.懒汉式:先不给创建这个类的对象,等你需要的时候再创建--延迟加载的思想
* 延迟加载的思想:是指不会在第一时间就把对象创建好占用内存
*               而是什么时候用到,什么时候再去创建对象
* 3.线程安全问题:由于我们存在唯一的对象single2,并且多条语句都操作了这个变量
*   如果将程序放到多线程的环境下,就容易出现数据安全的问题,所以解决方案:
*   1) 将3条语句都使用同步代码块包裹,保证同步排队的效果
*   2) 由于getSingle2()只有这3条语句,所以也可以将本方法设置为同步方法*/
public class Singleton2 {
    public static void main(String[] args) {
        //5.调用方法查看结果
        MySingle2 single1 = MySingle2.getSingle2();
        MySingle2 single2 = MySingle2.getSingle2();
        System.out.println(single1 == single2);
        System.out.println(single1);
        System.out.println(single2);
    }
}
//0.创建自己的单例程序
class MySingle2{
    //6.2创建一个静态的唯一的锁对象
    static Object o = new Object();
    //1.私有化本类的构造方法
    private MySingle2(){ }
    //2.创建的是本类对象的引用类型变量,用来保存对象的地址值,默认值是null
    private static MySingle2 single2 ;
    //3.提供公共的get方法
    synchronized public static MySingle2 getSingle2(){
        //4.判断之前是否创建过对象,之前创建过就直接走return
        //之前如果没有创建过,才走if,创建对象并将对象返回
        //6.有共享数据+多条语句操作数据,所以尽量提前处理,避免多线程数据安全隐患
        //6.1 解决方案1:加同步代码块
        //6.2 解决方案2:将本方法getSingle2()设置为同步方法
        //因为这个方法里所有的语句都需要同步
        synchronized (o) {//静态方法中使用的锁对象也得是静态的
            if (single2 == null) {//single2还是默认值,说明之前没有创建过对象
                single2 = new MySingle2();//没创建过才创建,并赋值给single2
            }
            return single2;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值