【浅学Java】多线程基础

多线程基础

1. 进程和线程

进程:一个运行起来的程序就是一个进程,进程是分配资源的基本单位。

线程:线程是调度的基本单位,线程存在于进程之中,一个进程可以有若干个线程,这些线程共享进程的资源。

引入线程的目的:引入线程就是提高为了“并发编程”的效率。虽然多进程也可以完成“并发编程”,但是由于创建进程/销毁进程/调度进程得开销太大了,导致“并发编程”的效率不是很高。

线程的优点:线程相比进程更加轻量化,在创建、销毁、调度时,都比进程更加高效。因为创建线程和销毁线程时,并不需要申请和释放资源。

面试题:进程和线程的练习和区别

  1. 进程里面包含线程,一个进程里面可以有多个线程
  2. 每个进程都有独立的内存空间(虚拟地址空间),同一个进程的多个线程之间,共享这个虚拟地址空间(也就是线程共享进程的资源)
  3. 进程时资源分配的基本单位,线程是操作系统调度执行的基本单位

2. 创建线程的方法

  1. 继承Thred,重写 run 方法
  2. 实现Runnable接口,重写 run 方法
  3. 采用匿名内部内的方式,继承Thred,重写 run 方法
  4. 采用匿名内部内的方式,实现Runnable接口,重写 run 方法
  5. 使用lambda表达式

这里更加推荐使用第 4 中方法,简单,也可以给线程起名字,例如:

public class ThreadDemo {
    public static void main(String[] args) {
        //创建匿名内部类的实例,将这个实例作为参数传给Thread。
        Thread thread = new Thread(new MyRunnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"myThread");//传入一个 name 参数
        thread.start();
    }
}`在这里插入代码片`

面试题:thread.start() 和 thread.run() 的区别

start:
在这里插入图片描述run:
在这里插入图片描述
通过上面的图可以看出:

  1. start 在运行时会创建一个新的线程
  2. 而run会在main的线程直接调用

3. 并发编程效率分析

public class ThreadDemo7 {
    private static final long count = 10_0000_0000;
    //串行针对 a,b进行自增
    public static void serial(){
        //获取到当前系统的毫秒级时间戳.
        long begin = System.currentTimeMillis();
        int a=0;
        for(long i=0;i<count;i++){
            a++;
        }
        int b=0;
        for(long i=0;i<count;i++){
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.println("serial time:"+(end-begin));
    }
    public static void concurrency(){
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                int a=0;
                for(long i=0;i<count;i++){
                    a++;
                }
            }
        };
        t1.start();
        Thread t2 = new Thread(){
            @Override
            public void run() {
                int b=0;
                for(long i=0;i<count;i++){
                    b++;
                }
            }
        };
        t2.start();
        //需要保证在 t1 和 t2 都执行玩了之后,再结束及时
        try {
            //join 就是等待对应的线程结束
            //当 t1 和 t2 没有执行完之前,join方法就会阻塞等待
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("concurrency time:"+(end-begin));
    }
    public static void main(String[] args) {
        serial();
        concurrency();
    }
}

运行结果:
在这里插入图片描述

根据执行结果可以看出,速度确实提高了。
是正好是一倍吗? 显然不一定,因为线程调度也需要时间。

4. Thread 的常见构造方法

在这里插入图片描述

5. Thread的几个常见的属性

在这里插入图片描述

public class ThreadDemo9 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //打印线程名字
                //通过 Thread.currentThread()方法 过去到线程实例
                //那个线程调用这个方法,就能获取到对应的实例.
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"myThread");

        thread.start();

        //打印以下线程的属性
        System.out.println("id:"+thread.getId());
        System.out.println("name:"+thread.getName());
        System.out.println("state:"+thread.getState());
        System.out.println("priority:"+thread.getPriority());
        System.out.println("isDaemon:"+thread.isDaemon());
        System.out.println("isInterrupted:"+thread.isInterrupted());
        System.out.println("isAlive:"+thread.isAlive());

    }
}

运行结果:
在这里插入图片描述

注意:进程的销毁和代码中对象的销毁是不一样的

进程的销毁时PCB的销毁,代码中对象的销毁依赖的是JDK中的GC回收机制,这两者的生命周期是不一样的。

6 中断线程

中断一个线程就是让一个线程停止工作

中断一个线程的方法:

1. 通过共享的标记来进行沟通

public class ThreadDemo10 {
    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(){
            @Override
            public void run() {
                while(flag){
                    System.out.println("线程运行中~~");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
        //主循环中等待3秒
        Thread.sleep(3000);
        //三秒之后,将 flag 改成 false
        flag = false;
    }
}

2. 调用 interrupt() 方法来通知

以 isInterrupted() 为示例:

public class ThreadDemo11 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                //默认状态下,isInterrupted 值为 false
                while(!Thread.currentThread().isInterrupted()){
                    System.out.println("线程运行中~~");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //break 可以保证循环结束
                        break;
                    }
                }
            }
        };
        thread.start();
        //在主线程中,通过 thread.isInterrupted() 方法来设置这个标记
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //这个操作就是把 Thread.currentThread().isInterrupted()置为 true
        thread.interrupt();
    }
}

注意:
在这里插入图片描述

isInterrupted() 和 interrupted() 的区别

在这里插入图片描述

  1. isInterrupted() 是Thread的示例方法,而interrupted() 是Thread 的静态方法 。
  2. isInterrupted() 只能反映线程是否被中断,而不能改变线程的状态。即当调用isInterrupted() 时,如果返回 true ,是不会对标记位进行修改的,再次调用返回的仍然是true(相当于开关按下去,不会反弹)
  3. interrupted()不仅能反映线程是否被中断,还能清除中断线程的标志位。即当调用interrupted() 时,当返回 true ,还会将标记位修改为false,再次调用返回的就是false(相当于开关按下去,会反弹)

7. 线程等待(join)

线程和线程之间,调度顺序是完全不确定的(取决于操作系统调度器自身的实现),为了能够实现对线程执行顺序的控制,就可以采用线程等待的方法来实现。

场景实现

一种常见的场景就是:t1 线程创建 t2 , t3 线程,让 t2 和 t3 这两个线程分别执行一些任务,然后 t1 线程进行汇总。为了满足这样的场景,就得使 t1 结束的时机都比 t2 , t3 迟

代码实现:

public class ThreadDemo7 {
    private static final long count = 10_0000_0000;
    public static void concurrency(){
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                int a=0;
                for(long i=0;i<count;i++){
                    a++;
                }
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                int b=0;
                for(long i=0;i<count;i++){
                    b++;
                }
            }
        };
        t1.start();
        t2.start();
        try {
            //当 t1 和 t2 没有执行完之前,join方法就会阻塞等待
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("concurrency time:"+(end-begin));
    }
    public static void main(String[] args) {
        concurrency();
    }
}

上述代码中,为了能够实现 计时的功能 ,主线程的long end = System.currentTimeMillis(); 必须得等 t1 和 t2 线程执行完之后再执行。
t1.join(); t2.join();即可以让当前执行的主线程暂停下来,等待 t1 和 t2 执行完之后再执行主线程。

8. 获取当前线程的实例——Thread.currentThread()

public class ThreadDemo13 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId());
                System.out.println(this.getId());
            }
        };
        t.start();
    }
}

执行结果:
在这里插入图片描述
在这种情况下,用图示的两种方法获取当前线程的示例,好像都没啥问题。但是要注意的是:必须使用 继承Thread ,重写 run 的方式创建线程,才会没区别。如果通过 Runnable 或者 lambda 的方式就不行了,因为此时的 this 并不代表当前线程。

9. 休眠当前线程——Thread.sleep()

sleep这个方法,本质上就是将线程PCB从就绪队列移动到了阻塞队列。

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

运行结果:
在这里插入图片描述
两个数字大概差 3000

10. 线程的状态

  1. NEW: 对象创建出来了,但是内核的PCB没有创建出来
  2. RUNNABLE: PCB已经创建出来,同时PCB处于随时待命状态(就绪),这个线程可能在CPU上运行,也可能在就绪队列中排队
  3. TERMINATED: 工作完成了,PCB已经结束了,但是创建的对象还在。
  4. BLOCKED: 活等
  5. WAITING: 死等
  6. TIMED_WAITING: 加锁等待,等待锁被其他线程释放之后,就会重新被激活。
public class ThreadDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t =new Thread(){
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            }
        };
        System.out.println(t.getId()+":"+t.getState());
        t.start();
        System.out.println(t.getId()+":"+t.getState());
        Thread.sleep(1000);
        System.out.println(t.getId()+":"+t.getState());
        Thread.sleep(3000);
        t.interrupt();
        Thread.sleep(1000);
        System.out.println(t.getId()+":"+t.getState());
    }
}

运行结果:
在这里插入图片描述

11. 线程安全(重难点)

11.1 观察线程不安全

代码1:

public class ThreadDemo15 {
    public static int count=0;
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for(int i=0;i<100000;i++){
                    count++;
                }
            }
        };
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

执行结果:
在这里插入图片描述

代码2:

public class ThreadDemo15 {
    public static int count=0;
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for(int i=0;i<50000;i++){
                    count++;
                }
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                for(int i=0;i<50000;i++){
                    count++;
                }
            }
        };
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

执行结果:
在这里插入图片描述
两个代码的结果分析:
当用一个线程进行100000次的 count++ 时,得到了的结果就是100000,但是当分成两个线程分别进行 50000 次的 count++ 时,就会产生一些奇怪的结果,而且每次执行的结果还都不确定。

这就是因为线程不安全引起的,代码而的线程在执行过程中可能的过程有:
在这里插入图片描述
情况分析(以情况3为例,假设两个线程在两个cpu上执行):
在这里插入图片描述可以看出,虽然执行了两次count++,但是最终的结果只增加了1,这就是线程不安全引起的。

同理,所以上面6种情况,最终内存上的结果分别是:2,2,1,1,1,1.

11.2 线程不安全的原因

在这里插入图片描述
还有一点就是:指令重排序——CPU的优化引起

12. synchronized关键字【面试重灾区】

0. synchronized 锁的是什么?

两个线程竞争同一把锁, 才会产生阻塞等待,两个线程分别尝试获取两把不同的锁, 不会产生竞争

1. synchronized的特性1——互斥

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

**加锁**——进入synchronized修饰的代码块
**解锁**——退出synchronized修饰的代码块

synchronized用的锁是存在Java对象头里的。可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态:
在这里插入图片描述

2. synchronized的特性2——刷新内存

在这里插入图片描述
从这可以看出,当程序中用了synchronized之后,程序的执行速度肯定是变慢了,即用了synchronized之后就与”高性能“无缘了。

3. synchronized的特性3——可重入

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

可重入 与 不可重入:
在这里插入图片描述
Java里面的锁是可重入锁,在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

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

4. synchronized的本质(关键点)

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.

5. synchronized 使用示例

  1. 直接修饰普通方法:锁的是SynchronizedDemo 对象
public class SynchronizedDemo {
    public synchronized void methond() {
   }
}
  1. 修饰静态方法:锁的是SynchronizedDemo 类对象
public class SynchronizedDemo {
    public synchronized static void method() {
   }
}
  1. 修饰代码块:明确指定锁那个对象
锁当前对象:
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
        //锁当前对象
            
       }
   }
}
锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

13. Java标准库中的线程安全类

  1. Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施,例如:
    ArrayList
    LinkedList
    HashMap
    TreeMap
    HashSet
    TreeSet
    StringBuilder

  2. 但是还有一些是线程安全的. 使用了一些锁机制来控制。例如:
    Vector (不推荐使用)
    HashTable (不推荐使用)
    ConcurrentHashMap
    StringBuffer

  3. 还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
    String

14. volatile 关键字

14.1. volatile的作用

volatile可以保证内存的可见性

14.2. volatile如何保证内存的可见性

因为编译器为了提高工作效率,对于代码对应的指令并不会完全按照指令进行,它可能会将指令中的一部分省略,以达到优化的目的,这就使得内存不能及时地刷新。volatile就通过限制优化,以保证内存地可见性。

14.3. 代码演示

代码:

public class ThreadDemo22 {
    public static int flag = 1;
    //public static volatile int flag = 1;
    public static void main(String[] args) {
        Thread t1 =new Thread(){
            @Override
            public void run() {
                while(flag!=0){

                }
                System.out.println("循环结束!");
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数:");
                flag = scanner.nextInt();
            }
        };
        t1.start();
        t2.start();
    }
}

运行结果分析:
在这里插入图片描述

【面试】synchronized和volatile的区别

  1. synchronized用来修饰一段代码和方法,而volatile只是用来修饰变量。
  2. synchronized不仅能够保证内存的可见性,还可以实现程序的原子性操作。而volatile只能够保证内存的可见性,不能保证原子性。
  3. synchronized可以造成线程的阻塞,而volatile不会。
  4. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

详见: synchronized和volatile的区别

15. wait 和 notify

wait ——等待
notify——通知
用来协同多个线程之间的执行顺序

wait的作用

  1. 让当前线程阻塞等待(将这个线程的PCB从就绪队列拿到等待队列中)并准备接收通知(来自notify)。
  2. 释放当前锁。想要使用wait/notify,必须搭配synchronized,需要先获取锁,才能谈得上释放锁。
  3. 满足一定条件时,重新尝试获取这个锁。

注意:1,2 是要原子完成的。

notify的使用

  1. notify一次只能唤醒一个线程,如果有多个线程在等待中,调用notify就只能随机唤醒其中一个。notifyAll一次可以唤醒多个线程。
  2. notify的作用唤醒线程,但是调用notify本身的线程并不会立即释放锁,而要等到synchronized代码块执行完之后才能释放锁。

wait 和 notify使用过程中的要保持对象一致性

比如:
如果在线程1中调用对象1的wait方法。 那么在线程2中,也调用对象1的notify才能唤醒线程1

代码演示

public class ThreadDemo18 {
    static class WaitTask implements Runnable{
        private Object locker=null;

        public WaitTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker){
                System.out.println("wait 开始");
                try {
                    //进行 wait 的线程
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 结束");
            }
        }
    }
    static class NotifyTask implements Runnable{
        private Object locker=null;

        public NotifyTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker){
                //进行 notify 的线程
                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(3000);
        t2.start();
    }
}

运行结果分析:
在这里插入图片描述

16. 单例模式

16.0 什么是单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例,单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

饿汉:类加载的同时, 创建实例.
懒汉:类加载的时候不创建实例. 第一次使用的时候才创建实例.

16.1 饿汉模式

class Singleton{
    //把构造方法设置为 private ,防止在类外面调用构造方法,
    //也禁止了调用者在其他类再创建其他实例的机会
    private Singleton(){

    }
    //利用static,在类加载阶段,在这里直接就创建实例
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
}
public class ThreadDemo21{
    public static void main(String[] args) {
        Singleton instance =Singleton.getInstance();
    }
}

16.2 懒汉模式

class SingleTon{
        private SingleTon() {

        }
        private static SingleTon instance = null;
        public static SingleTon getInstance(){
            if (instance==null){
                instance = new SingleTon();
            }
            return instance;
        }
}
public class ThreadDemo20 {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
    }
}

16.3 两个模式的线程安全性分析

饿汉模式:
在这里插入图片描述
懒汉模式:
在这里插入图片描述

16.4 懒汉模式中线程不安全的改进方案

  1. 分析:懒汉模式中,引起线程不安全的原因是:在进行如下代码时,是非原子性的:
if (instance==null){
    instance = new SingleTon();
 }

解决方法很简单,就是给这个操作加锁,如下所示:

synchronized (SingleTon.class){
   if (instance==null){
        instance = new SingleTon();
    }
}
  1. 分析:instance的定义方式,也可能会导致内存可见性的问题
    应该将:
private static SingleTon instance = null;

改成:

private static volatile SingleTon instance = null;

16.4 线程安全时,懒汉模式的效率问题

分析:在懒汉模式中,只要在第一批的并发线程中,才会出现线程安全问题,而后续的操作中,并不会有线程安全问题,在16.3的加锁操作之后,就会在每次调用SingleTon时,都会进行加锁操作,导致程序效率不高。

解决方法:加上判断语句,只在第一批的并发线程中进行加锁操作。

代码如下:

if(instance==null){
    synchronized (SingleTon.class){
        if (instance==null){
            instance = new SingleTon();
        }
    }
}

【面试】这里的双重 if 有什么作用?

在这里插入图片描述

17. 阻塞式队列

17.1 什么是阻塞队列

阻塞队列是一种特殊的队列,它也遵守“先进先出”的规律。
它的特殊之处:

  1. 当队列满的时候,继续入队就会进行阻塞,直到有其他线程的元素被取走
  2. 当队列为空时,继续出队就会进行阻塞,直到其他线程有元素入队之后

17.2 生产者消费者模型

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。即通过一个容器来解决生产者和消费者的强耦合问题。

  1. 阻塞队列相当于一个缓冲区,平衡了生产者与消费者之间的处理能力
  2. 阻塞队列也能使生产者和消费者之间 解耦

举个例子

这里得入口服务器相当于生产者,专用服务器相当于消费者
在这里插入图片描述

模拟实现阻塞队列

  1. 通过 “循环队列” 的方式来实现.
  2. 使用 synchronized 进行加锁控制.
  3. put 插入元素的时候, 判定如果队列满了, 就进行wait. (注意, 要在循环中进行 wait. 被唤醒时不一 定队列就不满了, 因为同时可能是唤醒了多个线程).
  4. take取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
public class MyBlockingQueue {
    int head=0;
    int tail=0;
    int size=0;
    int [] arr;
    Object locker = new Object();
    public MyBlockingQueue(){
        arr = new int[1000];
    }
    public void put(int elem) throws InterruptedException {
        synchronized (locker){
            while(size==arr.length){
                //阻塞
                locker.wait();
            }
            if(tail>=arr.length){
                tail=0;
            }
            arr[tail]=elem;
            size++;
            tail++;
            locker.notify();
        }
    }
    public int take() throws InterruptedException {
        int ret;
        synchronized (locker){
            while(size==0){
                //阻塞
                locker.wait();
            }
            ret = arr[head];
            head++;
            if(head>= arr.length){
                head=0;
            }
            size--;
            locker.notify();
        }
        return ret;
    }
}

代码分析(关于while)
在这里插入图片描述
关于阻塞分析:
在这里插入图片描述

测试代码:

public class Test_MyBlockingQueue {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
        Thread producer = new Thread(){
            @Override
            public void run() {
                for(int i =0;i<100;i++){
                    System.out.println("生产元素:"+i);
                    try {
                        myBlockingQueue.put(i);
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        //消费者
        Thread customer = new Thread(){
            @Override
            public void run() {
                while(true){
                    try {
                        int e = myBlockingQueue.take();
                        System.out.println("消费元素:"+e);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        customer.start();

        try {
            producer.join();
            customer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

【面试】为什么wait和notify要强制放到synchronized中使用?

class MyBlockingQueue {
    // 用来保存数据的集合
    Queue<String> queue = new LinkedList<>();
 
    public void put(String data) {
        // 队列加入数据
        queue.add(data); 
        // 唤醒线程继续执行(这里的线程指的是执行 take 方法的线程)
        notify(); // ③
    }
 
    public String take() throws InterruptedException {
        // 使用 while 判断是否有数据(这里使用 while 而非 if 是为了防止虚假唤醒)
        while (queue.isEmpty()) { // ①  
            // 没有任务,先阻塞等待
            wait(); // ②
        }
        return queue.remove(); // 返回数据
    }
}

在这里插入图片描述

class MyBlockingQueue {
    // 用来保存任务的集合
    Queue<String> queue = new LinkedList<>();
    public void put(String data) {
        synchronized (MyBlockingQueue.class) {
            // 队列加入数据
            queue.add(data);
            // 为了防止 take 方法阻塞休眠,这里需要调用唤醒方法 notify
            notify(); // ③
        }
    }
    public String take() throws InterruptedException {
        synchronized (MyBlockingQueue.class) {
            // 使用 while 判断是否有数据(这里使用 while 而非 if 是为了防止虚假唤醒)
            while (queue.isEmpty()) {  // ①
                // 没有任务,先阻塞等待
                wait(); // ②
            }
        }
        return queue.remove(); // 返回数据
    }
}

在这里插入图片描述
我的理解:在一定程度上保证原子性,当满足wait或者notify条件时,在去调用wait或者notify的过程中,条件不会发送变化。

18. 定时器

18.0 什么是定时器

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

18.1 模拟实现定时器

public class ThreadDemo26{
    //使用这个类来描述这个任务
    static class Task implements Comparable<Task>{
        //command表示这个任务
        private Runnable commmand;
        //time表示这个任务啥时候结束
        //这里的time使用ms级的时间戳表示
        private long time;

        //约定time是一个时间差
        //希望this.time保存一个绝对的时间(ms级的时间戳)
        public Task(Runnable commmand,long time){
            this.commmand=commmand;
            this.time=System.currentTimeMillis()+time;
        }
        public void run(){
            commmand.run();
        }

        @Override
        public int compareTo(Task o) {
            return (int)(this.time-o.time);
        }
    }
    static class Timer{
        //使用这个带优先级版本的阻塞队列来组织这些任务
        private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
        //使用locker对象来解决忙等问题
        Object locker = new Object();
        public void schedule (Runnable command,long delay){
            Task task = new Task(command,delay);
            //queue.put(task);
            //每次插入新的任务都唤醒一下扫描线程,让扫描线程能够重新计算 wait 的时间,保证新的任务也不会错过
            queue.put(task);
            synchronized (locker){
                locker.notify();
            }
        }
        public Timer(){//当创建Timer对象时,就会创建线程 t
            //创建扫描线程,这个扫描线程就来判定当前的任务,看看是不是已经到时间能执行了
            Thread t = new Thread(){
                @Override
                public void run() {
                    while(true){
                        //取出队列首元素,判断时间是不是到了
                        try {
                            synchronized (locker){
                                Task task = queue.take();
                                long curTime = System.currentTimeMillis();
                                if(task.time>curTime){
                                    //时间还没到,重新插入回去
                                    //queue.put(task);
                                    //根据时间差进行一个等待
                                    queue.put(task);
                                    locker.wait(task.time-curTime);
                                }else {
                                    //时间到了
                                    task.run();
                                    System.out.println();
                                }
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;//如果出现interrupt方法就能退出线程
                        }
                    }
                }
            };
            t.start();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("程序启动");
        Timer timer = new Timer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        },1000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        },5000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        },10000);
    }
}

19. 线程池

19.0 什么是线程池

线程池就相当于一个存放线程的池子。当我们使用线程的时候,我们可以直接去线程池中拿到线程,这就省去了创建线程的消耗;当我们不需要这个线程的时候,我们并不一定要将这个线程销毁,可以将这个线程放入线程池,方便下一次的使用。

之前,因为进程在并发编程时频繁的创建、销毁进程的开销是比较大的,所以我们引入了更轻量化的线程。但是,为了追求更高的并发编程的效率,我们就引入线程池。

线程池提高并发编程的底层原理:因为线程的创建以及销毁都是需要用户态和内核态的切换来完成的,这就是导致效率不高的原因;当引入线程池之后,线程的使用就不需要创建和销毁,因为也就不需要进行用户态和内核态的切换操作,从而提高了并发效率。

【面试】ThreadPoolExecutor的构造方法的参数都是啥意思?

在这里插入图片描述

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数
  3. keepAliveTime:除过核心线程的其他线程,在不被使用的多场时间里面保持存在,超过时间后,就得挪出线程池。
  4. unit:时间单位——ms,s,minute
  5. workQueue:一个阻塞队列,描述了线程池需要执行的任务
  6. ThreadFactory:线程的创建方式
  7. handler:拒绝策略。当任务满了的时候,又来了新任务时的解决策略,比如:丢弃最新的任务,丢弃最老的任务,阻塞队列,抛出异常

19.1 线程池的使用

由上可以知道,ThreadPoolExecutor使用起来是比较复杂的,于是标准库提供了Executors这个类,相当于对ThreadPoolExecutor又进行了一层封装。

  1. 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  2. 返回值类型为 ExecutorService
  3. 通过 ExecutorService.submit 可以注册一个任务到线程池中.
public class ThreadDemo27 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello world");
            }
        });
    }
}

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

Executors 本质上是 ThreadPoolExecutor 类的封装

19.2 线程池的模拟实现

实现逻辑:

  1. 线程池对外提供的功能就是,外部传入任务就行
  2. 在插入任务的同时,创建线程,保证线程总数不超过约定的最大线程数。
  3. 对于每个线程而言,他要不断地进行queue.take(),即不断地获取任务去执行。

线程池核心代码:

class Worker extends Thread{
    private BlockingQueue<Runnable> queue;
    public Worker(BlockingQueue<Runnable> queue){
        this.queue=queue;
    }
    @Override
    public void run() {
        while(true){
            try {
                Runnable task=queue.take();
                task.run();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class MyThreadPool {
    //最大线程数
    int maxThreadNum;

    public MyThreadPool(int maxThreadNum){
        this.maxThreadNum =maxThreadNum;
    }
    //存储任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
    //存储线程
    private List<Thread> workerList = new ArrayList<>();

    public void submit(Runnable task) throws InterruptedException {
        if(workerList.size()<maxThreadNum){
            //如果线程数没有到上线,就继续创建线程
            Worker worker = new Worker(queue);
            worker.start();
            workerList.add(worker);
        }
        queue.put(task);
    }
}

测试代码:

public class MyThreadPoolTest {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(2);
        for(int i=0;i<20;i++){
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Hello");
                }
            });
        }
    }
}

【面试】描述一下线程池的执行流程和拒绝策略有哪些?

在这里插入图片描述

在这里插入图片描述

20. 工厂模式

工厂模式也是一种设计模式,和单例模式是并列的关系。
工厂模式存在的意义就是给构造方法填坑
比如:
在这里插入图片描述
对于上面的例子,想直接用构造方法是不行的,这时就得用到其他的方法来构造实例了,这样用来构造实例的方法,称为“工厂方法”。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在静态方法中使用JdbcTemplate需要注意以下几点: 1. 静态方法中无法直接使用Spring容器中的Bean,因为静态方法是类级别的,而Bean是实例级别的。因此需要手动获取JdbcTemplate实例,可以通过ApplicationContext获取JdbcTemplate实例,或者通过静态变量保存JdbcTemplate实例。 2. 在使用JdbcTemplate时,需要先创建一个JdbcTemplate实例,并设置数据源。数据源可以通过Spring容器注入,或者手动创建。在静态方法中,可以通过静态变量保存JdbcTemplate实例,避免重复创建。 3. 在使用JdbcTemplate操作数据库时,需要注意线程安全问题。JdbcTemplate是线程安全的,但是需要保证JdbcTemplate实例的线程安全,即在多线程环境中需要保证同一JdbcTemplate实例不会被并发访问。 下面是一个示例代码: ``` public class JdbcUtils { private static JdbcTemplate jdbcTemplate; public static void setDataSource(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } public static void executeSql(String sql) { jdbcTemplate.execute(sql); } } ``` 在上面的代码中,我们通过静态变量保存了JdbcTemplate实例,并提供了一个静态方法setDataSource用于设置数据源。在使用JdbcTemplate时,我们可以直接调用静态方法executeSql执行SQL语句。需要注意的是,这里的executeSql方法是线程安全的,因为JdbcTemplate实例是共享的,并且JdbcTemplate本身是线程安全的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值