多线程理解

一、进程与线程

1.1 概念

程序是机器上安装的软件,是一个静止的内容。

当程序被启动,就会产生至少一个进程。一般情况下,一个程序产生一个进程,但有些特殊用途的程序进行时会产生多个进程。

在一个进程中,可以创建多个任务同时进行,这些任务称为线程,是一种轻量级进程。当这些线程同时执行(交替执行),称为多线程。

1.2 理解

线程是同时执行还是交替执行?

线程是利用cpu空闲时间交替执行,由于交替执行时间短,看起来像同时执行。

在现在的电脑上是同时执行还是交替执行?

现在电脑并非单核,单核cpu执行多线程都是交替执行,但多核意味着多个cpu,也就可做到同时执行。

1.3 线程和进程的区别

  • 一个程序至少一个进程。
  • 一个进程至少一个线程,可包含多个线程。
  • 进程是系统分配资源的基本单位,而线程是cpu调度的单位。
  • 进程之间一般不能共享数据,但线程之间可以共享数据。

二、线程的创建

2.1 线程的组成

  • cpu的时间片:每个线程在执行时都需要cpu分配时间;
  • 运行数据:
    • 堆空间数据。共享数据
    • 栈空间数据。一般是临时变量,线程中有独立空间未保存。
  • 逻辑代码

2.2 线程的创建

三种方法创建

  • 继承Thread类
    • 需要重写run方法
    • 然后创建该类的对象
    • 执行start()开始执行线程
  • 实现Runnable(任务)接口
    • 需重写run方法
    • 创建该类对象
    • 再创建Thread类对象
    • 执行start()开始执行线程
  • Callable和FutureTask(可以得到线程的返回值)
    • 得到任务对象
      • 定义实现Collable接口,重写call方法,封装其要做的事情
      • 用FutureTask把Callable对象封装成线程任务对象
    • 把线程任务对象交给Thread处理
    • 调用Thread的start方法启动线程,执行任务
    • 线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果

Runnable创建线程较麻烦,那么Thread和Runnable的区别在哪?

  • 继承Thread类的使用简单,而实现接口后还是要创建Thread类对象,使用相对复杂;
  • 继承类后不能再继承其他类,而实现接口后还可以继承其他类,使用相对灵活;
  • 继承类后,中间代码可能复用,而实现接口后,逻辑代码还可以复用;
  • 两种创建线程的如果有执行结果,是不能直接返回

在这里插入图片描述

正常情况,当我们没有创建线程时,下面代码执行顺序为先打印100次A然后再打印100次B,顺序执行

public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
        System.out.println(i + "...A");
    }
    for (int i = 0; i < 100; i++) {
        System.out.println(i + "...B");
    }
}

当我们想要使其交替执行,则将业务代码创建为线程,使其交替执行,交替执行的执行时间是随机的,可能A抢占10次时间片打印10次,然后B抢占5次时间片,打印5次,然后又是A等等,抢占时间随机,抢占顺序随机(无规律)。

//使用继承Thread类的方式创建
public static void main(String[] args) {
    //创建线程
    Thread1 th1 = new Thread1();
    Thread2 th2 = new Thread2();
    //启动线程
    th1.start();
    th2.start();
}

public class Thread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...A");
        }
    }
}

public class Thread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...B");
        }
    }
}
//实现Runnable接口创建线程任务
public static void main(String[] args) {
    //创建线程任务
    Runnable1 run1 = new Runnable1();
    Runnable2 run2 = new Runnable2();
    //创建Thread对象(传入线程任务)
    Thread th1 = new Thread(run1);
    Thread th2 = new Thread(run2);
    //启动线程
    th1.start();
    th2.start();
}

public class Runnable1 implements Runnable {
    //实现接口也需要实现其run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...A");
        }
    }
}

public class Runnable2 implements Runnable {
    //实现接口也需要实现其run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...B");
        }
    }
}
public class thread {
    public static void main(String[] args) {
        //创建任务对象,求1-100结果
        Callable<String> call = new MyCallable(100);
        //将任务对象封装成线程任务对象
        //此处使用FutureTask的原因1:FutureTask是Runnable的子类,可以将任务对象封装成线程任务对象
        //此处使用FutureTask的原因2:可以调用FutureTask的get方法去获取线程的返回值,get方法可以等待线程执行结束后再获取结果
        FutureTask<String> ft = new FutureTask(call);
        //创建Thread线程对象
        Thread t1 = new Thread(ft);
        //启动线程
        t1.start();

        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> ft2 = new FutureTask(call2);
        Thread t2 = new Thread(ft2);
        t2.start();

        try {
            //使用get方法来获取结果,当主程序运行到这里时,get方法去获取对象,如果线程还没运行结束,那么就会等待线程执行结束
            String s = ft.get();
            System.out.println("求和结果为:" + s);//4950
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            String s = ft2.get();
            System.out.println("求和结果为:" + s);//19900
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//类要实现Callable接口,且要重写call方法
class MyCallable implements Callable<String>{
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        //线程用来求1-n的和,并且返回结果
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return "求和结果为:" + sum;
    }
}

2.3 经典面试题

start()和run()的区别?

  • 直接调用run()是直接将线程类中的业务逻辑代码执行,等同于一个类,创建该类对象,使用其方法。根本没有使用线程相关内容,没有创建多的线程。
  • 当调用start方法时,自身进入就绪状态,等待抢占cpu的执行时间,进而执行run()中的业务内容。

当程序启动时,会自动创建一个进程,该进程中有默认的一个进线程,此线程名称为main(主线程)。

三、线程的状态

3.1 基本状态

  • 新建:创建Thread对象,与普通创建对象没有区别;
  • 就绪:调用start方法进入就绪状态,等待系统分配时间片;
  • 运行:抢占到时间片后,运行run方法中的业务代码,如果业务代码没有执行完毕,但是时间片到了,就会进入就绪状态,等待下一次分配时间片。
  • 终止:业务代码执行结束或main执行结束

线程饿死:一个线程一直没有抢占到时间片,无法执行。

3.2 常见方法

3.2.1 sleep 休眠

sleep指让当前进程主动进入休眠,退出抢占时间片,直到休眠结束再抢占时间片,单位为毫秒。

一旦进入休眠状态,其他进程会优先抢占时间片。

public static void main(String[] args) {
    //创建
    Thread1 th1 = new Thread1();
    Thread2 th2 = new Thread2();

    //启动
    th1.start();
    th2.start();
}

public class Thread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...A");
            //当A打印到20时,休眠5000毫秒后再打印,也就是五秒,需要处理异常
            if (i == 20){
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

public class Thread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...B");
        }
    }
}
3.2.2 yield 放弃

yield指让当前进程放弃这一次时间片抢夺,直接进入就绪状态,竞争下一次时间片。

注意:yield只是放弃这一次抢夺,并不能保证下一次抢夺不会优先。例如:当A放弃 本轮抢夺没有打印后进入下一次竞争,但是下一次竞争A又抢到了,然后打印,所以看似没有放弃,实则放弃一次后又抢到了打印的。

public static void main(String[] args) {
    //创建
    Thread1 th1 = new Thread1();
    Thread2 th2 = new Thread2();

    //启动
    th1.start();
    th2.start();
}

public class Thread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...A");
            //当A打印到20时,放弃本次争抢时间片,直接进行下次争抢
            if (i == 20){
                Thread.yield();
            }
        }
    }
}

public class Thread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "...B");
        }
    }
}
3.2.3 join 合并

join允许其他线程加入当前线程中,加入后需要将加入的线程执行完毕后才会继续执行当前线程。

public static void main(String[] args) {
    Runnable run1 = new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(i + "...A");
            }
        }
    };

    //创建
    Thread th1 = new Thread(run1);

    Runnable run2 = new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(i + "...B");
                //当B打印到20时就将th1线程合并进来,需要处理异常
                if (i == 20){
                    try {
                        th1.join();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    };
    //创建
    Thread th2 = new Thread(run2);

    //启动
    th1.start();
    th2.start();
3.2.4 获取线程名
//哪个线程运行就是获取哪个线程名称
Thread.currentThread().getName()

3.3 线程等待

等待:线程进入等待状态,等待结束后进入就绪状态。

  • 当线程中使用sleep后,进入限时等待,时间结束进入就绪状态。
  • 当线程中使用join后,进入不限时等待,直到进来的线程执行完毕后才进入就绪状态。
  • 当线程中使用wait后,进入了等待,直到被唤醒或等待超时,才进入就绪状态。

3.4 线程安全

当多线程同时访问共享资源时,如果破坏了原子操作,可能会出现线程不安全问题。

条件:

  • 多线程访问
  • 修改同一资源

例如:当我们有10套房子且在不分房子的情况下让三个人卖房子,可能会导致卖时比规定的10套要多出,因为房子只有交付出去了其他人才会知道,若此时有俩人同时卖了同一套房子,那么这套房子该给谁呢?这就出现了线程不安全。

//可能会多卖房子,并不是一定会出现问题
public static void main(String[] args) {
    //创建三个人(多线程)
    Persion zs = new Persion("张三");
    Persion ls = new Persion("李四");
    Persion ww = new Persion("王五");

    //启动多线程一起卖房子
    zs.start();
    ls.start();
    ww.start();
}

//创建Persion类并且继承Thread类
public class Persion extends Thread {

    //共同房源,十套
    private static Integer house = 10;
    //定义name属性
    private String name;

    //使用有参构造赋值
    public Persion(String name) {
        this.name = name;
    }

    //重写run方法
    @Override
    public void run() {
        while (house > 0) {
            //卖房子
            house--;
            System.out.println(name + "卖出一套房子,还有" + house + "套房子");
        }
    }
}
3.4.1 解决方法

线程同步(加锁)解决

锁对象的规范要求:

  • 建议使用共享资源作为锁对象
  • 对于实例方法建议使用this作为锁对象
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象

语法:使用synchronize关键字

  • synchronized同步代码块
  • synchronized同步方法
//synchronized同步代码块解决方法
//创建Persion类并且继承Thread类
public class Persion extends Thread {

 //同上省略

 //重写run方法
 @Override
 public void run() {
     while (house > 0) {
         //synchronized(填写公共属性,例如此处填写三个人一起卖的房子house)
         //相当于加锁,当有一个人在卖房子的时候,其他人都不能进入该代码块卖房子
         synchronized (house){
             //加入判断,防止线程在循环的house>0判断完成后,进入代码块后时间片用完重新竞争时间片执行,但此时house已经卖完
             //加入判断再次确认house是否卖完,然后让其无法卖
             if (house>0){
                 //卖房子
                 house--;
                 System.out.println(name + "卖出一套房子,还有" + house + "套房子");
             }
         }
     }
 }
}

3.5 线程阻塞

阻塞:当线程运行过程中,遇到加锁的代码,需要去获取锁,在没有获取时进入阻塞状态,需等待持有锁的线程将加锁代码执行结束后才能继续执行。

四、死锁

当一个线程持有锁A,等待锁B,另一个线程持有锁B,等待锁A,两个线程都不会释放锁,此时也无法获取到另一把锁,产生死锁。

死锁的根本成因:获取锁的顺序不一致导致

简单的顺序锁解决方法:

  • 让每个线程获取锁的顺序都是一样的,都去先获取A钥匙,再获取B钥匙,那么就能解决死锁的问题了。
  • 设置等待锁的时间,当超过这个时间后就释放当前锁让其他线程能够使用,然后再重新获取锁。
//例如boy和girl想要打开同一扇门,此门只能进入一个人,但是这扇门有两把钥匙,此时boy和girl一人一把,两人都想进入这扇门,所以两人都不想让步给对方钥匙,那么此时就进入了死锁。
public static void main(String[] args) {
    Thread boy = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (chops.A){
                System.out.println("boy抢到了A钥匙");
                System.out.println("等待B钥匙");
                synchronized (chops.B){
                    System.out.println("boy又抢到了B钥匙");
                    System.out.println("抢到了两把钥匙,打开门");
                }
            }
        }
    });

    Thread girl = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (chops.B){
                System.out.println("girl抢到了B钥匙");
                System.out.println("等待A钥匙");
                synchronized (chops.A){
                    System.out.println("girl又抢到了A钥匙");
                    System.out.println("抢到了两把钥匙,打开门");
                }
            }
        }
    });

    boy.start();
    girl.start();
}


//创建一个接口定义两个公共的静态对象
public interface chops {
    Object A = new Object();
    Object B = new Object();
}

五、线程通信

线程通信就像生活中:家里父亲和母亲包饺子,父亲擀面皮,母亲包饺子,因为父亲擀面皮速度快,不一会将桌子摆满了,父亲就说我先wait会,待会包完了notify我,我再来接着擀面皮。

使用wait方法让线程进入等待状态,wait只能在synchronized中使用,且synchronized和wait的对象应该一致。

使用notify或者notifyAll唤醒线程,进入就绪状态。

格式:当用什么对象wait后,就用什么对象去notify,wait还可以设置时间,表示当等待多长时间后放弃等待。

使用wait等待是无限期等待,需要唤醒或者超时。

  • 唤醒:指使用notify或者notifAll
  • 超时:指在指定时间内,如果没有唤醒,那么就放弃等待。
//利用wait解决男女生开门死锁问题,当Boy抢到了B钥匙后主动把钥匙交出来让Girl抢,然后等Girl打开门后钥匙空出来,再叫Boy抢A钥匙。这样就不会发生死锁问题

public class Demo1 {
    public static void main(String[] args) {
        //创建线程
        Boy boy = new Boy();
        Girl girl = new Girl();
        //运行线程
        boy.start();
        girl.start();
    }
}

public class Girl extends Thread {

    @Override
    public void run() {
        synchronized (A.a){
            System.out.println("Girl抢到了A钥匙");
            System.out.println("准备抢B钥匙");
            synchronized (A.b){
                System.out.println("Girl抢到了B钥匙");
                System.out.println("Girl打开门");
                A.b.notify();
            }
        }
    }
}

public class Boy extends Thread{
    @Override
    public void run() {
        synchronized (A.b){
            System.out.println("Boy抢到了B钥匙");
            System.out.println("准备抢A钥匙");
            try {
                A.b.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (A.a){
                System.out.println("Boy抢到了A钥匙");
                System.out.println("Boy打开门");
            }
        }
    }
}

public interface A {
     Object a = new Object();
     Object b = new Object();
}

注意:如果有多次线程同时使用某个锁对象进行wait状态,那么一次notify方法调用只会随机唤醒一个,需要多次调用notify方法,此时,可以使用notifyAll一次唤醒所有的进入wait状态的线程。

经典面试题:

sleep与wait的区别:

  • sleep需要指定时间,时间到了会自动醒来,而wait如果没有指定时间,会无限期等待,直到被唤醒为止。
  • sleep是一个静态方法,而且不需要在同步时调用,而wait是一个对象方法,需要在同步时使用。
  • **sleep在休眠时不会释放锁,而wait会释放锁。**

六、生产消费模式

6.1 设计模式

设计模式是指前人经验的总结,且经过长期的时间验证行之有效的方案,将在项目中可能遇到的问题进行分类总结,并找到其对应的解决方案。

二十三中设计模式:常见的二十三种问题的解决方案。

分为三大类:

  • 创建型模式:创建对象的不同方式。(5种)单例模式,工厂模式、原型模式等。
  • 结构型模式:多个对象形成一种新的结构。(7种)桥接模式、适配器模式、装饰模式。
  • 行为型模式:多个对象之间相互作用。(11种)命令模式、监听者模式、调停者模式、迭代模式。

注意:软件发展到现在,遇到的问题不止23种,所以现在有很多新的设计模式出现,不在23种设计模式中,例如:MVC模式

6.2 生产消费者模式

若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品存入缓冲区,消费者从缓冲区取走产品去消费,显然生产者和消费者之间必须保持同步,即不用需消费者到空的缓冲区取产品,生产者不允许向满的缓冲区存放产品。

面试题:

synchronized代码块和synchronized方法的区别?

  • synchronized方法表示方法中的所有代码块都被同步;
  • synchronized方法加锁对象是调用该方法的对象,所以静态方法所得对象是类名.class,非静态方法所得对象是this
  • synchronized代码块可以指定加锁哪部分代码,性能优一点;
  • synchronized代码块要指定加锁对象。

例如:4S店(消费者) 、仓库(缓冲区)、 产车工厂(生产者)

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

        //4S店和工厂都在同一个仓库in和out,所以用final修饰不可变
        final WareHouse wareHouse = new WareHouse();

        //生产消费者模式 4S店(消费者) 仓库(产品缓冲区) 工厂(消费者)
        Factor f1 = new Factor(wareHouse);
        Factor f2 = new Factor(wareHouse);

        fourS cs1 = new fourS(wareHouse);
        fourS cs2 = new fourS(wareHouse);
        fourS cs3 = new fourS(wareHouse);

        //启动线程
        f1.start();
        f2.start();
        cs1.start();
        cs2.start();
        cs3.start();

    }
}

//小车对象
public class Car {

    //定义id和name,便于打印观察个数
    private int id;
    private String name;

    public Car(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Car{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

//仓库
public class WareHouse {

    //定义仓库大小,以及仓库已经存放的个数
    private static Car[] Car = new Car[6];
    private static int count = 0;

    //工厂生产Car放入仓库,使用synchronized修饰方法,
    public synchronized void in(Car car) {
        //当仓库中的Car个数>=6时,表示仓库放满了,当前工厂线程进入wait等待
        while (count >= 6){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //当wait被唤醒后往后执行,将生产的车存入仓库数组
        Car[count++] = car;
        System.out.println("生产了一辆车" + car);
        //当生产车后就唤醒4S店去卖车
        this.notifyAll();
    }

    //4S店在仓库消费Car,使用synchronized修饰方法,
    public synchronized Car out() {
        //当仓库中的Car个数<=0时,表示仓库没有Car了,当前4S店线程进入wait等待
        while (count <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //当wait被唤醒后往后执行,将索引靠后的车卖出
        Car car = Car[--count];
        //将卖出车的位置置为null
        Car[count] = null;
        System.out.println("消费了一辆车" + car);
        //当卖出车后就唤醒工厂进行造车
        this.notifyAll();
        return car;
    }
}

//4S店,因为工厂和4S是同时进行的,所以要继承Thread多线程
public class fourS extends Thread{
    private WareHouse wareHouse;

    //有参构造,传入要放入的仓库
    public fourS(WareHouse wareHouse) {
        this.wareHouse = wareHouse;
    }
    //重写run方法,限制每个4S店只能卖10辆车
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //每调用一次out,表示仓库卖出一辆车
            wareHouse.out();
        }
    }
}

//工厂,因为工厂和4S是同时进行的,所以要继承Thread多线程
public class Factor extends Thread{
    private WareHouse wareHouse;

    //有参构造,传入要放入的仓库
    public Factor(WareHouse wareHouse){
        this.wareHouse = wareHouse;
    }
    //重写run方法,限制每个工厂只能生产15辆车
    @Override
    public void run() {
        for (int i = 0; i < 15; i++) {
            //每次生产车需要创建Car对象传入仓库,表示仓库存入一辆车
            Car car = new Car(i,Thread.currentThread().getName());
            wareHouse.in(car);
        }
    }

}

七 、线程终止

让正在执行的线程停止

一般有三种方法

7.1 使用stop

使用stop方法,此方法已经被弃用,stop是强行停止线程,相当于电脑运行时断电,会导致一些隐患。

//Runnable创建线程简写
//理论上girl打印1000次会比boy后执行完
Thread girl = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        System.out.println("girl-" + i);
    }
});

Thread boy = new Thread(() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println("boy---" + i);
        //当boy执行到i=90时,会将girl强制停止
        if (i == 90){
            girl.stop();
        }
    }
});

boy.start();
girl.start();

7.2 自定义标识

在线程运行过程中,定义一个线程运行时需要满足的标识,需要线程停止时,修改标识值,线程就会执行完毕。

//标识符为静态,一般用volatile修饰,后面会学习该修饰符
public static volatile boolean flag = false;
public static void main(String[] args) {
    //Runnable创建线程简写
    //理论上girl打印1000次会比boy后执行完
    Thread girl = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
            System.out.println("girl-" + i);
            //判断标识的值是否被修改,当被修改为true,就会执行return来结束线程
            if (flag){
                return;
            }
        }
    });

    Thread boy = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("boy---" + i);
            //当boy执行到i=90时,会将标识的值修改
            if (i == 90){
                flag = true;
            }
        }
    });

    boy.start();
    girl.start();

}

7.3 使用interrupt

系统对线程中定义的标识,通过改变该标识值来停止线程。

格式:线程对象.interrupt()

被停止对象需要判断Thread.interrupted()

注意:如果线程正在休眠,通过interrupted去终止线程,会出现异常。

解决方法:需要在sleep抛出异常中,将抛出的异常修改为终止命令return即可终止线程。

//Runnable创建线程简写
//理论上girl打印1000次会比boy后执行完
Thread girl = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        System.out.println("girl-" + i);
        //对系统标识进行判断,当被修改就会执行return
        if (Thread.interrupted()){
            return;
        }
        //若线程在休眠时,通过interrupted去终止线程,会出现异常
        //此时只需要将抛出的异常改为return就能正常结束线程
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            //                    e.printStackTrace();
            return;
        }
    }
});

Thread boy = new Thread(() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println("boy---" + i);
        //当boy执行到i=90时,会修改系统标识
        if (i == 90){
            girl.interrupt();
        }
    }
});

boy.start();
girl.start();

八、线程的优先级

优先级越高,被CPU分配时间片的概率越高。

优先级最高位10,最低为1,默认为5,可以通过常量设置。

格式:线程对象.setPriority(值);

九、守护线程

守护线程是一个特殊的线程,如果没有其他的线程在运行,守护线程会自动停止。JVM是一个守护线程,当程序执行结束时,JVM会自动停止。

格式:线程名.setDaemon(true);

//Runnable创建线程简写
Thread girl = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        System.out.println("girl-" + i);
    }
});

Thread boy = new Thread(() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println("boy---" + i);
    }
});
//将girl设置为守护线程,若此时没有其他线程正在运行,那么girl也会自动停止
girl.setDaemon(true);
boy.start();
girl.start();

十、volatile关键字的作用

synchronized:

  • 可见性:执行到同步代码块时,如果要访问临界资源,会去获取最新的值。
  • 互斥性:执行到同步代码块时,会持有锁,其他的线程如果要执行同步代码块,会等待前一个线程执行完毕后才去执行同步代码块。

volatile修饰属性时,表示该属性具备有可见性,注意:该关键字并不能解决线程安全问题。因为volatile只符合可见性特点,并没有互斥性特点。

当没有使用volatile关键字,也没有在循环中打印或者休眠时,会发现即使在外的线程中修改了变量的值,该线程也不会停止,那是因为循环中的判断并没有去读取最新的值,一直是用缓存中的值去判断,所以无法停止。

但是加了volatile关键字后,每次使用变量都会去获取最新的值,所以线程能够正常停止。

但是即使没有使用volatile关键字,如果在循环中休眠或者打印后,一样可以实现线程停止效果。注意:因为循环是CPU相对较忙,没有空闲时间进行变量值的更新,但是系统会尽量完成值的更新,所以一旦休眠或打印,对于CPU来说,会比较闲,也就说有足够时间进行变量值的更新。但不推荐这样写,应该使用volatile关键字。

十一 、Lock锁

  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个全新的锁对象Lock,更加灵活,方便。
  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
  • Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象
  • 建议锁对象为final修饰且用static修饰,那么该类就只有这一把锁
构造方法说明
public ReentrantLock()获得Lock锁的实现类对象
方法名称说明
void lock()获得锁
void unlock()释放锁

11.1 公平锁

公平锁创建

        //多态写法,如果创建锁使用有参构造,传入true,则创建一个公平锁
    	private static final Lock reentrantLock = new ReentrantLock(true);
	

11.2 读写锁

    private static final ReadWriteLock reentrantLock = new ReentrantReadWriteLock();
//具体使用可以查看api文档

十二、线程池(重点)

12.1 概述

线程池就是一个可以复用线程的技术。

  • 如果不使用线程池,那么每当用户发起一个请求,后台就会new一个新线程来处理,下次新任务来了又new一个新线程,而创建新线程开销很大,会影响系统性能
  • 如果使用线程池,那么我们可以规定几个线程来处理用户请求(任务队列),当用户多了那么他们就会等待线程空闲了再来处理,相当于线程复用。

任务接口有:Runnable和Callable

12.2 API

12.2.1 构造方法

JDK5.0开始提供线程池接口:ExecutorService,因为接口无法被实例化,所以我们要使用它的实现类TreadPoolExcutor

如何得到线程池对象

  • 方式一:使用ExecutorService的实现类TreadPoolExecutor来创建一个线程池对象。
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。(其实底层还是方式一,只不过帮我们封装了)

TreadPoolExecutor构造器的参数说明:

//ThreadPoolExecutor构造器
public ThreadPoolExecutor(int corePoolSize,//线程池的线程数量(核心线程,不会死掉)不能小于0
                          int maximumPoolSize,//线程池的最大线程数。>=核心线程数量
                          long keepAliveTime,//临时线程存活时间,不能小于0
                          TimeUnit unit,//存活时间的单位
                          BlockingQueue<Runnable> workQueue,//任务队列,不能为null
                          ThreadFactory threadFactory,//生产临时线程工厂,不能为null
                          RejectedExecutionHandler handler)//当核心线程和临时线程都在忙,且任务队列满后,再有新任务的处理方式,不能为null
指定线程池的线程数量(核心线程):corePoolSize不能小于0
指定线程池可支持的最大线程数:maximumPoolSize最大数量>=核心线程数量
指定临时线程的最大存活时间:keepAliveTime不能小于0
指定存活时间的单位(秒分时天):unit时间单位
指定任务队列:workQueue不能为null
指定哪个线程工厂创建线程:ThreadFactory不能为null
指定线程忙,任务满时,新任务来了怎么办:handler不能为null

举例各参数:比如KTV中,招了三个正式工(相当于核心线程数3个),并且老板说当忙时最多只能再招7个临时工(相当于设置最大的线程数为10,那么临时线程就是7个),此时KTV来了三个客户,三个核心线程开始忙,此时又来了五个客户,因为没有服务员来招待客人,所以客户们都坐在KTV外面的板凳上等,板凳只有五个(相当于任务队列设置为5),此时老板没有招临时工,认为前面三个服务员马上就会忙完来招待剩下的客人,没想到此时又来了一个客人,且三个正式工还在忙,板凳也没有坐的了,那么老板就找人力资源(相当于线程工厂创建线程)又招了一个临时工来接待客人,如果又来的客人没地方坐那么又会招一个。(相当于临时线程创建),那么当没有客人了,此时服务员都闲了起来,过了两天都没有来客人,那么老板就开除了临时工。(相当于设置了临时线程存活时间为2天

//创建一个线程池,核心线程数3个,最多4个线程,临时线程存活时间5秒,任务队列为1个,线程工厂,拒绝策略为抛出异常并拒绝(默认策略)
        ExecutorService pools = new ThreadPoolExecutor(3, 4, 5,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + "---->" + i);
                }
            }
        };

        //核心线程处理
        pools.execute(runnable);
        pools.execute(runnable);
        pools.execute(runnable);
        //任务队列
        pools.execute(runnable);
        //创建临时线程(因为核心线程在忙,且任务队列满了,会创建两个临时线程)
        pools.execute(runnable);
        //核心线程和临时线程都在忙,且任务队列满了,就开始拒绝并抛异常
        pools.execute(runnable);
12.2.2 常用方法
void execute(Runnable command)执行任务/命令,没有返回值,一般用来执行Runnable任务
Future submit(Callable task)执行任务,返回未来任务对象获取线程结束,一般拿来执行Callable任务
void shutdown()等任务执行完毕后关闭线程
List shutdownNow()立刻关闭,停止正在执行的任务,并返回队列中未执行的任务
12.2.3 新任务拒绝策略

最后一个参数

ThreadPoolExecutor.AbortPolicy丢弃任务并抛出异常,默认策略
ThreadPoolExecutor.DiscardPolicy丢弃任务,但不抛出异常,不推荐
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待醉酒的任务,把新任务加入队列
ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务run()方法从而绕过线程池直接执行

12.3 常见面试题

临时线程什么时候创建?

  • 新任务来得时候发现核心线程都在忙,且任务队列满了,并且可以创建临时线程是,才会创建临时线程,并不是一次性创建很多临时线程,根据新任务的个数来定,且临时线程也有线程个数根最大线程数挂钩。

什么时候会开始拒绝任务?

  • 当核心线程和临时线程都在忙,任务队列也满了时,才会开始拒绝新任务。

12.4 线程池处理Runnable任务

使用executr方法

public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService pools = new ThreadPoolExecutor(3, 10, 5,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        //创建任务
        MyRunnable runnable = new MyRunnable();

        //核心线程处理,使用execute方法
        pools.execute(runnable);
        pools.execute(runnable);
        pools.execute(runnable);

    }
}

//实现Runnable接口
class MyRunnable implements Runnable{
    //重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "---->" + i);
        }
    }
}

12.5 线程池处理Collable任务

使用summit方法

public class ThreadPool {
    public static void main(String[] args) throws Exception {
        ExecutorService pools = new ThreadPoolExecutor(3, 10, 5,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        //创建任务使用线程池执行,使用summit方法
        //Future是FutureTask的祖父类,所以得到返回值需要调用get()
        Future submit1 = pools.submit(new MyCallable(100));
        Future submit2 = pools.submit(new MyCallable(200));
        Future submit3 = pools.submit(new MyCallable(300));
        Future submit4 = pools.submit(new MyCallable(300));

        //获得线程计算的,这里使用get方法后,他会等待线程执行结束后在返回结果,否则就会一直等待
        System.out.println(submit1.get());
        System.out.println(submit2.get());
        System.out.println(submit3.get());
        System.out.println(submit4.get());

    }
}

//实现Callable接口
class MyCallable implements Callable {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + " 1-" + n + "的和为:" + sum;
    }
}

12.6 Executors工具类实现线程

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象

注意:其实Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的

方法名称说明
public static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间,会被回收掉
public static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程代替他
public static ExecutorService newSingleThreadExecutor()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池就会补充一个新的线程
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务

12.7 Executors弊端

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险
  • 因此建议使用ThreadPoolExector来指定线程池参数,这样可以明确线程池的运行规则,避免资源耗尽风险
方法名称存在问题
public static ExcutorService newFixedThreadPool(int nThreads)允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误
public static ExecutorService newSingleThreadExecutor()允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误
public static ExecutorService newCachedThreadPool()创建的线程数量最大上限是Integer.MAX_VALUE,线程数量可能会随着任务1:1增长,也可能出现OOM错误
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建的线程数量最大上限是Integer.MAX_VALUE,线程数量可能会随着任务1:1增长,也可能出现OOM错误

在这里插入图片描述

十三、定时器

13.1概念

  • 定时器就是一个控制任务延时调用,或者周期调用的技术
  • 作用:闹钟、定时邮件发送

定时器实现方式

  • 一、Timer
  • 二、ScheduledExecutorService

13.2 Timer

Timer定时器的特点和存在的问题

  • Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
  • 可能因为其中某个任务异常使Timer线程死掉,从而影响后续任务执行
构造器说明
public Timer()创建Timer定时器对象
方法说明
public void schedule(TimerTask task,long delay,long period)开启一个定时器,按照计划处理TimerTask任务
        //创建Timer定时器对象
		Timer timer = new Timer();
        //每隔两秒执行一次打印,0表示立即开始,2000表示隔2秒执行一次
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "打印-->AAA 时间" + new Date());
//                如果此处执行的很慢,那么也会影响其他任务的效率(因为Timer是单线程,是顺序执行)
//                try {
//                    Thread.sleep(10000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                //如果此处出现异常,那么其他定时任务也不会执行(因为Timer是单线程,是顺序执行)
//                System.out.println(10 / 0);
            }
        }, 0, 2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "打印-->BBB 时间" + new Date());
            }
        }, 0, 2000);

13.3 ScheduledExecutorService定时器

ScheduledExecutorService是jdk1.5引入的开发包,用来弥补Timer的缺陷,ScheduledExecutorService内部为线程池。

  • ScheduledExcutorService优点:
    • 基于线程池,某个任务的执行情况并不会影响其他定时任务的执行
Executors的方法说明
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)得到线程池对象
ScheduledExecutorService的方法说明
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)周期调度方法

        //线程池中创建3个线程,得到线程池对象
        ScheduledExecutorService pools = Executors.newScheduledThreadPool(3);

        //线程池周期调度scheduleAtFixedRate,此处设置立即开始,且每隔2秒执行一次
        pools.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "-->AAA" + new Date());
                //当该任务执行效率低时,会开启其他线程去执行其他定时器
//                try {
//                    Thread.sleep(20000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                //当该任务导致线程死掉,并不会影响其他定时器,因为还有其他线程执行
//                System.out.println(10 / 0);
            }
        }, 0, 2, TimeUnit.SECONDS);

        pools.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "-->BBB" + new Date());
            }
        }, 0, 2, TimeUnit.SECONDS);

十四、并发与并行,同步与异步

  • 正在运行的程序(软件)就是一个独立的进程,线程属于进程,多个线程其实是并发和并行同时进行的。

什么是并发?

  • CPU同时处理线程的数量有限
  • CPU轮询为系统的每个线程服务,由于CPU切换速度快,给我们感觉这些线程在同时执行,这就是并发

什么是并行?

  • 在同一个时刻上,同时有多个线程在被CPU处理并执行

个人理解:当计算机为单核时,因为cpu切换速度很快,一个核处理一个进程后,快速再去处理另一个进程,我们感觉它是同时执行,这个叫并发;当计算机多核时,例如4核,那么他可以同时执行4个进程,那么这个叫做并行。

同步和异步

  • 同步是指一个线程中顺序执行,需要等待第一个执行完成后继续执行第二个;
  • 异步指多个线程同时执行

十五、线程的状态

线程的状态

  • 就是线程从生到死的过程,中间经历的各种状态及状态转换
  • 理解线程的状态有利于提升并发编程的理解能力

Java线程的状态

  • Java总共定义了6种状态
  • 6中状态都定义在Thread类的内部枚举类中

在这里插入图片描述

线程状态描述
NEW(新建)线程刚被新建,但是并未启动
Runnable(可运行)线程已经调用了start()等待CPU调度
Blocked(锁阻塞)线程在执行的时候并没有竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待)一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能唤醒
Timed Waiting(计时等待)同waiting状态,有几个方法又超时参数,调用他们将进入Timed Waiting状态,带有超时参数的常用方法有Thread.sleep、Object.wait
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

十六、线程安全的集合

16.1 Collections类中提供的线程安全的集合获得方法

Collections类中提供了一些线程安全的集合获得方法(这些方法都是以synchronized开头),但是这些方法都是jdk1.2时提供,线程安全,但是性能不高,不推荐使用。

16.2 CopyOnWriteArrayList

写有锁,读无锁

写时会复制一份,写入后替换掉原来的地址

优点:线程安全;缺点:耗费内存,因为每次写需要复制一份。

与ArrayList用法一样。

16.3 CopyOnWriteArraySet

底层使用CopyOnWriterList实现

区别在于添加时应该使用addIfAbsent()方法实现,该方法在添加时会遍历集合,如果发现有相同元素,则放弃添加。

16.4 ConcurrentHashMap

使用方式与HashMap相同。

JDK1.7:

  • 使用分段锁segment;
  • 初始时采用16段,只有当添加到同一个段时,才需要互斥等待,如果不是同一个段,不需要等待;
  • 最理想状态分别添加到16个段,理论可以16个线程同时添加。

JDK1.8:

  • 采用CAS(compare and swap比较转换算法)机制
  • 有三个核心变量,V、E、N,V是要更新的变量,E是预期值,当V==E时,认为没有其他线程修改过,则进行修改V=N,否则就认为有别的线程修改过,则取消操作。

悲观锁和乐观锁:

悲观锁就是一种互斥锁,要求只能有一个线程在操作,其他的线程需等待。

乐观锁一般需要一个状态(版本),每次操作时先下载版本号,再进行修改,修改后提交时先比较版本号,如果版本号一致,则没有人在修改时进行了修改,就可以正常提交修改,并将版本号一同修改。

synchronized就是悲观锁;后面所学的Git分布式管理工具的流程就如同乐观锁;

16.5 Queue接口

队列,遵循FIFO(First In First Out先进先出)原则

16.6 ConcurrentLinkedQueue

线程安全,高并发是性能最好的队列。

采用CAS原则

16.7 BlockingQueue

线程阻塞的队列,是Queue的子接口,增加两个无限期等待的方法。

put(E e):向队列中添加元素,如果队列中没有空间,则等待。

E take():在队列中获取元素,如果队列中没有元素,则等待。

可生产消费者模式。

16.7.1 ArrayBlockingQueue

使用数组实现的有界队列,需要固定大小。

16.7.2 LinkedBlockingQueue

使用链表实现的无界队列,默认大小Integer.MAX_VALUE。

public class Test1 {

    //仓库,设置最多只能放6辆car
    private static ArrayBlockingQueue queue = new ArrayBlockingQueue(6);
    //线程池5个,2个工厂,3个4s店
    private static ExecutorService pool = Executors.newFixedThreadPool(5);

    public static void main(String[] args) {

        //工厂
        Runnable c = new Runnable() {
            @Override
            public void run() {
                //工厂生产car  最多15辆
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 15; i++) {
                    Car car = new Car(i, name);
                    //add方法可以将car放入队列,放满后就等待,不会多生产
                    queue.add(car);
                    System.out.println(name + "生产了一辆汽车--->" + car);
                }
            }
        };

        //4s店
        Runnable s = new Runnable() {
            @Override
            public void run() {
                //4s店消费car 最多10辆
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10; i++) {
                    try {
                        //task方法会消费队列中的car,队列中没有可消费的就等待,不会多消费
                        queue.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "消费了"+ i +"辆汽车--->");
                }
            }
        };

        //执行任务,2个工厂生产,3个4s店消费
        //查看打印是否多消费或者多生产
        pool.submit(c);
        pool.submit(c);
        pool.submit(s);
        pool.submit(s);
        pool.submit(s);
        pool.shutdown();

    }

}

class Car {
    private String name;
    private int id;

    public Car() {
    }

    public Car(int id, String name) {
        this.name = name;
        this.id = id;
    }

    @Override
    public String toString() {
        return "Car{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值