多线程【java全端课】

72 阅读11分钟

一、多线程(重点和难点)

1.1 什么是多线程?

1、程序(program)、进程(process)、线程(thread)

程序是未运行状态。进程是程序的运行时的状态。线程是进程中的其中1条执行路径。

2、串行(serial)、并行(parallel)、并发(concurrent)

1.2 Java中如何创建和启动多线程

Java中一共提供了4种方式来创建和启动多线程。JavaSE阶段只先介绍2种,在高级部分再介绍另外2种。

如果我们没有单独再开启其他线程,Java程序也有一个线程,是main线程,即主线程。现在希望创建和开启main线程之外的线程,与main线程是“同时”执行的关系,并发的关系。

1.2.1 继承Thread类

步骤:

  • 编写一个类,可以是有名字的类,也可以是匿名的类,继承Thread类

    • 如果这个线程类的代码比较简洁,而且这个线程类的对象只有1个,那么通常使用匿名的类就可以
    • 如果这个线程类的代码比较复杂,而且这个线程类的对象需要创建多个,那么请用有名的类
  • 必须重写public void run()方法

    • 在run方法中编写,你这个线程需要完成的任务代码
  • 创建这个线程类的对象

  • 启动线程,调用线程类对象的start方法

1.2.2 实现Runnable接口

Java中类有单继承的限制。有时候,需要使用实现接口的方式来创建多线程。

步骤:

  • 让线程类实现java.lang.Runnable接口
  • 必须重写public void run()方法
    • 在run方法中编写,你这个线程需要完成的任务代码
  • 创建接口的实现类的对象
  • 创建一个Thread类的对象,让它帮我们代理一下这个接口的实现类的对象
  • 调用Thread类的start方法
package com.mytest.thread;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //例如:打印1-10的偶数
        for(int i=2; i<=10; i+=2){
            System.out.println("偶数:" + i);
        }
    }
}

package com.mytest.thread;

public class TestMyRunnable {
    public static void main(String[] args) {
        MyRunnable m = new MyRunnable();
        //m.start();//MyRunnable的父类没有start方法,它自己也没有start方法

        Thread t = new Thread(m);//m是被代理的,t是代理
        t.start();//当t线程被启动后,CPU会调用t的run方法,然后t的run再调用m的run方法
        /*
        下面是Thread类的run方法源码:
         public void run() {
            if (target != null) {
                target.run();
            }
        }
        这里的target就是我们创建t对象时,传入的m对象。
         */

        //打印1-10的奇数
        for(int i=1; i<=10; i+=2){
            System.out.println("奇数:" + i);
        }
    }
}

1.2.3 问题答疑

1、使用JUnit和main方法来测试多线程的区别
  • JUnit的test方法自己的代码执行完,就退出JVM了,不会管其他线程是否执行完
  • main方法如果自己的代码执行完了,也不会立刻退出JVM,会等其他线程执行完
2、如果需要多个线程,是不是每一个线程都要弄一个独立的类?
  • 如果多个线程的事情是一样的,那么类只需要1个,但是对象可以创建多个。
  • 如果多个线程的事情不一样,那么需要每个任务单独写一个类。

1.3 Thread类的方法

1.3.1 方法系列1

  • String getName():获取线程名称
    • 默认的线程名称,Thread-下标
  • void setName(新名称):修改线程名字
  • int getPriority():获取优先级
  • void setPriority(优先级):设置优先级
    • 优先级高的线程的几率更大,但是不代表优先级低完全没机会。
    • 如果要设置优先级,必须在 MIN_PRIORITY 到 MAX_PRIORITY 范围内,否则会发生IllegalArgumentException非法参数异常。
    • MIN_PRIORITY:1
    • MAX_PRIORITY :10
    • NORM_PRIORITY:5
  • static Thread currentThread() :可以在任意方法中通过Thread.currentThread()调用这个方法,至于获取的是哪个线程对象,就要看是哪个线程在执行这句代码。

1.3.2 方法系列2

1、sleep

static void sleep(long millis):用于让当前线程(执行这句语句的线程)进入休眠状态。时间单位是毫秒。

2、yield

static void yield():用于让当前线程暂停一下,让出CPU,重新加入抢夺CPU的队伍。

3、join

public final void join():让调用这个方法的线程“挤到”当前线程(执行这句代码的线程)之前,阻塞当前线程。当前线程只能等待,等待“挤”进来的线程执行完才能继续。

public final void join(long millis):让调用这个方法的线程“挤到”当前线程(执行这句代码的线程)之前,阻塞当前线程。当前线程只能等待,等待一段时间,时间到了才能继续。

4、stop

stop:早期有这个方法,但是后面发现有问题,现在废弃了,不推荐使用了。

那么如何优雅地停止线程?

可以使用变量来控制线程的执行,通常用boolean 类型的flag变量。

5、interrupt

interrupt:中断线程。但是它只对线程的休眠sleep、等待wait、加塞join等方法有效果。某个线程被中断会发生InterruptedException线程中断异常,它是编译时异常,必须try-catch处理。或者当前方法不处理,又能throws的话,也可以抛给调用者处理。大部分都是try-catch。

package com.mytest.thread;

public class TestThreadMethod {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    for (int i = 1; i <= 1000; i++) {
                        Thread.sleep(10);
                        System.out.println(i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //如果try-catch在循环里面,那么中断只影响单次的循环
                //如果try-catch在循环外面,那么中断会影响整个循环
            }
        };
        t.start();//开启

        //下面是主线程代码
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();//中断t线程
    }
}

1.3.3 方法系列3

void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。

守护线程是指为其他线程服务的后台线程。例如:JVM中的GC线程等。这种线程不会独立存在。当非守护线程结束后,守护线程会自动结束。

1.4 线程安全

1.4.1 什么是线程安全问题?

1.4.3 解决线程不安全问题

方向:加锁

JavaSE阶段:同步锁 synchronized

1、如何使用同步锁

同步锁的使用方式有2种:

【修饰符】 class 类名{
    【其他修饰符】 synchronized  返回值类型 方法名(形参列表){ //同步方法
        需要加锁的语句代码;
    }
}
【修饰符】 class 类名{
    【其他修饰符】  返回值类型 方法名(形参列表){ 
        
        //语句
        
        synchronized(监视器对象){//同步代码块
            需要加锁的语句代码;
        }
        
        //语句
        
    }
}
2、同步锁的原理

Java中每一个对象都可以作为线程的监视器对象。比喻:每一个人都可以当厕所所长。

Java的对象结构分为3个部分:

  • 对象头:包含对象所有的类指针,锁标记,GC标记等等
    • 其中一个锁标记,就可以用于表示当前对象是不是被某个线程给“占用”
    • 比喻:谁要进入厕所,那么先要征得所长的同意,让所长拿着你的名字牌,表示厕所中有人了,其他人只能等待,等到你用完厕所。用完厕所的人,要从所长那里把名字牌拿走。
    • 对应Java的话,就是某个线程要进入synchronized标记的方法或代码块执行之前,先要占用这个监视器对象的锁标记位。其他线程只能等待,等待这个线程执行完这段代码为止。才继续抢这个监视器对象。执行完同步方法或同步代码块的线程,要清除标记位。
  • 实例变量数据区:对象的属性
  • 填充空白:可选,不是所有对象都有。当整个对象占用的内存不是字节的整数倍,那么会填充一些空白凑够字节的整数倍,例如:boolean类型的值占1位
3、如何选择同步锁对象
  • 具有竞争关系的多个线程,选择**“同一个”**同步锁对象即可。对象的类型不重要。
  • 必须是一个对象。
  • 特别是this对象,以及非静态方法要小心,同步锁对象是不是同一个,要判断清楚

对于同步方法来说,同步锁对象是默认的,非静态方法是this对象,静态方法是当前类的Class对象。

4、同步范围的选择问题
  • 同步范围太大,会导致其他线程没有机会
  • 同步范围太小,线程安全问题不彻底

静态方法去掉static为啥就不安全了?

  • 方法是非静态的,那么它的默认锁对象/监视器对象是this
  • 方法是静态的,那么它的默认锁对象/监视器对象是当类的Class对象,即TicketThreadOne.class
5、如何判断自己的线程代码有没有安全问题?
  • 多个线程
  • 共享资源:可以是同一个变量,同一个对象,同一个文件,同一个打印机等
  • 多个线程对这些共享数据有写/修改操作

只要上面的3个条件同时满足了,就一定有安全问题。就一定要加synchronized或 juc中的锁。

2.5 单例设计模式

1、什么是设计模式?

大量程序员在编写代码解决问题的过程中,总结出来的一套一套的解决方案。

针对不同的问题,通常会有固定的解决方案的模板。

常见的设计模式有23种。设计模式在不同的语言之间有通用性。

在Java中,具体的设计模式的实现代码与其他语言有区别。因为不同的语言的语法毕竟是不同的。但是理论是相同的。

2、什么是单例设计模式?

为了保证某个类的对象在整个程序运行期间,只有唯一的1个。不允许出现第2个。

要解决这个问题,咱们在设计这个类的时候,必须有一套模板,规范,要求。这些模板就是单例设计模式。

3、单例设计模式的分类

写法有很多种,大致可以分为2大类:

(1)饿汉式单例设计模式:饥不择食,着急。这个类的对象在类初始化时,就提前创建好了。不管你现在用不用这个对象。

(2)懒汉式单例设计模式:拖延。这个类的对象必须等到别人第一次来明确获取这个对象了,才new。

饿汉式的写法1:

public enum SingleOne {
    INSTANCE //这个对象名自己取就可以,不一定非得是INSTANCE
}

饿汉式的写法2:

public class SingleTwo {
    //这里final可选,但是public和static必选的,static表示共享同一个,public外面可以获取到
    public static final SingleTwo INSTANCE = new SingleTwo();
    private SingleTwo(){//构造器私有化

    }
}

饿汉式的写法3:

public class SingleThree {
    private static final SingleThree INSTANCE = new SingleThree();
    private SingleThree(){//构造器私有化

    }
    public static SingleThree getInstance(){
        return INSTANCE;
    }
}

懒汉式的写法1:

package com.mytest.single;

public class SingleFour {
    private SingleFour(){//构造器私有化
      
    }

    public static SingleFour getInstance(){
        return Inner.INSTANCE;//INSTANCE的类型是SingleFour
    }

    private static class Inner{//静态内部类
        private static SingleFour INSTANCE = new SingleFour();
    }
}

懒汉式的写法2:

package com.mytest.single;

public class SingleFive {
    private static SingleFive instance;

    private SingleFive(){//构造器私有化

    }

    public static synchronized SingleFive getInstance(){
        if(instance == null){
            instance = new SingleFive();
        }
        return instance;
    }
/*    public static SingleFive getInstance(){
        //SingleFive.class代表当前类的Class对象,它是共享的
        synchronized (SingleFive.class) {
            if (instance == null) {
                instance = new SingleFive();
            }
            return instance;
        }
    }*/

    /*public static SingleFive getInstance(){
        //SingleFive.class代表当前类的Class对象,它是共享的
        if(instance == null) {
            synchronized (SingleFive.class) {
                if (instance == null) {
                    instance = new SingleFive();
                }
            }
        }
        return instance;
    }*/
}

1.6 生产者与消费者问题

1.6.1 什么是生产者与消费者问题?

当一个或一些线程负责往“共享数据区”填充/增加数据,这个/些线程被称为生产者线程,

另一个或一些线程负责从“共享数据区”消耗/减少数据,这个/些线程被称为消费者线程。

当它俩/它们“一起”工作时可能出现:

  • 线程安全问题:解决办法是加synchronized
  • 线程的协作问题:
    • 当“共享数据区”空的时候,消费者线程应该停下来“等待”,生产者可以工作。消费者线程需要等待别人“唤醒”它或指定等待时间。
    • 当“共享数据区”满的时候,生产者线程应该停下来“等待”,消费者可以工作。生产者线程需要等待别人“唤醒”它或指定等待时间。
    • 所以解决办法就是 等待与唤醒机制。JavaSE阶段用的是 wait() 和notify() /notifyAll()。这些方法定义在了Object类中。
    • 而且它们都必须由 同步锁对象/监视器对象来调用,不能由别人调用。