1.多线程初阶

1.关于线程

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

着多份代码


为什么会出现线程?

(1)"并发编程" 成为 "刚需",单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.;有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程;

(2)线程比进程更轻量**:**创建线程比创建进程更快,销毁线程比销毁进程更快.,调度线程比调度进程更快;


线程VS进程

进程是包含线程的. 每个进程至少有一个线程存在,即主线程;

进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间;

进程是系统分配资源的最小单位,线程是系统调度的最小单位;


Java中认识多线程

python 复制代码
public class MyThread_demo {
    public static class MyThread extends Thread{
        Random random=new Random();
        public void run(){
            while(true){
                System.out.println(Thread.currentThread().getName());
                try{
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    public static void main(String[] args) {
        MyThread t1=new MyThread();
        MyThread t2=new MyThread();
        MyThread t3=new MyThread();

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

观察输出:可以看出多线程的运行规律

针对线程,我们可以使用 **jconsole**命令观察

2.创建线程

(1)继承Thread类,重写run方法,使用start方法来调用

python 复制代码
public class MyThread_demo {
     public static class MyThread extends Thread{
        public void run(){
            System.out.println("这是我创建的线程");
        }
    }

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

(2)实现Runnable接口,创建Thread类,调用 Thread 的构造方法时将 Runnable 对象作为 target 参数

python 复制代码
  public static class MyRunnable implements Runnable{
        public void run(){
            System.out.println("这是我创建的线程");
        }
    }

    public static void main(String[] args) {
        Thread t1=new Thread(new MyRunnable());
        t1.start();
    }

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

python 复制代码
        Thread t2=new Thread(){
            public void run(){
                System.out.println("这是我使用匿名内部类创建的线程");
            }
        };

        t2.start();

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

python 复制代码
   Thread t3=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是我使用匿名内部类创建的线程");
            }
        });

(5)lambda表达式创建Runnable子类

python 复制代码
  Thread t4=new Thread(()-> {
            System.out.println("这是我使用lambda表达式创建的线程");
        });
    t4.start();

3.Thread类

(1)常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名

(2)常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

(3)常见操作

启动线程:start()

python 复制代码
Thread t5=new Thread(()->{
           System.out.println("线程5");
        });
        
t5.start();

中断线程: 通过共享的标记来进行沟通 ;调用 interrupt() 方法来通知

方法1:使用volitate关键字

python 复制代码
 public 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 t1=new Thread(target,"李四");
        System.out.println(Thread.currentThread().getName()+":让李四开始转账");
        t1.start();
        Thread.sleep(10*1000);
        System.out.println(Thread.currentThread().getName()+":对方是个骗子,别转了");
        target.isQuit=true;
    }

输出结果:

python 复制代码
main:让李四开始转账
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
李四:我正在转钱
main:对方是个骗子,别转了
李四:幸好没转出去,吓死我了

分析代码:

如果没有 volatile

  • 线程"李四"可能会缓存 isQuit 的值 (比如在寄存器或 CPU 缓存中),导致它一直看到 false
  • 即使主线程修改了 isQuit = true,子线程也可能永远不知道,从而陷入无限循环。

有了 volatile

  • 每次"李四"读取 isQuit 时,都会从主内存中取最新值;主线程修改 isQuit = true 后会立即刷新到主内存;因此"李四"能及时感知到变化并退出循环

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

python 复制代码
  public static class MyRunnable implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + ":我正在转钱");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + ":被打断了,赶紧停手!");
                    break; // 跳出循环
                }
            }
            System.out.println(Thread.currentThread().getName() + ":幸好没转出去,吓死我了");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread t1 = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName() + ":让李四开始转账");
        t1.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName() + ":对方是个骗子,别转了");
        t1.interrupt();
    }

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

<font style="color:rgb(119,119,119);">Thread.isInterrupted() </font>相当于按下开关, 开关自动弹起来了. 这个称为 "清除标志位"

<font style="color:rgb(119,119,119);">Thread.currentThread().isInterrupted()</font> 相当于按下开关之后, 开关弹不起来, 这个称为

"不清除标志位"


等待一个线程结束:join()

python 复制代码
        public static void main(String[] args) throws InterruptedException {
            Runnable target=new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        try {
                            System.out.println(Thread.currentThread().getName()+":我还在工作");
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(Thread.currentThread().getName()+":我工作完了");
                }
            };

            Thread t1=new Thread(target,"李四");
            Thread t2=new Thread(target,"王五");
            System.out.println("先让李四工作");
            t1.start();
            t1.join();
            System.out.println("李四工作完了,该让王五工作了");
            t2.start();
            t2.join();
            System.out.println("王五工作结束了");

    }

获取当前线程引用:Thread.currentThread()

python 复制代码
 public static void main(String[] args) {
            Thread t1=Thread.currentThread();
            System.out.println(t1.getName());
        }

休眠当前线程:sleep()

python 复制代码
public static void main(String[] args) throws InterruptedException {
                System.out.println(System.currentTimeMillis());
                Thread.sleep(1000);
                System.out.println(System.currentTimeMillis());
        }

4.线程的状态

(1)观察所有线程的状态

python 复制代码
   public static void main(String[] args) {
            for(Thread.State state:Thread.State.values()){
                System.out.println(state);
            }
        }

NEW: 安排了工作, 还未开始行动

RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.

BLOCKED: 这几个都表示排队等着其他事情

WAITING: 这几个都表示排队等着其他事情

TIMED_WAITING: 这几个都表示排队等着其他事情

TERMINATED: 工作完成了.

(2)线程状态及转移

5.多线程带来的风险:线程安全

(1)问题引出

python 复制代码
   static class Counter{
         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 < 5000; i++) {
                counter.increase();
            }
        });

        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                counter.increase();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }

运行了好多遍,结果都得不到10000,这就是线程不安全的表现

(2)线程不安全的原因

修改共享数据:多个线程针对 counter.count 变量进行修改,此时这个 counter.count 是一个多个线程都能访问到的 "共享数据";

原子性:我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私,这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了,这样就保证了这段代码的原子性了,有时也把这个现象叫做同步互斥,表示操作是互相排斥的;

**可见性:**一个线程对共享变量值的修改,能够及时地被其他线程看到;

**代码顺序性:**多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

(3)解决问题:使用Synchronized关键字

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到

同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于加锁

退出 synchronized 修饰的代码块, 相当于解锁

Synchronized工作过程

  1. 获得互斥锁

  2. 从主内存拷贝变量的最新副本到工作的内存

  3. 执行代码

  4. 将更改后的共享变量的值刷新到主内存

  5. 释放互斥锁

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

(4)理解阻塞等待

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

上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的一部分工作.

(5)synchronized 既能保证原子性, 也能保证内存可见性

给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.

python 复制代码
       static class Counter{
            public  int flag=0;
        }
        public static void main(String[] args) {
            Counter counter=new Counter();
            Thread t1=new Thread(()->{
                while (true){
                    synchronized (counter){
                        if(counter.flag!=0){
                            break;
                        }
                    }

                }
                System.out.println("循环结束");
            });

            Thread t2=new Thread(()->{
                Scanner scan=new Scanner(System.in);
                System.out.println("请输入数字:");
                counter.flag=scan.nextInt();
            });

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

6.认识volitate关键字

(1)保证内存可见性

**代码在写入 volatile 修饰的变量的时候:**改变线程工作内存中volatile变量副本的值,将改变后的副本的值从工作内存刷新到主内存 ;

代码在读取 volatile 修饰的变量的时候:从主内存中读取volatile变量的最新值到线程的工作内存中 ,

从工作内存中读取volatile变量的副本 ;

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

(2)问题发现

python 复制代码
  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){

                }
                System.out.println("循环结束");
            });

            Thread t2=new Thread(()->{
                Scanner scan=new Scanner(System.in);
                System.out.println("请输入数字:");
                counter.flag=scan.nextInt();
            });

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

针对上述代码,当改变flag的值时,进程并没有停止。。。。

t1 读的是自己工作内存中的内容. 当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化

需要给flag加上volitate关键字

7.wait()和notify()

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

(1)wait()方法

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

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

wait 结束等待的条件: 其他线程调用该对象的 notify 方法,wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间),其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.


python 复制代码
 public static void main(String[] args) throws InterruptedException {
                Object object=new Object();
                synchronized (object){
                    System.out.println("等待开始");
                    object.wait();
                    System.out.println("等待结束");
                }
            }

可以观察到,程序在一直等待下去...

(2)notify()方法:唤醒等待的线程

notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的

其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁;

如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程;

在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行

完,也就是退出同步代码块之后才会释放对象锁。

python 复制代码
    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();
    }

WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object.

(3)notifyAll()方法

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

8.单例模式

(1)饿汉模式:类加载的同时创建实例

python 复制代码
 static class Singleton{
        // 类加载时就创建实例
        private static Singleton instance=new Singleton();
        private Singleton(){
            // 私有构造函数,防止外部实例化
        }
        public static Singleton getInstance(){
            //提供全局访问点
            return instance;
        }
 }

(2)懒汉模式(单线程版):类加载的时候不创建实例. 第一次使用的时候才创建实例

python 复制代码
  static 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 可以改善这里的线程安全问题:保证只有一个线程可以执行实例化

python 复制代码
  static class Singleton{
        private static Singleton instance=null;
        private Singleton(){

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

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

python 复制代码
  static class Singleton{
        private static volatile Singleton instance=null;
        private Singleton(){

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

理解双重 if 判定 / volatile:

加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.

外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了,同时为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile .

当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,,其中竞争成功的线程, 再完成创建实例的操作,当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.


简易理解场景:

(1) 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没

有创建的消息. 于是开始竞争同一把锁;

(2) 其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例是

否已经创建. 如果没创建, 就把这个实例创建出来;

(3) 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的if (instance == null)来

确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了;

(4) 后续的线程, 不必加锁, 直接就通过外层if (instance == null)就知道实例已经创建了, 从

而不再尝试获取锁了. 降低了开销

9.阻塞式队列

(1)定义

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

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

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

(2)生产者消费者模型

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

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等

待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取

代码

python 复制代码
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<Integer> queue=new LinkedBlockingDeque<>();

        Thread customer=new Thread(()->{
            while (true){
                try {
                    int val=queue.take();
                    System.out.println("消费元素"+val);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();

        Thread producer=new Thread(()->{
            Random random=new Random();
            while (true){
                try {
                    int val=random.nextInt(1000);
                    System.out.println("生产元素"+val);
                    queue.put(val);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        customer.join();
        producer.join();
    }

(3)标准库中的阻塞队列

BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.

put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.

BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

10.定时器

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

(1)标准库中的定时器

标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .

schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行

python 复制代码
          Timer timer=new Timer();
          timer.schedule(new TimerTask() {
              @Override
              public void run() {
                  System.out.println("111");
              }
          },3000);

11.线程池

线程池最大的好处就是减少每次启动、销毁线程的损耗。

(1)标准库中的线程池

使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池,返回值类型为 ExecutorService ;

通过 ExecutorService.submit 可以注册一个任务到线程池中.

python 复制代码
ExecutorService pool= Executors.newFixedThreadPool(10);

          pool.submit(new Runnable() {
              @Override
              public void run() {
                  System.out.println("hello");
              }
          });

12.线程vs进程

进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位;

进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈;

由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信;

线程的创建、切换及终止效率更高。


创建一个新线程的代价要比创建一个新进程小得多

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

线程占用的资源要比进程少很多

能充分利用多处理器的可并行数量

在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

相关推荐
ccut 第一混2 小时前
用c# 制作一个扑克牌小游戏
开发语言·c#
尤利乌斯.X2 小时前
在Java中调用MATLAB函数的完整流程:从打包-jar-到服务器部署
java·服务器·python·matlab·ci/cd·jar·个人开发
听风吟丶2 小时前
Java 9 + 模块化系统实战:从 Jar 地狱到模块解耦的架构升级
开发语言·python·pycharm
spencer_tseng2 小时前
easy-captcha-1.6.2.jar
java·jar
做怪小疯子2 小时前
JavaScript 中Array 整理
开发语言·前端·javascript
旭编2 小时前
牛客周赛 Round 117
java·开发语言
六元七角八分2 小时前
CSDN文章如何转出为PDF文件保存
开发语言·javascript·pdf
froginwe113 小时前
MongoDB 删除数据库
开发语言
无敌最俊朗@3 小时前
01-总结
java·jvm·数据库