JavaEE初阶学习:多线程的初步学习

1.线程的定义和创建

1.线程的概念

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.

线程是轻量级的进程,在一个进程内部可以存在一个或多个线程,进程与进程之间是不能共享内存的,进程之间的消息通信不方便,但是一个进程内部的线程之间是共享这个进程的内存空间的,线程之间通信很方便。

为了方便大家理解,我们举个例子:

一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。

如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。

此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。

2.为什么要引入线程

如以上讲的,线程之间共享内存。比如,一个文字输入软件,其内部可以有三个线程,一个用来响应鼠标、键盘的交互线程,一个用来运算,另一个用来备份。因为进程之间不共享内存,所以不能用多个进程来实现这时就用多线程可以解决。线程之间共享内存,所以从一个线程切换到另一个线程不需要陷入内核,也不需要切换上下文,线程之间的切换比进程切换快捷。

3.进程和线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。

4.线程的创建

1. 继承 Thread 类

  1. 继承 Thread 来创建一个线程类
  2. 创建 MyThread 类的实例
  3. 调用 start 方法启动线程
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello Thread!");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第1张图片

2.实现 Runnable 接口

  1. 实现 Runnable 接口
  2. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
  3. 调用 start 方法
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello Thread!");
    }
}
public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第2张图片

对比上面两种方法:

  • 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
  • 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用Thread.currentThread()

3.匿名内部类创建 Thread 子类对象

public class Demo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("使用匿名类创建 Thread 子类对象");
            }
        };
        t1.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第3张图片

4.匿名内部类创建 Runnable 子类对象

public class Demo4 {
    public static void main(String[] args) {
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用匿名类创建 Runnable 子类对象");
            }
        });
        t2.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第4张图片

5.lambda 表达式创建 Runnable 子类对象

public class Demo5 {
    public static void main(String[] args) {
        Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
        Thread t4 = new Thread(() -> {
            System.out.println("使用匿名类创建 Thread 子类对象");
        });
        t3.start();
        t4.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第5张图片

2.Thread类及其方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

JavaEE初阶学习:多线程的初步学习_第6张图片

1.Thread构造方法

JavaEE初阶学习:多线程的初步学习_第7张图片

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("为线程对象命名!");
Thread t4 = new Thread(new MyRunnable(), "为线程对象命名!");

2.Thread常见属性

JavaEE初阶学习:多线程的初步学习_第8张图片

  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明
public class Demo6 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 存在");
                            Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 消失");
        });
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    }
}

JavaEE初阶学习:多线程的初步学习_第9张图片

3.线程的启动

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

JavaEE初阶学习:多线程的初步学习_第10张图片

调用 start 方法, 才真的在操作系统的底层创建出一个线程.

4.线程的中断

大哥一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那大哥该如何通知二弟停止呢?这就涉及到我们的停止线程的方式了。

目前常见的有以下两种方式:

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

1.使用自定义的变量来作为标志位

  • 需要给标志位上加 volatile 关键字
ublic class Demo7 {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()
                        + ": 正在转账!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()
                    + ": 停止转账");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        System.out.println(Thread.currentThread().getName()
                + ": 让二弟开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,通知二弟对方是个骗子!");
        target.isQuit = true;
    }
}

JavaEE初阶学习:多线程的初步学习_第11张图片

2.使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.

JavaEE初阶学习:多线程的初步学习_第12张图片

  • 使用 thread 对象的 interrupted() 方法通知线程结束.
public class Demo8 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 两种方法均可以
            while (!Thread.interrupted()) {
                //while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                        + ": 正在转账!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
                    // 注意此处的 break
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName()
                    + ": 停止转账!");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        System.out.println(Thread.currentThread().getName()
                + ": 让二弟开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,通知二弟对方是个骗子!");
        thread.interrupt();
    }
}

JavaEE初阶学习:多线程的初步学习_第13张图片
thread 收到通知的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
    (1) 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
  2. 否则,只是内部的一个中断标志被设置,thread 可以通过
    (1)Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
    (2)Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

3.观察标志位是否清除

标志位是否清除, 就类似于一个开关.

Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”

Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为
“不清除标志位”.

  • 使用 Thread.isInterrupted() , 线程中断会清除标志位.
public class Demo9 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted());
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        thread.start();
        thread.interrupt();
    }
}

JavaEE初阶学习:多线程的初步学习_第14张图片
只有一开始是 true,后边都是 false,因为标志位被清除

  • 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除.
public class Demo10 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().isInterrupted());
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        thread.start();
        thread.interrupt();
    }
}

JavaEE初阶学习:多线程的初步学习_第15张图片
全部是 true,因为标志位没有被清除

5.线程的等待

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,二弟只有等三弟转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(Thread.currentThread().getName()
                            + ": 我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
        };
        Thread thread1 = new Thread(target, "二弟");
        Thread thread2 = new Thread(target, "三弟");
        System.out.println("先让二弟开始工作");
        thread1.start();
        thread1.join();
        System.out.println("二弟工作结束了,让三弟开始工作");
        thread2.start();
        thread2.join();
        System.out.println("三弟工作结束了");
    }
}

JavaEE初阶学习:多线程的初步学习_第16张图片

JavaEE初阶学习:多线程的初步学习_第17张图片

6.获取当前线程引用

public class Demo2 {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
    }
}

JavaEE初阶学习:多线程的初步学习_第18张图片
在这里插入图片描述

7.线程的休眠

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
    }
}

JavaEE初阶学习:多线程的初步学习_第19张图片

因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

JavaEE初阶学习:多线程的初步学习_第20张图片

3.线程的状态

public class Demo4 {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}
  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了

1.线程状态和状态转移

JavaEE初阶学习:多线程的初步学习_第21张图片

JavaEE初阶学习:多线程的初步学习_第22张图片

还是我们之前的例子:
刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;

当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;

当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态,至于这些状态的细分,我们以后再详解;

如果李四、王五已经忙完,为 TERMINATED 状态。

所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

1.关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换

  • 使用 isAlive 方法判定线程的存活状态
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
            }
        }, "李四");
        System.out.println(t.getName() + ": " + t.getState());;
        t.start();
        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());;
        }
        System.out.println(t.getName() + ": " + t.getState());;
    }
}

JavaEE初阶学习:多线程的初步学习_第23张图片

2. 关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换

public class Demo5 {
    public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }
}

使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

修改上面的代码, 把 t1 中的 sleep 换成 wait

public static void main(String[] args) {
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (object) {
                try {
                    // [修改这里就可以了!!!!!]
                    // Thread.sleep(1000);
                    object.wait();
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }, "t1");
 ...
}

使用 jconsole 可以看到 t1 的状态是 WAITING.

对于jconsole如何使用参考https://blog.csdn.net/u014112521/article/details/121991115

结论:

  • BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
  • TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒

3.yield() 让出 CPU

public class Demo6 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("张三");
                    // 先注释掉, 再放开
                    // Thread.yield();
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("李四");
                }
            }
        }, "t2");
        t2.start();
    }
}

可以看到:

  1. 不使用 yield 的时候, 张三李四大概五五开
  2. 使用 yield 时, 张三的数量远远少于李四

yield 不改变线程的状态, 但是会重新去排队.

4.线程安全

1.线程安全的定义

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

2.产生线程不安全的原因

产生线程不安全的原因:

  1. 线程是抢占式执行,线程的调度充满随机性
  2. 多个线程对对同一个变量进行修改操作
  3. 针对变量的操作不是原子的
  4. 内存可见性,也会影响到线程安全
  5. 指令重排序,也会影响到线程安全的问题

3.解决线程不安全的问题

举例:

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

JavaEE初阶学习:多线程的初步学习_第24张图片
上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改.
此时这个 counter.count 是一个多个线程都能访问到的 “共享数据”

此时我们引用synchronized 关键字来解决问题

class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
    }
}

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

JavaEE初阶学习:多线程的初步学习_第25张图片

4.保证线程安全的思路

  1. 使用没有共享资源的模型

  2. 适用共享资源只读,不写的模型
    (1). 不需要写共享资源的模型
    (2). 使用不可变对象

  3. 直面线程安全(重点)
    (1). 保证原子性
    (2). 保证顺序性
    (3). 保证可见性

5.synchronized 关键字

1.synchronized 的特征

1.互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

JavaEE初阶学习:多线程的初步学习_第26张图片
synchronized用的锁是存在Java对象头里的。
JavaEE初阶学习:多线程的初步学习_第27张图片
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).

如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.

如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

JavaEE初阶学习:多线程的初步学习_第28张图片

理解 “阻塞等待”.

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待,一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

注意:

  • 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

synchronized的底层是使用操作系统的mutex lock实现的.

2.刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性.

3.可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

理解 “把自己锁死”

一个线程没有释放锁, 然后又尝试再次加锁.

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁

JavaEE初阶学习:多线程的初步学习_第29张图片
这样的锁称为 不可重入锁.

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.

举例:

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

在上述的代码中,

  • increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)

这个代码是完全没问题的. 因为 synchronized 是可重入锁.

在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

2.synchronized 的使用

1.直接修饰普通方法

锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

2.修饰静态方法

锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

3.修饰代码块

明确指定锁哪个对象.

  • 锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
       }
   }
}
  • 锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争.

6.volatile 关键字

1.保证内存可见性

volatile 修饰的变量, 能够保证 “内存可见性”.

JavaEE初阶学习:多线程的初步学习_第30张图片

代码在写入 volatile 修饰的变量的时候

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.

加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.

代码示例
在这个代码中

  • 创建两个线程 t1 和 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束.
public class Demo8 {
    static class Counter {
        public int flag = 0;
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
                // do nothing
            }
            System.out.println("循环结束!");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第31张图片

当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

如果给 flag 加上 volatile

public class Demo8 {
    static class Counter {
        public volatile int flag = 0;
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
                // do nothing
            }
            System.out.println("循环结束!");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

当用户输入非0值时, t1 线程循环能够立即结束.
JavaEE初阶学习:多线程的初步学习_第32张图片

2.不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

public class Demo9 {
    static class Counter {
        volatile public int count = 0;
        void increase() {
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

JavaEE初阶学习:多线程的初步学习_第33张图片

此时可以看到, 最终 count 的值仍然无法保证是 100000.

7.wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.

但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

JavaEE初阶学习:多线程的初步学习_第34张图片

球场上的每个运动员都是独立的 “执行流” , 可以认为是一个 “线程”.

而完成一个具体的进攻得分动作, 则需要多个运动员相互配合, 按照一定的顺序执行一定的动作, 线程1 先 “传球” , 线程2 才能 “扣篮”.

完成这个协调工作, 主要涉及到三个方法

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.

注意: wait, notify, notifyAll 都是 Object 类的方法.

1.wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
   }
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()。

2.notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的- 对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

使用notify()方法唤醒线程

  • 创建 WaitTask 类, 对应一个线程, run 内部循环调用 wait.
  • 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次 notify
  • 注意, WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object.
public class Demo10 {
    static class WaitTask implements Runnable {
        private Object locker;
        public WaitTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker) {
                while (true) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    static class NotifyTask implements Runnable {
        private Object locker;
        public NotifyTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new WaitTask(locker));
        Thread t2 = new Thread(new NotifyTask(locker));
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第35张图片

3. notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

使用notifyAll()方法唤醒所有等待线程

  • 创建 3 个 WaitTask 实例. 1 个 NotifyTask 实例.
public class Demo11 {
    static class WaitTask implements Runnable {
        private Object locker;
        public WaitTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker) {
                while (true) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    static class NotifyTask implements Runnable {
        private Object locker;
        public NotifyTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new WaitTask(locker));
        Thread t3 = new Thread(new WaitTask(locker));
        Thread t4 = new Thread(new WaitTask(locker));
        Thread t2 = new Thread(new NotifyTask(locker));
        t1.start();
        t3.start();
        t4.start();
        Thread.sleep(1000);
        t2.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第36张图片

此时可以看到, 调用 notify 只能唤醒一个线程

  • 修改 NotifyTask 中的 run 方法, 把 notify 替换成 notifyAll
public class Demo11 {
    static class WaitTask implements Runnable {
        private Object locker;
        public WaitTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker) {
                while (true) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    static class NotifyTask implements Runnable {
        private Object locker;
        public NotifyTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notifyAll();
                System.out.println("notify 结束");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new WaitTask(locker));
        Thread t3 = new Thread(new WaitTask(locker));
        Thread t4 = new Thread(new WaitTask(locker));
        Thread t2 = new Thread(new NotifyTask(locker));
        t1.start();
        t3.start();
        t4.start();
        Thread.sleep(1000);
        t2.start();
    }
}

JavaEE初阶学习:多线程的初步学习_第37张图片

此时可以看到, 调用 notifyAll 能同时唤醒 3 个wait 中的线程

注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.

notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着

JavaEE初阶学习:多线程的初步学习_第38张图片

notifyAll 一下全都唤醒, 需要这些线程重新竞争锁

JavaEE初阶学习:多线程的初步学习_第39张图片

4.wait 和 sleep 的对比

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间.

当然为了面试的目的,我们还是总结下:

  1. wait 需要搭配 synchronized 使用. sleep 不需要.
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.

8.单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

1.饿汉模式

类加载的同时, 创建实例.

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
   	}
}

2.懒汉模式-单线程版

类加载的时候不创建实例. 第一次使用的时候才创建实例.

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   	}
}

3.懒汉模式-多线程版

上面的懒汉模式的实现是线程不安全的.

线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.
一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance 了)

加上 synchronized 可以改善这里的线程安全问题.

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   	}
}

4.懒汉模式-多线程版(改进)

以下代码在加锁的基础上, 做出了进一步改动:

  • 使用双重 if 判定, 降低锁竞争的频率.
  • 给 instance 加上了 volatile.
class Singleton2 {
    //1.不是立即初始化实例
    private static volatile Singleton2 instance = null;
    //2.把构造方法视为private
    private Singleton2() {}
    //3.提供一个方法来获取到上述单例的实例
    //  只有当真正需要用到这个实例的时候,才会真正创建这个实例
    public static Singleton2 getInstance() {
        //优化:让getInstance初始化之前才进行加锁,初始化之后,就不再加锁了
        //如果这个条件成立,说明当前的单例未初始化过的,存在线程安全风险,就需要加锁~~
        if (instance == null) { //是否要加锁
            synchronized(Singleton2.class) {
                //实用类对象作为锁对象
                if (instance == null) { //是否要创建实例
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

9.阻塞队列

阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.

实现方式:

  • 通过 “循环队列” 的方式来实现.
  • 使用 synchronized 进行加锁控制.
  • put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
  • take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
class MyBlockingQueue {
    //保存数据的本体
    private int[] data = new int[1000];
    //有效元素的个数
    private int size = 0;
    //队首下标
    private int head = 0;
    //队尾下标
    private int tail = 0;
    //专门的锁对象
    private Object locker = new Object();

    //入队列
    public void put(int val) throws InterruptedException {
        synchronized (locker) {
            if (size == data.length) {
                /*//暂时先直接返回
                return;*/
                locker.wait();
            }
            //把新的元素放到tail位置
            data[tail] = val;
            tail++;
            //处理tail到达数组末尾的情况
            if (tail >= data.length) {
                tail = 0;
            }
            size++;
            //如果入队列成功,则队列非空,于是就唤醒take中的阻塞等待
            locker.notify();
        }
    }
}

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.

比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求, 服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求. 这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.

  1. 阻塞队列也能使生产者和消费者之间 解耦.

比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.

擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行,无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的).

class MyBlockingQueue {
    //保存数据的本体
    private int[] data = new int[1000];
    //有效元素的个数
    private int size = 0;
    //队首下标
    private int head = 0;
    //队尾下标
    private int tail = 0;
    //专门的锁对象
    private Object locker = new Object();

    //入队列
    public void put(int val) throws InterruptedException {
        synchronized (locker) {
            if (size == data.length) {
                /*//暂时先直接返回
                return;*/
                locker.wait();
            }
            //把新的元素放到tail位置
            data[tail] = val;
            tail++;
            //处理tail到达数组末尾的情况
            if (tail >= data.length) {
                tail = 0;
            }
            size++;
            //如果入队列成功,则队列非空,于是就唤醒take中的阻塞等待
            locker.notify();
        }
    }


    //出队列
    public Integer take() throws InterruptedException {
        synchronized (locker) {
            if (size == 0) {
                /*//如果队列为空,就返回一个非法值
                return null;*/
                locker.wait();
            }
            int ret = data[head];
            head++;
            if (head >= data.length) {
                head = 0;
            }
            size--;
            //take成功之后,就唤醒put中的等待
            locker.notify();
            return ret;
        }
    }


}
public class Demo12 {

    private static MyBlockingQueue queue = new MyBlockingQueue();
    
    public static void main(String[] args) {
        //实现一个简单的生产者模型
        Thread producer = new Thread(() -> {
            int num = 0;
            while (true) {
                try {
                    System.out.println("生产了: " + num);
                    queue.put(num);
                    num++;
                    //当生产者生产的慢一些的时候,消费者就得跟着生产者的步伐走
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int num = queue.take();
                    System.out.println("消费了: " + num);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
    }
}

10.定时器

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

定时器是一种实际开发中非常常用的组件.

比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.

比如一个Map, 希望里面的某个 key 在 3s 之后过期(自动删除).

类似于这样的场景就需要用到定时器.

标准库中的定时器

  • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
import java.util.Timer;
import java.util.TimerTask;

public class Demo13 {
    public static void main(String[] args) {
        //Timer内部是有专门的线程,来负责注册的任务的~~
        //Timer内部都需要东西?
        //1)描述任务 创建一个专门的类来表示一个定时器中的任务(TimeTask) 2)组织任务(使用一定的数据结构把一些任务放到一起),通过一定的数据结构来组织 3)执行时间到了的任务

        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },3000);
        System.out.println("main");
    }
}

实现定时器

//创建一个类,表示一个任务
class MyTask implements Comparable<MyTask>{
    //任务具体要干啥
    private Runnable runnable;
    //任务具体什么时候干,保存任务要执行的毫秒级时间戳
    private long time;

    //after 是一个时间间隔,不是绝对的时间戳
    public MyTask(Runnable runnable,long after) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + after;
    }

    public void run() {
        runnable.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}

class MyTimer {
    //定时器内部需要能够存放多个任务
    //此处的队列要考虑到线程安全 PriorityBlockingQueue => 即带有优先级,又带有阻塞的队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable,long delay) {
        MyTask task = new MyTask(runnable,delay);
        queue.put(task);
        //每次任务插入成功以后,都唤醒一下扫描线程让线程重新检查一下队首的任务看是否时间到要执行
        synchronized (locker) {
            locker.notify();
        }
    }

    private Object locker = new Object();

    public MyTimer() {
        Thread t = new Thread(() -> {
             while (true) {
                 try {
                     //先取出队首元素
                     MyTask task = queue.take();
                     //再比较一下看看当前这个时间任务到了没?
                     long curTime = System.currentTimeMillis();
                     if (curTime < task.getTime()) {
                         //时间没到,把任务塞回到队列中
                         queue.put(task);
                         //指定一个等待时间
                         synchronized (locker) {
                             locker.wait(task.getTime() - curTime);
                         }
                     }else {
                         //时间到了.执行这个任务
                         task.run();
                     }

                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
        });
        t.start();
    }
}

public class Demo14 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello timer!");
            }
        },3000);
        System.out.println("main");
    }

}

JavaEE初阶学习:多线程的初步学习_第40张图片

11.线程池

线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销.

因为频繁的开启线程或者停止线程,线程需要重新从cpu从就绪状态调度到运行状态,需要发送cpu的上下文切换,效率非常低。

线程池是复用机制,提前创建好一些固定的线程数一直在运行状态,实现复用,从而可以减少就绪到运行状态的切换。

JavaEE初阶学习:多线程的初步学习_第41张图片
标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//线程池
//一般认为,纯用户态的操作,效率要比经过内核态处理的操作,要效果更高~~
//线程池里面有什么?
//1.先能够描述任务(直接用Runnable) 2.需要组织任务(直接用BlockingQueue) 3.能够描述工作线程  4.还需要组织这些线程 5.需要实现往线程池里添加任务
public class Demo15 {
    public static void main(String[] args) {
        //创建一个固定线程数目的线程池,参数指定了线程个数
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //创建一个自动扩容的线程池,会根据任务量来自动进行扩容
        //Executors.newCachedThreadPool();
        //创建一个只有一个线程的线程池
        //Executors.newSingleThreadExecutor();
        //创建一个带有定时器使用的线程池,类似于 Timer
        //Executors.newScheduledThreadPool();

        for (int i = 0; i < 100; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Hello threadpool");
                }
            });
        }
    }
}

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装.

实现线程池

  • 核心操作为 submit, 将任务加入线程池中
  • 使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
  • 指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增线程了.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

class MyThreadPool {
    //1.描述一个任务,直接使用 Runnable.不需要额外创建类
    //2.使用一个数据结构来组织若干个任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
    //3.描述一个线程,工作线程的功能就是从任务队列中取任务并执行
    static class Worker extends Thread {
        //当前线程池中有若干个Worker线程 这些线程内部都持有了上述的任务队列
        private BlockingQueue<Runnable> queue = null;

        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            //就需要能够拿到上面的队列!!
            while (true) {
                try {
                    //循环的获取任务队列中的任务
                    //这里如果队列为空,就直接阻塞,如果队列非空,就获取到里面的内容~~
                    Runnable runnable = queue.take();
                    //获取到之后,就去执行任务
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //4.创建一个数据结构来组织若干个线程
    private List<Thread> workers = new ArrayList<>();

    public MyThreadPool(int n) {
        //在构造方法中,创建出若干个线程,放到上述的数组中
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);
            worker.start();
            workers.add(worker);
        }
    }

    //5.创建一个方法,能够允许程序员来放任务到线程池中
    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Demo15 {
    public static void main(String[] args) {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }
    }

}

JavaEE初阶学习:多线程的初步学习_第42张图片

12. 线程和进程的对比

线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

进程与线程的区别

  1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
  4. 线程的创建、切换及终止效率更高。

你可能感兴趣的:(手把手学JavaSE,java-ee,学习,java)