JavaEE-多线程(2)

目录

一,volatile 关键字

二,wait/notify

1,调整代码的先后执行顺序

2,线程饿死

wait/sleep的区别:

notifyAll

三,单例模式

1,饿汉模式

2,懒汉模式

四,阻塞队列

(1)线路安全

(2)具有阻塞特性

五,线程池

六,定时器


一,volatile 关键字

private static int n=0;

public static void main(String[] args) {
    Thread thread=new Thread(()->{
        while (n==0){

        }
        System.out.println("thread结束");
    });
    Thread thread1=new Thread(()->{
        Scanner scanner=new Scanner(System.in);
        System.out.println("输入一个整数:");
        n=scanner.nextInt();
    });
    thread.start();
    thread1.start();
}

正常来说,输入一个非0的数字,Thread中的条件就不成立了,这时Thread就会结束,但是结果不符合预期

上述问题为"内存可见性问题".

while (n==0)这个代码可分为两步(1)从内存器中读取到寄存器中(2)cmp指令,比较n的值,前者相较而言,这个操作速度是非常慢的,而后者是非常快的.且while (n==0)这个代码会重复非常多次.此时,JVM执行代码的时候,发现每次循环的执行过程中执行(1)操作开销是非常大的,而且每次(1)操作的结果都是一样的,JVM根本没有意识到,用户会在未来更改n的值,于是JVM作了一个大胆的操作,直接把(1)操作给优化了,此次循环,都不会从内存中读取数据,而是直接读取寄存器/cache中的数据(缓存的结果).当JVM作出上述决定,开销大大减小了,但是当用户将内存中的n的值改变,对于线程Thread来说是"不可见的"因此这样引起了bug

如果代码是单线程的,编译器的优化是比较准确的,但是如果是多线程,编译器/JVM优化可能会出现误判,导致不该优化的地方给优化了,于是就造成了可见性问题

这样就可以避免可见性问题了,因为加入sleep之后,刚才谈到到针对n内存的读取的优化操作就不再进行了,因为和读内存相比,sleep的开销更大,远远超过了读内存的本身,所以就算把读取内存优化掉,也没有意义

以上是一种方法,还有另外一种方法加上关键之volatile

volatile修饰一个变量,提示编译器,这个变量是易变的.这时,编译器就会禁止上述优化,确保每次循环都是从内存中重新读取数据

引入volatile的时候,编译器生成这个代码的时候,就会给这个变量的读取操作附近生成一些特殊的指令,称为"内存屏障".后序JVM执行到这些特殊的指令就知道,不能在进行上述那些优化了

volatile只能针对一个线程读一个线程写这种情况,不能解决原子性问题(count++).

可解决:

(1)保证内存可见性

(2)禁止指令重排序(后续详解)

二,wait/notify

1,调整代码的先后执行顺序

多个线程,需要控制他们的先后顺序,就可以让后执行的使用wait,先执行的程序完成某些逻辑后,在通过notify唤醒对应的wait

2,线程饿死

也可以解决"线程饿死"问题,一个线程进行操作,如果成功,那么进行操作,如果失败,就主动释放锁,并且"阻塞等待",此时这个线程就不参与锁的竞争了,此时,再由其他线程进行操作,直到替他线程通过,通知唤醒之前那个线程

wait/notify都是Object的提供方法,任意Object对象,都可以使用. wait中会进行一个解锁的操作,所以wait是在synchronized中执行.然后要进行阻塞等待,且两者要同时执行

wait/notify都是在持有锁的情况下进行:

private static Object locker=new Object();
public static void main(String[] args) {
    Thread thread1=new Thread(()->{
        synchronized (locker){
            System.out.println("t1 wait之前");
            try {
                locker.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1 wait之后");
        }
    });
    Thread thread2=new Thread(()->{
        System.out.println("t2 notify之前");
        Scanner scanner=new Scanner(System.in);
        scanner.next();
        synchronized (locker){
            locker.notify();
        }
        System.out.println("t2 notify之后");

    });
    thread1.start();
    thread2.start();
}

wait默认是"死等",但是wait还有带参数的版本,可以指定最大等待时间时间

wait/sleep的区别:

两者有本质区别,使用wait的目的就是为了提前唤醒,sleep是固定时间的堵塞,不涉及唤醒,虽然sleep可以被interrupt"唤醒",interrupt操作的意思不是唤醒,而是要终止线程了,wait必须搭配synchronized使用,并且wait会先释放锁,同时进行等待,sleep与锁无关

假设有多个线程都在同一个对象上wait,此时notify只能随机唤醒一个线程

notifyAll

唤醒所有线程.一个一个唤醒整个程序所以比较有序的,如果一起唤醒,这些唤醒的线程就会无序的竞争锁

private static Object locker=new Object();

public static void main(String[] args) {
    Thread thread1=new Thread(()->{
        synchronized (locker){
            System.out.println("t1 wait 之前");
            try {
                locker.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1 wait 之后");
        }
    });
    Thread thread2=new Thread(()->{
        synchronized (locker){
            System.out.println("t2 wait 之前");
            try {
                locker.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t2 wait 之后");
        }
    });
    Thread thread3=new Thread(()->{
        synchronized (locker){
            System.out.println("t3 wait 之前");
            try {
                locker.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t3 wait 之后");
        }
    });
    Thread thread4=new Thread(()->{
        Scanner scanner=new Scanner(System.in);
        scanner.nextLine();
        System.out.println("t4 notify 之前");
       synchronized (locker){
           locker.notifyAll();
       }
        System.out.println("t4 notify 之后");
    });
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
}

如果对方没有wait,或者有一个线程被notify多次,不会有什么副作用

练习:

依次打印A B C

private static Object locker1=new Object();
private static Object locker2=new Object();

public static void main(String[] args) {
    Thread thread1=new Thread(()->{
        System.out.println("A");
        synchronized (locker1){
            locker1.notify();
        }
    });
    Thread thread2=new Thread(()->{
        synchronized (locker1){
            try {
                locker1.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("B");
        synchronized (locker2){
            locker2.notify();
        }
    });
    Thread thread3=new Thread(()->{
        synchronized (locker2){
            try {
                locker2.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("C");
    });
    thread1.start();
    thread2.start();
    thread3.start();
}

有时会出错,我们所期望的是t2先执行(wait),然后t1执行打印,唤醒t2.但是如何不是这样的,如果t1先执行,提前唤醒了,但是t2还没有做好准备,这就会导致t2一直等待....

解决:在t1前加一个sleep,使t1慢于t2执行

三,单例模式

开发中希望有的类在一个进程中,不应该有多个实例(比如说DataSource这个类,没有必要创建出多个实例)

单例模式,只能避免别人的"失误",但是不能规避"故意攻击",;例如反射/序列化反序列化可以打破上述单例模式

1,饿汉模式

饿的意思是"迫切",在类加载的时候,就会创建出这个单例的实例.

此时只有不在其他代码中new这个类,每次需要的时候都通过getInstance来实现,这个类就是单例的了

class Singleton{
    private static Singleton instance=new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
    //单例模式的最关键:
    private Singleton(){}
}

涉及多线程的时候是安全的,因为getInstance()只涉及读的过程

2,懒汉模式

计算机上的"懒"是褒义词.意为"效率更高".推迟了创建实例的时机,第一次使用的时候才会创建实例

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

由于上述原因,getInstance()这个方法在多线程问题中是不安全的,上述逻辑中,就创建了两个实例,虽然第二次创建的实例会将第一个实例的instance覆盖掉,使得第一次创建的实例,没有引用的指向,很快就会被回收掉,但即使如此,上述代码仍被认为是存在bug的

解决:加锁

加锁之后,确实解决了线路安全问题,但是加锁也同样可能带来阻塞,如果上述对象已经new完了对象,其实就不会进入if语句中,那么不加锁线程也是安全的,所以如果按照那个当前代码写法,只要调度getInstance()就会触发加锁操作,产生阻塞,影响性能

优化:

两个if条件,只是恰好写法一样,实际上作用是完全不同的,外面的if判断的是是否加锁(原因:如果多个线程同时调用getInstance()方法,就会多个线程对锁进行竞争,这时线程就会进入阻塞,一旦阻塞,什么时候回复执行,中间时间是完全不确定的,也可能在阻塞的时候instance已经被改)里面的if是判断是否要创立对象,只不过巧了,在这个代码中以同样的方式完成上述两种操作

上述步骤,可能会引起"指令重排序"上述大概分为三个指令(1)分配空间(2)执行构造方法(3)内存空间的地址,赋值给引用变量.对于单线程而言步骤的顺序并不重要,但是对于多线程来说就会出问题,原因如下:

解决:

编译器发现instance是易失的,就会围绕这个变量的优化更加克制,不仅仅会在读取变量优化上克制,也会在修改这个变量上克制.加上volatile后,也能禁止对instance赋值操作插入到其他操作之中

因此Java的volatile的两个功能:

(1)保证内存可见性

(2)禁止指令重排序

四,阻塞队列

在普通的队列(标准库中的队列及其子类默认都是线程不安全的)的基础上进行扩充

特点:(可用于实现"消费者模型")

(1)线路安全

(2)具有阻塞特性

a)如果队列为空,进行出队列操作就会出现堵塞;一直阻塞到其他线程向队列中添加元素为止

b)如果队列为满,进行入队列操作就会出现堵塞;一直阻塞到其他线程从队列中取走元素为止

通常谈到的"阻塞队列"是代码中的一个数据结构,但是由于这个东西太好用了,以至于会把这样的数据结构但单独分装到一个服务器程序,并且单独在服务器上进行部署,此时,这样的的队列就有了一个新名字"消息队列"

优势:

(1)⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。 ⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯

(2)通过阻塞队列,可以起到"削峰填谷"效果,在遇到请求量激增的情况下,可以有效的保护下游服务器

为什么一个服务器收到的请求更多,就有可能会挂??

答:一台服务器就是一台"电脑",上面提供了一些硬件资源(CPU,内存,硬盘,网络带宽....)而这个机器就算配置再好,资源也是有限的.服务器每次收到一个一个请求,处理这个请求的时候就会执行一系列底代码,在执行这些代码的时候,就需要消耗硬件资源,当这些请求量的总和超过了硬件资源量,此时机器就会出现问题

为什么在需求激增的时候,上游服务器和队列不太容易挂,但是下游服务器容易挂呢??

答:上游服务器是类似于"网关服务器"收到客户的请求再把请求转到其他的服务器,代码是比较简单的,消耗的硬件资源也是比较少的.同理,队列,其实也是比较简单的程序,单位请求消耗的硬件资源比较少.但是下游服务区,是真正干活的,要完成的任务量非常庞大,消耗的时间也多,消耗的硬件资源也是更多的

代价:

1,需要更多的机器来着部署这样的消息队列

2,A和B之间的通信延长,会变长

BlockingQueue是一个接口,继承于Queue,所以也继承了offer/poll等方法,但是不推荐使用,BlockingQueue提供了新的方法take/put.,这两个方法也可以被interrupt唤醒

阻塞队列的简易实现:

class MyBlockerQueue{
    //基于数组的队列
    private String[] data=null;
    private volatile int head;
    private volatile int tail;
    private volatile int size;
    private Object locker=new Object();
    public MyBlockerQueue(int capacity){
        data=new String[capacity];
    }
    public void put(String val) throws InterruptedException {
        synchronized (locker){
            if (size==data.length){
                locker.wait();
            }
            data[tail]=val;
            tail++;
            if (tail>=data.length){
                tail=0;
            }
            size++;
            locker.notify();
        }

    }
    public String take() throws InterruptedException {
        String ret="";
        synchronized (locker){
            if (size==0){
               locker.wait();
            }
            ret=data[head];
            head++;
            if (head>=data.length){ 
                head=0;
            }
            size--;
            locker.notify();
        }
        return ret;
    }

}

当队列为满,添加元素时就会进入阻塞,此时如果有元素出队列,就会唤醒此阻塞.反之,当队列为空要使元素出队列就会也会进入阻塞,只有添加元素才能唤醒这个阻塞(文字的颜色与代码的颜色相对应)

但是上述代码存在问题,改进:

while的作用是,就是在wait唤醒之后 再次确认条件,看是否可以继续执行,如果是if语句,且用catch的方式针对InterruptedException异常的时候,就会出现继续往下执行代码的情况,出现错误

五,线程池

最初引入线程的时候是因为进程的开销开销太大,但是,随着业界上对性能要求越来越高,对应,线程的创建/销毁的频次越来越多,此时,线程创建.销毁开销就不可以忽略不计了,

线程池就是解决上述问题的常用手段,线程池就是擦线程提前从系统中申请好,放到一个地方.后面需要线程的时候直接从这个地方来取,而不是从系统重重新申请,线程用完之后,还是换回到刚才这个地方

操作系统=操作系统内核+操作系统配套的应用程序

从系统创建线程,这样的逻辑就是调用API,由系统内核执行一系列逻辑来完成这个过程.直接从线程池中取,这个过程是纯用户态代码,是由咱们自己控制的,整个过程可控,效率更高

应用程序有很多,这些应用程序都是有内核统一负责管制和服务的,内核里的工作是非成繁忙的,因此提交给内核要做的任务可能是不可控的

Java标准库提供了现成的线程池类ThreadPoolExecutor.

int corePoolSize:核心线程数

int maximumPoolSize:最大线程数

此线程池支持线程扩容,在实际使用过程中,如果线程不够,就会进行扩容.

在Java标准库的线程池中会把线程分为两类:

1)核心线程(可以理解为最少有多少个线程)

2)非核心线程(扩容过程中新增的)

核心线程数+非核心线程数的最大值=最大线程数

long keepAliveTime

非核心线程会在线程空闲的时候被销毁,允许的最大空闲时间

TimeUnit unit枚举,时间的单位

BlockingQueue<Runnable> workQueue工作队列,线程池的工作过程就是典型的"生产者消费者模型",程序员使用的时候,通过形如"submit"这样的方法,把要执行的任务设置到线程池里,线程池内部的工作线程,负责执行这些任务.Runnable接口本身的含义就是一段可以执行的任务

ThreadFactory threadFactory线程工厂:此处用来创建对象的static方法就称之为"工厂方法,有时候,工厂方法也会放到单独的类里面实现,用来放工厂方法的类就称之为"工厂类"通过这个类就可以批量完成Thread的实例创建和初始化

RejectedExecutionHandler handler拒绝策略,如果线程池的任务队列满了,但是还是要给这个队列添加元素,这时想要不要阻塞,直接拒绝.

ThreadPoolExecutor.AbortPolicy:添加任务的时候直接抛出异常ThreadPoolExecutor.CallerRunsPolicy:线程池拒绝执行,有调用submit的线程执行

ThreadPoolExecutor.DiscardOldestPolicy把任务队列中最老的任务踢掉,然后执行

ThreadPoolExecutor.DiscardPolicy把任务中最新的任务踢掉

--------------------------------------------------------------------------------------

ThreadPoolExecutor封装前的,定制性强,功能强大,但是复杂.Executors是封装过的,用起来简单

举个栗子:

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 100; i++) {
            int id=i;
            service.submit(()->{
            Thread cur=Thread.currentThread();
            System.out.println(id+","+cur.getName());
            });
        }

    Thread.sleep(2000);//防止任务没执行完就被终止掉
    service.shutdown();//把进程里面的线程都停止掉
    System.out.println("进程停止");
}

使用线程池的时候需要定几个线程呢??

答:无法确定,不能只靠公式计算,首先一台主机上并不是只有一个程序在运行,除此之外写的每个程序也不会100%饱满CPU,在工作过程中可能会遇到一些阻塞或者IO操作,这些都会使线程主动放弃CPU的使用

推荐:实际开发中,建议通过做实验的方法,找到一个合适的线程池数,给线程池设置不同的线程数,分别进行性能测试,关注消耗的响应时间/消耗的资源指标,选择一个合适的线程数

模拟实现一个线程池:

class MyThreadPool{
    private BlockingQueue<Runnable> queue=new ArrayBlockingQueue(1000);
    public MyThreadPool(int n ){
        for (int i = 0; i < n; i++) {
            Thread t=new Thread(()->{
                while ( !Thread.currentThread().isInterrupted()){
                    try {
                        Runnable runnable=queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}

六,定时器

标准库中提供了⼀个 Timer 类. Timer 类的核⼼⽅法为 schedule

实现定时器:

使用List保存,不是好的选择,后续执行的时候就需要依次遍历,执行完毕,还需要从对应链表里面删除,比较好的数据结构是堆,这样可以将数据从delay时间小到大进行排序

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;
    private long time;
    public MyTimerTask(Runnable runnable,long delay){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+delay;
    }
    public void run(){
        runnable.run();
    }
    public long getTime(){
        return time;
    }

    @Override
    public int compareTo(MyTimerTask o1) {
        return (int)(this.time-o1.time);
    }
}
class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private Object locker=new Object();

    public MyTimer() {
        Thread thread = new Thread(() -> {
            try {
                while (true) {
                    synchronized (locker){
                        while (queue.isEmpty()) {
                          locker.wait();
                        }
                        MyTimerTask cur = queue.peek();
                        if (System.currentTimeMillis() >= cur.getTime()) {
                          cur.run();
                           queue.poll();
                        } else {
                            locker.wait(cur.getTime()-System.currentTimeMillis());
                        }
                    }
                }
            }
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        thread.start();
    }
    public void schedule(Runnable runnable,long delay){
        synchronized (locker) {
            MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
            queue.offer(myTimerTask);
            locker.notify();
        }
    }
}

至此,这是一个多线程问题,但是PriorityQueue不含多线程安全问题,所以我们需要进行上锁

防止在没有任务进入队列的时候,进入空循环,运用wait,只有在队列中添加元素后才会被唤醒

防止任务的时间过长,代码进行不断地循环,可能会出现线程饿死的情况,所以这里等待一个任务时间和当前时间的差值,但是这里为什么使用wait而不用sleep??

答:

1)不应该使用sleep,如果来个一个时间更早的任务,这时sleep不会被唤醒,就会错过,但是如果使用wait来说,每增加一个任务唤醒等待,且会对等待时间重新进行计算

2)sleep休眠的时候,是不会释放锁的,所以他是抱着锁睡得,此时其他任务添加不进来

至此,这是一个多线程问题,但是PriorityQueue不含多线程安全问题,但是为什么我们不换成堵塞队列呢??

答:

以上代码中,这两行代码都有可能发生阻塞,就会发生两把锁,多个线程,以容易出现死锁的情况.如果想要避免这种情况,就需要精心设计出加锁顺序,就会使代码的复杂程度增加

实现定时器,除了使用优先级队列这种方式来实现,还可以使用"时间轮"这种数据结构来实现

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值