多线程

74 阅读12分钟

多线程

1. 几个概念

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

进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。
               进程作为操作系统调度和分配资源的最小单位。
               
线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。
              线程作为CPU调度和执行的最小单位
线程调度策略

分时调度:所有线程`轮流使用` CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。

抢占式调度:让`优先级高`的线程以`较大的概率`优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
> 单核CPU与多核CPU
> 并行与并发

2. 如何创建多线程(重点)

方式1:继承Thread类

① 创建一个继承于Thread类的子类
class PrintNumber extends Thread{}
② 重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
    	********
    }
③ 创建当前Thread的子类的对象
PrintNumber t1 = new PrintNumber();
④ 通过对象调用start(): 1.启动线程 2.调用当前线程的run()
t1.start();
//① 创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //② 重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() +":" + i);
            }
        }
    }
}
public class EvenNumberTest {
    public static void main(String[] args) {
        //③ 创建当前Thread的子类的对象
        PrintNumber t1 = new PrintNumber();
        //④ 通过对象调用start()
        t1.start();
    }
}

方式2:实现Runnable接口

① 创建一个实现Runnable接口的类
class EvenNumberPrint implements Runnable{}
② 实现接口中的run() -->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
		**********
    }
③ 创建当前实现类的对象
EvenNumberPrint p = new EvenNumberPrint();
④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread t1 = new Thread(p);
⑤ Thread类的实例调用start():1.启动线程 2.调用当前线程的run()
t1.start();
//① 创建一个实现Runnable接口的类
class EvenNumberPrint implements Runnable{
    //② 实现接口中的run() -->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for(int i = 1;i <= 100;i++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class EvenNumberTest {
    public static void main(String[] args) {
        //③ 创建当前实现类的对象
        EvenNumberPrint p = new EvenNumberPrint();
        //④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
        Thread t1 = new Thread(p);
        //⑤ Thread类的实例调用start():1.启动线程 2.调用当前线程的run()
        t1.start();
    }
}
  • 方式3:实现Callable接口 (jdk5.0新增)没咋用过,有需要自行百度了解

  • 方式4:使用线程池(jdk5.0新增)没咋用过,有需要自行百度了解

方式一方式二对比

共同点:

  • ① 启动线程,使用的都是Thread类中定义的start()
  • ② 创建的线程对象,都是Thread类或其子类的实例。

不同点:

  • 一个是类的继承
  • 一个是接口的实现。

建议使用实现Runnable接口的方式。

Runnable方式的好处

  • ① 实现的方式,避免的类的单继承的局限性
  • ② 更适合处理有共享数据的问题。
  • ③ 实现了代码和数据的分离。

3. Thread类的常用方法、线程的生命周期

常用的构造器和方法:

1.线程中的构造器

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

2.线程中的常用方法:

  • start():①启动线程 ②调用线程的run()
  • run():将线程要执行的操作,声明在run()中。
  • currentThread():获取当前执行代码对应的线程
  • getName(): 获取线程名
  • setName(): 设置线程名
  • sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
  • yield():静态方法,一旦执行此方法,就释放CPU的执行权
  • join(): 在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。
  • isAlive():判断当前线程是否存活

过时方法:

stop():强行结束一个线程的执行,直接进入死亡状态。不建议使用

void suspend() / void resume() :可能造成死锁,所以也不建议使用

3.线程的优先级:

  • getPriority():获取线程的优先级
  • setPriority():设置线程的优先级。范围[1,10]

Thread类内部声明的三个常量:

  • MAX_PRIORITY(10):最高优先级
  • MIN _PRIORITY (1):最低优先级
  • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

线程的生命周期:

jdk5.0之前:

image-20221203142900528.png

jdk5.0及之后:Thread类中定义了一个内部类State

public enum State {
       
        NEW,
    
        RUNNABLE,

        BLOCKED,

        WAITING,

        TIMED_WAITING,
 
        TERMINATED;
}

image-20221203143046926.png

4. 如何解决线程安全问题(重点、难点)

  • 什么是线程的安全问题?

    • 多个线程操作共享数据,就有可能出现安全问题。
  • 如何解决线程的安全问题?有几种方式?

    • 同步机制:

      • ① 同步代码块
      synchronized(同步监视器){
          //需要被同步的代码
      }
      
      • ② 同步方法
      public static synchronized void xxxx(){
          //需要被同步的代码
      }
      
      • 重点关注两个事:共享数据及操作共享数据的代码;同步监视器(保证唯一性)

      在实现Runnable接口的方式中,同步监视器可以考虑使用:this。、

      在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用:当前类.class。

      非静态的同步方法,默认同步监视器是this

      静态的同步方法,默认同步监视器是当前类本身。

    • jdk5.0新增:Lock接口及其实现类。(保证多个线程共用同一个Lock的实例)

  • synchronized好处:解决了线程的安全问题。

  • 弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低。

举例Runnable

实现Runnable接口同步代码块:

/**
 * @author Created by xxw on 2023-09-07 11:34
 * @Description  使用实现Runnable接口的方式,实现卖票。--->存在线程安全问题的。
 *               使用同步代码块解决上述卖票中的线程安全问题。
 */
class SaleTicket implements Runnable {
    int ticket = 100;
    Object obj = new Object();
    Dog dog = new Dog();

    @Override
    public void run() {
//        synchronized (this) {
        while (true) {

            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
//            synchronized (obj){ //obj:是唯一的?yes
//            synchronized (dog){ //dog:是唯一的?yes
            synchronized (this) { //this:是唯一的?yes,就是题目中的s

                if (ticket > 0) {

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;

                } else {
                    break;
                }
            }
        }

    }
}

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

        SaleTicket s = new SaleTicket();

        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        Thread t3 = new Thread(s);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Dog {

}

实现Runnable接口同步方法

/**
 * @author Created by xxw on 2023-09-07 11:36
 * @Description 使用同步方法解决实现Runnable接口的线程安全问题。
 */
class SaleTicket1 implements Runnable{
    int ticket = 100;
    boolean isFlag = true;
    @Override
    public void run() {
        while(isFlag){
            show();
        }

    }
    public synchronized void show(){ //此时的同步监视器是:this。此题目中即为s,是唯一的。
        if(ticket > 0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
            ticket--;

        }else{
            isFlag = false;
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {

        SaleTicket1 s = new SaleTicket1();

        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        Thread t3 = new Thread(s);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

举例Thread

继承Thread类同步代码块:

/**
 * @author Created by xxw on 2023-09-07 11:37
 * @Description 使用继承Thread类的方式,实现卖票
 * 使用同步代码块的方式解决线程安全问题。
 */
class Window extends Thread {
    static int ticket = 100;
    static Object obj = new Object();

    @Override
    public void run() {

        while (true) {
//            synchronized (this) { //this:此时表示w1,w2,w2。不能保证锁的唯一性。
//            synchronized (obj) { //obj: 使用static修饰以后,就能保证其唯一性。
            synchronized (Window.class) { // 结构:Class clz = Window.class,是唯一的。
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

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

        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();

    }
}

继承Thread类同步方法:

/**
 * @author Created by xxw on 2023-09-07 11:38
 * @Description 使用同步方法解决继承Thread类中的线程安全问题。
 */
class Window1 extends Thread {
    static int ticket = 100;
    static Object obj = new Object();
    static boolean isFlag = true;

    @Override
    public void run() {
        while (isFlag) {
            show();
        }
    }
//    public synchronized void show(){ //此时同步监视器:this。此题目中this:w1,w2,w3,仍然是线程不安全的。
    public static synchronized void show(){ //此时同步监视器:当前类本身,即为Window1.class,是唯一的。
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
            ticket--;
        } else {
            isFlag = false;
        }
    }
}

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

        Window1 w1 = new Window1();
        Window1 w2 = new Window1();
        Window1 w3 = new Window1();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}

5. 同步机制相关的问题

懒汉式的线程安全的写法

/**
 * @author Created by xxw on 2023-09-07 11:41
 * @Description 实现线程安全的懒汉式
 */
public class BankTest {
    static Bank b1 = null;
    static Bank b2 = null;

    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                b1 = Bank.getInstance();
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                b2 = Bank.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1 == b2);
    }
}

class Bank{

    private Bank(){}

    private static volatile Bank instance = null;

    //实现线程安全的方式1
//    public static synchronized Bank getInstance(){ //同步监视器,默认为Bank.class
//        if(instance == null){
//
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//
//            instance = new Bank();
//        }
//        return instance;
//    }
//    //实现线程安全的方式2
//    public static Bank getInstance(){
//        synchronized (Bank.class) {
//            if(instance == null){
//
//                try {
//                    Thread.sleep(1000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//
//                instance = new Bank();
//            }
//
//        }
//        return instance;
//    }

    //实现线程安全的方式3:相较于方式1和方式2来讲,效率更高。
    // 为了避免出现指令重排,需要将instance声明为volatile
    public static Bank getInstance(){
        if(instance == null) {
            synchronized (Bank.class) {
                //这里是为了线程安全进行判断
                if (instance == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

同步机制会带来的问题:死锁

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

死锁产生的条件及规避方式

诱发死锁的原因?
  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

以上4个条件,同时出现就会触发死锁。

如何避免死锁?

针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。

针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

锁Lock的使用

  • 步骤:
步骤1. 创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
    private static final ReentrantLock lock = new ReentrantLock();
步骤2. 执行lock()方法,锁定对共享资源的调用
	lock.lock();
步骤3. unlock()的调用,释放对共享数据的锁定
	lock.unlock();
  • 测试:
/**
 * @author Created by xxw on 2023-09-07 11:47
 * @Description 使用继承Thread类的方式,实现卖票
 *               锁Lock的使用
 */
class Window extends Thread{
    static int ticket = 100;
    //1. 创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
    private static final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true){
            try{
                //2. 执行lock()方法,锁定对共享资源的调用
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;

                }else{
                    break;
                }
            }finally{
                //3. unlock()的调用,释放对共享数据的锁定
                lock.unlock();
            }
        }
    }
}

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

        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();

    }
}

synchronized同步的方式与Lock的对比?

synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后释放对同步监视器的调用。

Lock是通过两个方法控制需要被同步的代码,更灵活一些。

Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高

6. 线程间的通信

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

涉及到三个方法的使用:

  • wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
  • notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。
    • (如果被wait()的多个线程的优先级相同,则随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

此三个方法的使用,必须是在同步代码块同步方法中。

(Lock需要配合Condition实现线程间的通信)

此三个方法的调用者,必须是同步监视器。否则,会报IllegalMonitorStateException异常

此三个方法声 明在Object类中。

wait() 和 sleep()的区别?

相同点:

一旦执行,当前线程都会进入阻塞状态

不同点:

声明的位置:

  • wait():声明在Object类中
  • sleep():声明在Thread类中,静态的

使用的场景不同:

  • wait():只能使用在同步代码块或同步方法中
  • sleep():可以在任何需要使用的场景

使用在同步代码块或同步方法中:

  • wait():一旦执行,会释放同步监视器
  • sleep():一旦执行,不会释放同步监视器

结束阻塞的方式:

  • wait(): 到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞
  • sleep(): 到达指定时间自动结束阻塞

举例:

使用两个线程打印 1-100。线程1, 线程2 交替打印

/**
 * @author Created by xxw on 2023-09-07 11:53
 * @Description 使用两个线程打印 1-100。线程1, 线程2 交替打印
 */
class PrintNumber implements Runnable{

    private int number = 1;
    Object obj = new Object();
    @Override
    public void run() {
        while(true){

//            synchronized (this) {
            synchronized (obj) {
                obj.notify();

                if(number <= 100){
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        obj.wait(); //线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }

    }
}

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

        PrintNumber p = new PrintNumber();

        Thread t1 = new Thread(p,"线程1");
        Thread t2 = new Thread(p,"线程2");


        t1.start();
        t2.start();

    }
}

生产者&消费者

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

/**
 * @author Created by xxw on 2023-09-07 11:54
 * @Description
 *        生产者&消费者
 *        生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有
 *        固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品
 *        了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来
 *        取走产品。
 *
 *        分析:
 *        1. 是否是多线程问题? 是,生产者、消费者
 *        2. 是否有共享数据?有! 共享数据是:产品
 *        3. 是否有线程安全问题? 有!因为有共享数据
 *        4. 是否需要处理线程安全问题?是! 如何处理?使用同步机制
 *        5. 是否存在线程间的通信? 存在。
 */
class Clerk{ //店员

    private int productNum = 0;//产品的数量

    //增加产品数量的方法
    public synchronized void addProduct(){

        if(productNum >= 20){
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            productNum++;
            System.out.println(Thread.currentThread().getName() + "生产了第" + productNum + "个产品");
            //唤醒
            notifyAll();
        }
    }

    //减少产品数量的方法
    public synchronized void minusProduct(){
        if(productNum <= 0){
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.println(Thread.currentThread().getName() + "消费了第" + productNum + "个产品");
            productNum--;
            //唤醒
            notifyAll();
        }



    }
}


class Producer extends Thread{  //生产者

    private Clerk clerk;

    public Producer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {

        while(true){
            System.out.println("生产者开始生产产品...");

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.addProduct();

        }

    }
}
class Consumer extends Thread{ //消费者
    private Clerk clerk;

    public Consumer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while(true){
            System.out.println("消费者开始消费产品...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.minusProduct();
        }

    }
}

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

        Clerk clerk = new Clerk();

        Producer pro1 = new Producer(clerk);
        Consumer con1 = new Consumer(clerk);
        Consumer con2 = new Consumer(clerk);

        pro1.setName("生产者1");
        con1.setName("消费者1");
        con2.setName("消费者2");

        pro1.start();
        con1.start();
        con2.start();
    }
}