多线程详解(上)

文章目录

一、线程的概念

1)线程是什么

一个线程就是一个"执行流",每个线程之间都可以按照顺序执行自己的代码。多个线程之间同时执行着多分代码

2)为甚要有线程

(1)"并发编程"成为"刚需"

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

(2)在并发编程中, 线程比进程更轻量.

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

3)线程和进程的区别

  1. 进程包含线程,一个进程里可以有一个或者多个线程
  2. 进程和线程都是用来实现并发编程的,但是线程比进程更轻量,更高效,体现在创建、销毁、调度线程都比进程快
  3. 同一个进程的线程之间,共用同一份的资源(内存+硬盘 ),省去了申请资源的开销
  4. 进程和进程之间,是具有独立性的,一个进程挂了,不会影响到其他进程;线程和线程之间(前提实在同一个进程内),是可能会相互影响的(线程安全问题+线程出现异常)
  5. 进程是资源分配的基本单位,线程是调度执行的基本单位

二、Thread的使用

1)线程的创建

继承Thread类

java 复制代码
class MyThread extends Thread{
    @Override
    public void run() {
        super.run ();
        while(true) {
            System.out.println ( "hello thread" );

            try {
                Thread.sleep ( 1000 );
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread ();
        t.start ();
        while(true){
            System.out.println ("hello main");
            Thread.sleep ( 1000 );
        }

    }
}

实现Runnable接口

java 复制代码
class MyRunable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println ("hello thread");
            try {
                Thread.sleep ( 1000 );
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
        }
    }
}


public class Demo2 {
    public static void main(String[] args) {
        Runnable runnable = new MyRunable ();
        Thread t = new Thread (runnable);
        t.start ();

        while(true){
            System.out.println ("hello main");
            try {
                Thread.sleep ( 1000 );
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
        }
    }

}

继承Thread类(使用匿名内部类)

java 复制代码
public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread (){
            @Override
            public void run() {
                super.run ();
                while(true){
                    System.out.println ("hello thread");
                    try {
                        Thread.sleep ( 1000 );
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                    }
                }
            }
        };

        t.start ();

        while(true){
            System.out.println ("hello main");
            Thread.sleep ( 1000 );
        }
    }
}

实现Runnable接口(使用匿名内部类)

java 复制代码
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        

        Thread t= new Thread ( new Runnable () {
            @Override
            public void run() {
                while(true) {
                    System.out.println ( "hello thread" );
                    try {
                        Thread.sleep ( 1000 );
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                    }
                }
            }
        } );
        t.start ();


        while(true){
            System.out.println ("hello main");
            Thread.sleep ( 1000 );
        }
    }
}

使用lambda

java 复制代码
public class Demo5 {
    public static void main(String[] args) {
        Thread t =new Thread (()->{
            while(true){
                System.out.println ("hello thread");
                try {
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
        });

        t.start ();

        while(true){
            System.out.println ("hello main");
            try {
                Thread.sleep ( 1000 );
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
        }

    }
}

2)Thread中的构造方法

3)Thread中的重要属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
java 复制代码
public class Demo6 {
    public static void main(String[] args) {
        Thread t =new Thread (()->{
            while(true){
                System.out.println ("hello thread");
                try {
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
        },"这是新线程");

        System.out.println ( t.isDaemon () );

        //设置t为后台线程
        t.setDaemon ( true );

        System.out.println ( t.isDaemon () );
        t.start ();
    }

}
  • 是否存活,即简单的理解,为 run 方法(回调方法)是否运行结束了
    Thread对象的生命周期要比系统内核中的线程更长一些------Thread对象还在,内核中的线程已经销毁了
java 复制代码
public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t =new Thread (()->{
            System.out.println ("线程开始");
            try {
                Thread.sleep ( 2000 );
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
            System.out.println ("线程结束");
        });

        t.start ();
        System.out.println (t.isAlive ());
        Thread.sleep ( 3000 );

        System.out.println (t.isAlive ());
    }
}
  • 线程的中断问题,下面我们进一步说明

4)线程启动

通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程

就开始运行了。

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

start方法内部,是会调用操作系统的API,来在系统内核创建出线程

run方法就只是单纯描述了该线程要执行什么内容(会在start创建好后被自动调用)

5)中断一个线程

在java中,要销毁/终止线程,做法是比较唯一的,就是想办法让run方法尽快执行结束

常见的有以下两种方式:

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

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

java 复制代码
public class Demo8 {
    private static boolean isQuit =false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread (()->{
            while(!isQuit){
                System.out.println ("线程工作中");
                try {
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }

            }
            System.out.println ("线程工作完毕");
        });

        t.start ();
        Thread.sleep ( 5000 );
        isQuit = true;
        System.out.println ("设置isQuit为true");
    }
}

缺点

  • 需要手动创建变量
  • 当线程内部再sleep的时候,主线程修改变量,新线程不能及时响应

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

Thread.currentThread().isInterrupted() 代替自定义标志位

java 复制代码
public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread (()->{
            //Thread类內部,有一个现成的标志位,可以用来判定当前循环是否要结束
            while(!Thread.currentThread ().isInterrupted ()){//currentThread () 哪个线程调用这个方法,就返回哪个线程的对象
                System.out.println ("线程工作中");
                try {
                    Thread.sleep ( 1000 );//interrupt唤醒线程之后,此时sleep方法抛出异常,同时会自动清除刚才设置的标志位
                } catch (InterruptedException e) {
                     //1.假装没听见,循环继续正常执行
                    e.printStackTrace ();
                    //2.加上一个break,表示让线程立即结束

                    //3.做一些其他工作。,完成之后再结束
                    break;
                    //如果没有sleep,就没有上述操作空间
                }
            }
            System.out.println ("线程停止工作");
        });
        t.start ();
        Thread.sleep ( 5000 );

        System.out.println ("让t线程终止");
        t.interrupt();//把上述Thread对象内部的标志位设为true

    }
}

即使线程内部的逻辑出现阻塞(sleep)也是可以使用这个方法唤醒的

正常来说,sleep会休眠到时间到,才会结束,此处给出的interrupt就可以使sleep内部触发一个异常,从而提前被唤醒

6)线程等待

让一个线程等待另一个线程执行结束,再继续执行,本质上就是控制线程结束的顺序

join实现线程等待效果

主线程中,调用t.join(),此时就是主线程等待t线程先结束

java 复制代码
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread ( () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println ( "t线程工作中" );
                try {
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
        } );
        t.start ();

        //让主线程等待t线程执行结束
        //一旦调用join,主线程就会出发阻塞,此时t线程可以趁机完成后续工作
        //一直到阻塞到t执行完毕了,join才会解除阻塞,才能继续执行
        System.out.println ("join 等待开始");
        t.join ();
        System.out.println ("join 等待结束");
    }
}

7)获取当前线程的引用

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用

8)休眠当前线程

方法 说明
public static void sleep(long millis) throws InterruptedException; 休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throwsInterruptedException 可以更高精度的休眠

线程调度是不可控的,sleep方法只能保证实际休眠时间是大于参数设置的休眠时间

java 复制代码
public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        long beg =System.currentTimeMillis ();
        Thread.sleep ( 1000 );
        long end =System.currentTimeMillis ();
        System.out.println ("时间"+(end-beg)+"ms");
    }
}

系统会按照1000ms这个时间来控制休眠

当1000ms过了之后,系统会唤醒这个线程(阻塞->就绪)

但这个线程成了就绪状态,并不意味着立即回到cpu上运行(这中间有一个"调度开销")

对于windows和linux这样的系统来说,调度开销可能达到ms级别

有些场景对时间精度要求很高,这时往往需要使用"实时操作系统"

三、线程的状态

1)线程的所有状态

线程的状态是一个枚举类型 Thread.State

java 复制代码
public class Demo12 {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values ()) {
            System.out.println (state);
        }
    }
}
  • NEW:安排了工作,还未开始行动
  • RUNNABLE:就绪状态(线程已经在cpu上执行了或者线程正在排队等待上cpu执行)
  • BLOCKED:阻塞,由于锁竞争导致的阻塞
  • WAITING:阻塞,由于wait这种不固定时间的方式产生的阻塞
  • TIMED_WAITING:阻塞,由于sleep这种固定时间的方式产生的阻塞
  • TERMINATED: Thread对象还在,内核中的线程已经没了

四、线程安全

1)线程安全的概念

如果多线程环境下代码运行结果符合我们的预期,即在单线程环境应该的结果,则说明这是线程安全的

java 复制代码
public class Demo14 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread (()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread (()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

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

        
        t1.join ();
        t2.join ();

        //预期结果应该是100000
        System.out.println (count);
    }
}

2)线程不安全的原因

  1. 操作系统中,线程的调度是随机的(抢占式执行)
  2. 两个线程,针对同一个变量进行修改
  3. 修改操作,不是原子的 count++属于 非原子 的操作(先读,再修改)
    上面提到的count++,其实是由散步操作组成的:
    1).从内存中把数据读到寄存器中(在cpu内)
    2).把寄存器中的数据进行加1
    3).把寄存器的数据保存到内存中
    4.内存可见性问题
    5.指令重排序问题

五、 synchronized关键字-监视器锁monitor lock

1)synchronized的特性

1.互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会产生"锁竞争/锁冲突"后一个线程就会阻塞等待

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

synchronized(),() 中需要放入一个用来加锁的对象

这个对象是啥不重要,重要的是通过这个对象来区分两个线程是否竞争同一个锁

synchronized用的锁是存在java对象头里的

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的"锁定" 状态

synchronized的使用方法

法1:

java 复制代码
public class Demo14 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object ();

        Thread t1 = new Thread (()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                    count++;
                }

            }
        });

        Thread t2 = new Thread (()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                    count++;
                }

            }
        });

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


        t1.join ();
        t2.join ();

        //预期结果应该是100000
        System.out.println (count);
    }
}

法二:

java 复制代码
//synchronized的使用方法

class Counter{
    public int count;

    synchronized public void increase(){//synchronized修饰实例方法
        count++;
    }

    //上述代码等价于
//    public void increase(){
//        synchronized (this){
//            count++;
//        }
//    }

    synchronized  public static void increase1(){//synchronized修饰静态方法

    }

    public static  void increase2(){
        synchronized (Counter.class){//类对象

        }
    }
}



public class Demo15 {
    public static void main(String[] args) throws InterruptedException {
        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);
    }
}

2.可重入

可重入锁,指的是一个线程,连续针对一把锁,加锁两次,不会出现死锁,满足这个要求,就是"可重入",不满足,就是"不可重入"

java 复制代码
//t线程中存在下列代码
synchronized(locker){
	synchronized(locker){
	..........
	}
}

一个线程没有释放锁, 然后又尝试再次加锁.就会出现死锁情况

关于死锁

1.一个线程,针对一把锁,连续加锁两次,如果是不可重入锁,就死锁了
synchronized 同步块对同一线程来说是可重入的,不会出现死锁问题

2.两个线程,两把锁(此时无论是不是可重入锁,都会死锁)

(1)线程t1 获取A锁,线程t2 获取B锁

(2)t1尝试获取B,t2尝试获取A

java 复制代码
public class Demo16 {
    private static Object locker1 = new Object ();
    private static Object locker2 = new Object ();

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
                synchronized (locker2){
                    System.out.println ("t1 加锁成功");
                }
            }


        });

        Thread t2 = new Thread(()->{
            synchronized (locker2){
                try {
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
                synchronized (locker1){
                    System.out.println ("t2 加锁成功");
                }

            }

        });

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

3.N个线程,M把锁,更容易出现死锁

哲学家就餐问题

5根筷子,5个哲学家

通常情况下,整个系统可以良好运转

但是极端境况下,就会出现问题

比如,同一时刻,五个哲学家都想吃面,同时拿起左手的筷子

此时,五个哲学家发现他们的右手没有筷子,于是只能等待

等待过程中,哲学家们不会放下左手的筷子

死锁的成因,涉及四个必要条件

  1. 互斥使用(锁的基本特性)。当一个线程持有一把锁后,另一个线程也想获取到这把锁,就要等待阻塞
  2. 不可抢占(锁的基本特性)。当锁已经被线程1拿到后,线程2只能选择等待线程1主动释放,不能抢过来
  3. 请求保持(代码结构)。一个线程尝试获取多把锁(先拿到锁1之后,在尝试获取锁2,获取的时候,锁1不会释放)
  4. 循环等待/环路等待(代码结构)。等待的依赖关系,形成环了

解决死锁,核心就是破坏上述必要条件,只要破坏一个,死锁就形成不了

  • 1和2破坏不了
  • 对于3来说,调整代码结构,避免编写"锁嵌套"逻辑
  • 对于4来说,可以约定加锁顺序,就可以避免循环等待

六、volatile关键字

1)保证内存可见性

计算机运行的程序/代码,经常要访问数据.这些依赖的数据,往往就会存储在 内存 中,cpu使用这个变量的时候,就会把这个内存中的数据,先读出来,放到cpu的寄存器中

cpu读取内存的这个操作,相对与寄存器读取是非常慢的

cpu进行大部分操作,都是非常快,一旦到读/写内存,此时速度就慢下来了

为了减少上述的问题,提高效率,此时编译器就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器

java 复制代码
//内存可见性状况引起的问题
public class Demo17 {
    private static int isQuit = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread (()->{
            while(isQuit==0){//1.load指令读取内存中isQuit的值到寄存器中
                             //2.通过cmp指令比较寄存器的值是否为0,决定是否要继续循环
                             //
                             //由于这个循环,循环的速度飞快,短时间内就会产生大量的循环
                             //此时,编译器/JVM就发现,虽然执行了这么多次load,但是load出来的结果都一样,并且load操作非常费时间(1次lord可以执行上万次cmp)
                             //所以,编译器只是第一次循环的时候,才读了内存,后续不再读内存了,而是直接从寄存器中,取出isQuit的值了

                             //由于此时修改isQuit代码的是另一个线程,编译器没有正确的判断
            }
            System.out.println ("t1退出");
        });

        t1.start ();


        Thread t2 = new Thread (()->{
            Scanner sc =new Scanner ( System.in );
            System.out.println ("请输入isQuit:");
            isQuit = sc.nextInt ();

        });


        t2.start ();
    }
}

加上volatile后就可以解决这个问题了

java 复制代码
public class Demo17 {
    private volatile static int isQuit = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread (()->{
            while(isQuit==0){//1.load指令读取内存中isQuit的值到寄存器中
                             //2.通过cmp指令比较寄存器的值是否为0,决定是否要继续循环
                             //
                             //由于这个循环,循环的速度飞快,短时间内就会产生大量的循环
                             //此时,编译器/JVM就发现,虽然执行了这么多次load,但是load出来的结果都一样,并且load操作非常费时间(1次lord可以执行上万次cmp)
                             //所以,编译器只是第一次循环的时候,才读了内存,后续不再读内存了,而是直接从寄存器中,取出isQuit的值了

                             //由于此时修改isQuit代码的是另一个线程,编译器没有正确的判断
            }
            System.out.println ("t1退出");
        });

        t1.start ();


        Thread t2 = new Thread (()->{
            Scanner sc =new Scanner ( System.in );
            System.out.println ("请输入isQuit:");
            isQuit = sc.nextInt ();

        });


        t2.start ();
    }
}

2)volatile不保证原子性

synchronized能保证原子性,volatile保证的是内存可见性

3)synchronized也能保证内存可见性

synchronized既能保证原子性, 也能保证内存可见性.

java 复制代码
public class Demo18 {
    private volatile static int isQuit = 0;

    public static void main(String[] args) {
        Object object = new Object ();
        Thread t1 = new Thread (()->{
            synchronized (object) {
                while (isQuit == 0) {
                }
            }
            System.out.println ("t1退出");
        });

        t1.start ();


        Thread t2 = new Thread (()->{
            Scanner sc =new Scanner ( System.in );
            System.out.println ("请输入isQuit:");
            isQuit = sc.nextInt ();

        });


        t2.start ();
    }
}

七、wait和notify

协调执行顺序

join是影响线程结束的先后顺序

相比之下,此处希望线程不结束,也能有先后顺序的控制

wait() / wait(long timeout) 等待,让指定线程进入阻塞状态

notify() / notifyAll() 通知,唤醒对应的阻塞状态线程

wait和notify都是Object的方法,随便定义一个对象对可以用

wait,notify可以避免线程饿死

1)wait()方法

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

wait 做的事情:

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

wait 结束等待的条件:

  • 其他线程调用该对象的notify方法
  • wait等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的interrupted方法,导致wait方法抛出InterruptedException异常
java 复制代码
public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object ();

        synchronized (object){
            System.out.println ("wait 之前");

            object.wait ();

            System.out.println ("wait 之后");
        }
    }
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就

需要使用到了另外一个方法唤醒的方法notify()

2)notify()方法

notify 方法是唤醒等待的线程

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有"先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
java 复制代码
public class Demo20 {
    public static void main(String[] args) {
        Object object = new Object ();

        Thread t1 = new Thread (()->{
            synchronized (object){
                System.out.println ("wait 之前");

                try {
                    object.wait ();
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }

                System.out.println ("wait 之后");
            }
        });

        Thread t2 = new Thread (()->{
            synchronized (object){
                System.out.println ("进行通知");
                object.notify ();
            }
        });

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



    }
}

3)notifyAll()方法

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

4)wait和sleep的对比

1.wait 需要搭配 synchronized 使用. sleep 不需要

2.wait 是 Object 的方法 sleep 是 Thread 的静态方法.

八、多线程代码案例

1 )单例模式

单例模式是非常经典的的设计模式

单例模式保证某个类在程序中只存在唯一一份实例,而不会创建多个实例

这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个

单例模式具体实现方式,分为"饿汉"和"懒汉"两种

(1)饿汉模式

类加载时,创建实例

java 复制代码
class Singleton{
    private static Singleton instance = new Singleton ();//static成员  在Singleton类被加载的时候,就会执行到这里的创建实例操作

    private Singleton(){

    };
    
    public static Singleton getInstance(){
        return instance;
    } 
}

(2)懒汉模式-单线程版

java 复制代码
class SingletonLazy{
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance(){//首次调用getInstance的时候才会真正创建实例
        if(instance==null){
            instance = new SingletonLazy ();
        }
        return instance;
    }
}

(3)懒汉模式-多线程版

上述的懒汉模式在多线程中是不安全的

线程安全问题发生在首次创建实例时,如果在多个线程中同时调用getInstance方法,就可能导致创建多个实例

如果多个线程,同时修改同一个变量,此时可能会出现线程安全问题
如果多个线程,同时读取同一个变量,此时不会出现线程安全问题

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

java 复制代码
class SingletonLazy{
    private static SingletonLazy instance = null;

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

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

上述懒汉模式-多线程版中,每次调用getInstance方法都需要加锁/解锁,开销比较高, 而懒汉模式的线程不安全只是发生在首次创建实例的时候.因此后续使用的时候, 不必再进行加锁了

java 复制代码
class SingletonLazy{
    private volatile static SingletonLazy instance = null;

    public   static SingletonLazy getInstance(){
        if(instance == null) {//这两个if的执行时机可能会差异很大,执行结果也可能截然相反
                              //第一个if用来判定是否需要加锁
            synchronized (SingletonLazy.class) {
                if (instance == null) {//第二个if用来判定是否需要new对象
                    instance = new SingletonLazy ();
                }
            }
        }
        return instance;
    }
}

理解双重if判定

  • 外层的if是判定下看当前是否已经把instance实例创建出来了
  • 当多线程首次调用getInstance,多个线程可能发现instance为null,于是进入外层if语句,继续往下执行来竞争锁,其中竞争成功的线程,再完成创建实例的操作
  • 当实例创建完成后,其他竞争到锁的线程就会被挡在内层if语句前,也就不会继续创建其他实例了

我们来理清一下思路

  1. 假设有三个线程,开始执行getInstan,通过外层 if (instance == null)知道了实例还没有创建的消息,于是开始竞争同一把锁
  2. 其中线程2率先抢到锁,此时线程2通过内层的 if (instance == null)进一步确认实例是否已经创建,如果没创建就把实例创建出来
  3. 当线程2释放锁后,线程1和3也先后拿到了锁,也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了
  4. 后续的线程就直接跳过外层if (instance == null)就已经知道实例被创建了,从而不需要再进行加锁解锁,降低了开销

指令重排序

指令重排序,是编译器为了提高执行效率,在保持代码原有的逻辑的前提下,对代码顺序进行重新编排

指令重排序在多线程下,可能会出现误判

new操作,是可能会触发指令重排序的

new操作分为三步:

  1. 申请内存空间
  2. 在内存空间上构建对象(构造方法)
  3. 将构造好对象的内存地址,赋值给instance引用

第1步是一定先执行的,假设进行了指令重排序,执行顺序变为 1->3->2

当t1执行完 第1步和第3步 时,此时instance就已经是非空了!!!但此时,instance指向的是一个还没初始化的对象
第2步 没开始执行,就在同一时间,t2线程开始执行了!!!

t2判定Instance == null ,条件不成立!!!于是t2直接 return instance

进一步t2线程的代码就可能会访问Instance里面的属性和方法了,这时就容易出现bug了

使用volatile的原因

为了避免"指令重排序"导致读取的instance出现偏差,于是补充上 volatile .

2)阻塞队列

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

阻塞队列是一种线程安全的数据结构,带有阻塞特性:

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

阻塞队列的一个最经典场景就是"生产者消费者模型"

生产者消费者模型

生产者消费者模型就是通过一个容器来解决生产者和消费者的耦合问提

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

1)阻塞队列相当于一个缓冲区,平衡的生产者和消费者之间的对数据量处理能力(削峰填谷)

比如在 "秒杀" 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,

服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放

到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.

2)阻塞队列也能使生产者和消费者之间解耦合

比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺

子皮的人就是 "生产者", 包饺子的人就是 "消费者".

擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人

也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超

市买的).

标准库的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.

  • BlockingQueue 是一个接口,真正实现的类是LinkedBlockingQueue
  • put 方法用于阻塞式的入队列,take 用于阻塞式的出队列
  • BlockingQueue 也有offer,poll,peek等方法,但这些方法不带有阻塞特性
java 复制代码
public class Deom23 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue = new LinkedBlockingDeque<> ();
        queue.put("abc");
        System.out.println ( queue.take () );
    }
}

生产者消费者模型

java 复制代码
public class Demo24 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<> ();

        Thread customer = new Thread (()->{
            while(true) {
                try {
                    String num = blockingDeque.take ();
                    System.out.println ( "消费元素" + num );

                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
        },"消费者");


        customer.start ();

        Thread producer = new Thread (()->{
            Random random = new Random ();
            while(true) {

                String num = random.nextInt (100)+"";
                try {
                    blockingDeque.put ( num );
                    System.out.println ( "生产元素" + num );
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }


            }
        },"生产者");

        producer.start ();

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

阻塞队列的实现

一个队列,要么空,要么满

take 和 put 只有一边能阻塞

如果put阻塞,其他线程继续调用put也会阻塞,只有靠take唤醒

如果take阻塞,其他线程继续调用take也会阻塞,只有靠put唤醒

interrupt方法,是可能中断wait的状态

java 复制代码
class MyBlockingQueue{
    private String[] data = new String[1000];

    private volatile int head = 0;//后续代码,有的进行读和写,加上volatile,避免内存可见性问提

    private volatile int tail = 0;

    private volatile int size = 0;

    public synchronized void put(String elemt) throws InterruptedException {
        while(data.length==size) {//当wait返回时,确认一下当前队列满不满,满着就继续执行wait
            //队列满了
            //如果是队列满,继续插入就会阻塞
            this.wait ();//在当前代码中,如果interrupt唤醒了wait,直接整个方法就结束了,因为我们使用throws抛出的异常,这个时候代码是没事的
            //但当用try-catch,出现异常,方法不会结束,会继续往下执行,此时会把tail指向的元素覆盖掉.实际上此处的队列还是满的,此时tail指向的元素,并非是无效元素(就是弄丢了一个有效元素)
        }

        //队列没满
        data[tail] = elemt;

        tail = (tail+1)%data.length;//如果tail++到达了数组末尾,这时就要他回到开头,环形队列

        this.notify ();//这个notify是用来唤醒take中的wait

        size++;
    }

    public synchronized String take() throws InterruptedException {
        while(size == 0){
            //队列为空
            this.wait ();
        }

        //队列不为空
        String ret = data[head];
        head = (head+1)%data.length;

        this.notify ();//这个notify用来唤醒put中的wait

        size--;
        return ret;
    }


}

public class Demo25 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue blockingDeque = new MyBlockingQueue ();

        Thread customer = new Thread (()->{
            while(true) {
                try {
                    String num = blockingDeque.take ();
                    System.out.println ( "消费元素" + num );
                    Thread.sleep ( 1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
        },"消费者");


        customer.start ();

        Thread producer = new Thread (()->{
            Random random = new Random ();
            while(true) {

                String num = random.nextInt (100)+"";
                try {
                    blockingDeque.put ( num );
                    System.out.println ( "生产元素" + num );

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


            }
        },"生产者");

        producer.start ();

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

3)定时器

标准库中的定时器

  • 标准库中提供了一个Timer类,Timer类的核心方法为schedul
  • schedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位毫秒)
java 复制代码
public class Demo26 {
    public static void main(String[] args) {
        Timer timer = new Timer ();
        //给定时器安排一个任务,预定在xxx时间去执行
        timer.schedule ( new TimerTask () {//此处使用匿名内部类的写法,继承了TimerTask并创建出一个实例
            @Override
            public void run() {
                System.out.println ("执行定时器的任务");//通过run来描述任务的详细情况
            }
        },3000 );
        System.out.println ("程序启动");
    }
}

主线程执行schedule方法的时候,就是把这个任务给放到timer对象中了

与此同时,timer里头也包含了一个线程,这个线程叫"扫描线程",一旦时间到,扫描线程就会执行刚才安排的任务

完成了任务之后,整个进程没有结束,因为Timer内部的线程阻止了进程结束

Timer里,是可以安排多个任务的

实现定时器

定时器的构成:

  • 一个带优先级的阻塞队列作为存储任务的数据结构

为啥要带优先级?

因为阻塞队列中的任务都有各自执行的时间(delay),最先执行的任务一定是delay最小的,使用带优先级的队列就可以高效的把这个delay最小任务找出来

  • 队列中的每个元素是一个MyTimerTask对象,把所有任务保存起来
  • 同时有一个线程一直扫描队首元素,看队首元素是否需要执行
java 复制代码
//通过这个类,描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{//优先级队列需要提供比较
    //要有一个执行的任务
    private Runnable runnable;

    //有一个任务执行的时间
    private long time;

    //此处的delay就是schedule方法传入的"相对时间"

    public MyTimerTask(Runnable runnable , long delay) {
        this.runnable = runnable;

        //构造出要执行任务的绝对时间
        this.time = System.currentTimeMillis ()+delay;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //队首元素是最小时间的值
        return (int) (this.time - o.time);
    }

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }
}




//创建定时器
class MyTimer{
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<> ();

    private Object locker = new Object ();

    public void schedule(Runnable runnable,long delay){
        synchronized (locker) {
            queue.offer ( new MyTimerTask ( runnable , delay ) );
            locker.notify ();
        }
    }



    //创建一个扫描线程
    public MyTimer() {
        Thread t = new Thread (()->{
            //扫描线程需要不停的扫描队首元素,看是否到达时间
            while(true){
                try{
                    synchronized (locker) {
                        //使用while的目的是为了在wait被唤醒时,再确认一下条件
                        while (queue.isEmpty ()) {
                            //队列为空,阻塞等待
                            //使用wait进行等待
                            //这里的wait需要由另外的线程唤醒
                            //添加新的任务就应该唤醒
                            locker.wait ();
                        }
                        MyTimerTask task = queue.peek();
                        // 比较一下看当前的队首元素是否可以执行了.
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 当前时间已经达到了任务时间, 就可以执行任务了
                            task.getRunnable().run();
                            // 任务执行完了, 就可以从队列中删除了.
                            queue.poll();
                        } else {
                            // 当前时间还没到任务时间, 暂时不执行任务.
                            // 暂时先啥都不干, 等待下一轮的循环判定了.
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
        });

        t.start ();
    }
}



public class Demo27 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        }, 2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        }, 1000);
        System.out.println("程序开始执行");
    }
}

4)线程池

线程池最大的好处就是减少每次启动线程,销毁线程的开销

标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含10个线程的线程池
  • 返回类型为 ExecutorService
  • 通过ExecutorServic.submit可以注册一个任务到线程池中
java 复制代码
public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool ( 10 );
        service.submit ( new Runnable () {
            @Override
            public void run() {
                System.out.println ("hello");
            }
        } );
    }
  • Executors.newCachedThreadPool() 构造出的线程池中的线程数目能够动态适应 创建出来之后不会急着销毁,会在池子里保留一定时间,以备随时再用到
  • Executors.newFixedThreadPool(10) 构造出的线程池中的线程数目是固定的
  • Executors.newSingleThreadPool() 构造出只有一个线程的线程池
  • Executors.newScheduledThreadPool() 类似定时器,但有多个扫描线程
    这几个工厂方法的线程池,本质上是对一个 类 进行封装, ThreadPoolExecutor
    这个类功能非常丰富,提供了很多参数,标准库上述的几个工厂方法,其实就是给这个类填写了不同的参数用来构造线程池的

ThreadPoolExecutor 核心方法就两个 (1)构造 (2)任务注册(添加任务)

在这个包下

![在这里插入图片描述](https://img-blog.csdnimg.cn/b41e6b3f8c3745eb92fa2c7fee99fadc.png

corePoolSize 核心线程数

maximumPoolSize 最大线程数

这个线程池里的线程数目可以动态变化的

变化范围是[corePoolSize ,maximumPoolSize]

keepAliveTime 允许线程留存的时间

unit 是留存时间的单位

阻塞队列,用来存放线程池中的任务,可以根据需要灵活设置

需要优先级 就可以设置PriorityBlockingQueue

任务数目恒定 就可以设置ArrayBlockingQueue

此处使用ThreadFactory作为工厂类,由这个类负责创建线程

使用工厂类创建,主要是为了在创建过程中,对线程进行初始化

这是线程池的拒绝策略

一个线程池,能容纳的任务数量是有限的

当持续往线程池里添加任务的时候,一旦已经到达上线,继续再添加,会出现的效果跟选择的拒绝策略有关

直接抛出异常

新添加的任务,让添加任务的线程负责执行

丢弃任务队列中最老的任务

丢弃当前新加的任务

设置线程数目的多少

一个线程,执行的代码,主要有两类:

1.CPU密集型: 代码里主要的逻辑实在进行算数运算/逻辑判断

2.IO密集型: 代码里主要进行的是IO操作

假设一个线程的所有代码都是cpu密集型代码,这时,线程池设置的数量不应该超过N(N为cpu逻辑核心数).

假设一个线程的所有代码都是IO密集型代码,这时不吃cpu,线程池设置的数量就可以超过N(N为cpu逻辑核心数).一个核心可以通过调度的方式,来实现并发执行

代码不同,线程池的线程数目设置就不同

无法知道一个代码,具体多少内容是cpu密集型,多少内容是IO密集型

正确做法:使用实验的方式,对程序进行性能测试

测试过程中尝试修改线程数目来确定最优解

工厂模式

线程池对象不是我们直接new的

而是通过一个专门的方法,返回一个线程的对象

通常创建对象,使用new.new关键字会触发类的构造方法.但是,构造方法存在一定的局限性

Executors.newFixedThreadPool(10) 工厂模式(设计模式)

工厂模式是给构造方法填坑的

很多时候,构造一个对象,希望有多种构造方式

多种方式,就需要使用多个版本的构造方法来分别实现

但是构造方法要求方法是类名,不同的构造方法只能 通过重载的方式来区分了(重载=>参数类型/个数 不同)

使用工厂设计模式就能解决这个问题 使用普通方法,代替构造方法完成初始化工作 普通方法就可以使用方法的名字来区分,也就不再收到重载的规则制约了

实践中,一般单独创建一个类,给这个类创建一些静态方法,由这样的静态方法负责构造出对象

java 复制代码
class PointFactory{
    public static Point makePointByXY(double x, double y){
        Point p = new Point ();
        p.setLocation ( x,y );
        return p;
    }

    public static Point makePointByRA(double r, double a){
        Point p = new Point ();
        p.setLocation ( r,a );
        return p;
    }

}

public class Demo28 {
    public static void main(String[] args) {
       
        Point p = PointFactory.makePointByXY ( 10,20 );
    }
}

线程池的实现

  • 使用阻塞队列来保存任务
  • 核心操作为 submit, 将任务加入到队列中
  • 实现构造方法,在里创建出线程去消费队列中的任务
java 复制代码
class MyThreadPool{
    //任务队列
    private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<> ();

    //通过这个方法,把任务添加到队列中
    public void submit(Runnable runnable) throws InterruptedException {
        //此处我们的拒绝策略,相当于第五种策略,阻塞等待(这是下策)
        queue.put ( runnable );
    }

    public MyThreadPool(int n){
        //创建出n个线程,负责上述队列中的任务
        for (int i = 0; i < n; i++) {
            Thread t = new Thread (()->{
                try {
                    Runnable runnable = queue.take ();
                    runnable.run ();
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            });

            t.start ();
        }
    }
}

public class Demo29 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool ( 4 );
        for (int i = 0; i < 1000; i++) {
            int id = i;
            myThreadPool.submit ( new Runnable () {
                @Override
                public void run() {
                    System.out.println ("执行任务"+id);//因为内部类变量捕获,所以打印不了i
                }
            } );
        }
    }
}
相关推荐
向阳121813 分钟前
mybatis 缓存
java·缓存·mybatis
上等猿19 分钟前
函数式编程&Lambda表达式
java
摇光9333 分钟前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
沐泽Mu36 分钟前
嵌入式学习-QT-Day09
开发语言·qt·学习
蓝染-惣右介37 分钟前
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
java·设计模式
小猿_0041 分钟前
C语言实现顺序表详解
c语言·开发语言
余~~185381628001 小时前
NFC 碰一碰发视频源码搭建技术详解,支持OEM
开发语言·人工智能·python·音视频
秋恬意1 小时前
IBatis和MyBatis在细节上的不同有哪些
java·mybatis
GOATLong1 小时前
c++智能指针
开发语言·c++