thread

线程相关的基本概念

程序(program):

​ 是为完成特定任务、用某种语言编写的一组指令的集合。简单的说:就是写的代码

进程:

  1. 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。

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

线程:

  1. 线程由进程创建的,是进程的一个实体
  2. 一个进程可以拥有多个线程

单线程:同一个时刻,只允许执行一个线程

多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件

并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发

并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行

并发并行可同时存在

线程的基本使用

创建线程的两种方式:

image-20230426133723789

  1. 继承Thread类,重写run方法

​ 当一个类继承了Thread类,该类就可以当成线程来使用。

​ 然后重写run方法,写入自己的业务逻辑

​ run Thread类实现了 Runnable接口的run方法

image-20230426133623131

  1. 实现Runnable接口,重写run方法
  • java是单迷承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了。

  • java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程

package com.thread_;

public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Thread thread = new Thread(dog);	//静态代理模式
        thread.start();
    }
}

class Dog implements Runnable {
    @Override
    public void run() {
        int count = 0;
        while (true){
            System.out.println("hi" + (++count) + Thread.currentThread().getName());

            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }

            if (count == 10){
                break;
            }
        }
    }
}

继承Thread 与 实现Runable的区别

  1. 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档可以看到Thread类本身就实现了Runnable接口start()->start0()
  2. 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制

线程终止

  1. 当线程完成任务后,会自动退出

  2. 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式

线程常用的方法
  1. setName //设置线程名称,使之与参数name相同

  2. getName //返回该线程的名称

  3. start //使该线程开始执行;Java虚拟机底层调用该线程的start()方法

  4. run //调用线程对象run方法;

  5. setPriority //更改线程的优先级

  6. getPriority //获取线程的优先级

  7. sleep //在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)8. interrupt //中断线程

  8. yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功

  9. join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务

注意事项:

  • start底层会创建新的线程,调用run, run就是一个简单的方法调用,不会启动新线程

  • 线程优先级的范围

  • interrupt:中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程

  • sleep:线程的静态方法,使当前线程休眠

用户线程和守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束守护线程自动结束
  3. 常见的守护线程:垃圾回收机制
线程的生命周期

image-20230426161041925

线程转换图:

image-20230426162648594

Synchronized

线程同步机制:

  1. 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性
  2. 理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
同步代码块
synchronized(对象){//得到对象的锁,才能操作同步代码
		//需要被同步代码;
}
synchronized还可以放在方法声明中,表示整个方法-为同步方法
    public synchronized void m (String name){
		//需要被同步的代码
}

互斥锁:

  1. Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
  2. 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象
  3. 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时。表明该对象在任一时刻只能由一个线程访问
  4. 同步的局限性:导致程序的执行效率要降低
  5. 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
  6. 同步方法((静态的)的锁为当前类本身

注意

1.同步方法如果没有使用static修饰:默认锁对象为this

2.如果方法使用static修饰,默认锁对象:当前类.class

3.实现的落地步骤:

  • 需要先分析上锁的代码

  • 选择同步代码块或同步方法,通常优先选择同步代码块,一般来说同步区域越小程序效率越高

  • 要求多个线程的锁对象为同一个即可!

class SellTicket03 implements Runnable {
    private int ticketNum = 100;
    private boolean loop = true;
    Object object = new object();
    
    public /*synchronized*/ void sell() {
        
        synchronized (new Object()/*object*/) {		//new Object()和object前者仍然出现同步问题,因为多个线程的锁对象不为同一个
            if (ticketNum <= 0) {
                System.out.println("票已经售完");
                loop = false;
                return;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票" + "余票" + (--ticketNum));
        }
    }
    @Override
    public void run() {
        while (loop){
            sell();
        }
    }
}
//synchronized关键字简单的解决售票同步问题
package com.thread_;

@SuppressWarnings({"all"})
public class SellTicket {
    public static void main(String[] args) {
//        SellTicket01 sellTicket01 = new SellTicket01();
//        SellTicket01 sellTicket02 = new SellTicket01();
//        SellTicket01 sellTicket03 = new SellTicket01();
//        sellTicket01.start();
//        sellTicket02.start();
//        sellTicket03.start();

//        SellTicket02 sellTicket02 = new SellTicket02();
//        new Thread(sellTicket02).start();
//        new Thread(sellTicket02).start();
//        new Thread(sellTicket02).start();

        SellTicket03 sellTicket03 = new SellTicket03();
        new Thread(sellTicket03).start();
        new Thread(sellTicket03).start();
        new Thread(sellTicket03).start();

    }
}

class SellTicket03 implements Runnable {
    private int ticketNum = 100;
    private boolean loop = true;
    public /*synchronized*/ void sell() {
        synchronized (this) {
            if (ticketNum <= 0) {
                System.out.println("票已经售完");
                loop = false;
                return;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票" + "余票" + (--ticketNum));
        }
    }
    @Override
    public void run() {
        while (loop){
            sell();
        }
    }
}

class SellTicket01 extends Thread {
    private static int ticketNum = 100;
    @Override
    public void run() {
        while (true){
            if (ticketNum <= 0){
                System.out.println("票已经售完");
                break;
            }
            try {
                sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票" + "余票" + (--ticketNum));

        }
    }
}

class SellTicket02 implements Runnable {
    private int ticketNum = 100;
    @Override
    public void run() {
        while (true){
            if (ticketNum <= 0){
                System.out.println("票已经售完");
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票" + "余票" + (--ticketNum));

        }
    }
}
线程死锁

多个线程都占用了对方的锁资源,但不肯相让,导致了死锁

释放锁

下列操作会释放锁

  1. 当前线程的同步方法、同步代码块执行结束
  2. 当前线程在同步代码块、同步方法中遇到break、return
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

下列操作不会释放锁

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。应尽量避免使用suspend()和resume()来控制线程

补充内容

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

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

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

image-20230429182031882

synchronized 与Lock 的对比:

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:
    • Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
线程协作
生产者消费者模式

实例:

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库﹐消费者将仓库中产品取走消费.
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止.
  • 如果仓库中放有产品﹐则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止.

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件:

  • 对于生产者,没有生产产品之前,要通知消费者等待﹒而生产了产品之后﹐又需要马上通知消费者消费
  • 对于消费者﹐在消费之后,要通知生产者已经结束消费﹐需要生产新的产品以供消费.
  • 在生产者消费者问题中,仅有synchronized是不够的
    • synchronized可阻止并发更新同一个共享资源,实现了同步
    • synchronized不能用来实现不同线程之间的消息传递(通信)

Java提供了几个方法解决线程之间的通信问题:

image-20230429182736989

解决方法1:管程法

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

解决方法2:信号灯法

可以理解为缓冲区为1的管程法。

线程池

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

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理(....)
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

JDK 5.0起提供了线程池相关API: ExecutorService和 Executors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

    • void execute(Runnable command)︰执行任务/命令,没有返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
    • void shutdown()∶关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

posted @ 2023-04-26 20:45  Q1uuuu  阅读(45)  评论(0)    收藏  举报