美文网首页
Java多线程

Java多线程

作者: Burning燃烧 | 来源:发表于2019-10-29 15:48 被阅读0次

    一、基本概念

    1、CPU核心数与线程数的关系

    一般来说是1:1的关系 即1个核心对应1个线程,但我们在程序中可以创建多个线程的原因是由于CPU的时间片调度

    2、CPU时间片轮转(RR调度)

    把CPU的运行时间进行切片分别轮转到各个线程

    3、进程和线程

    进程:操作系统对资源分配的最小单位

    线程:CPU调度的最小单位

    进程>线程,线程不能单独存在,必须要依附于进程存在

    线程数量限制:在操作系统层面Linux限制为1000,Windows限制为2000

    4、并行和并发

    (1)并行:同时执行(例如高速公路的4车道,并行数就是4)

    (2) 并发:单位时间内的吞吐量(与并行的关键区别就在于时间限制),CPU的并发能力取决于CPU时间片的切换速度

    二、多线程

    1、线程的启动方式

    Java中有三种线程启动的方式

    (1)、继承Thread类

    image

    (2)、实现Runnable接口

    image

    (3)、实现Callable接口

    image

    实现Runnable和Callable接口的不同在于,实现Callable是允许有返回值的;以上三种创建线程的方式,最后都是通过Thread类进行开启,在Java中只有Thread类是线程的创建和实现类。

    2、线程的结束方式

    (1)、stop方式

    image
    stop方法被官方定义为弃用的;因为stop方法会强制退出线程,可能会导致线程中的其它资源未被正确释放等安全问题。
    

    (2)、suspend方式

    将线程挂起,同样也是被JDK弃用的;因为susbend方法不会释放锁,容易导致死锁发生
    

    (3)、interrupt

    Thread的成员方法,不会立刻导致线程退出,只会将线程的中断标志位置为true
    

    (4)、interrupted

    Thread的静态方法,返回boolean值,除具有isInterrupted正常功能外,还会重置中断标志位为false
    

    (5)、isInterrupted

    Thread的成员方法,判断线程是否被中断
    一个线程退出的例子:
    
    image
    调用interrupt方法将线程标志位置为true,用isInterrupted检测当前的线程标志位;当然在实际开发当中也可以通过一些boolean标志位进行控制。
    

    3、线程的生命周期、线程调度、等待唤醒、ThreadLocal相关

    (1)生命周期

    线程的五种基本状态:新建、就绪、运行、阻塞、死亡

    image

    当我们new一个线程并调用start方法时,该线程就从新建到就绪(Runnable)状态;当线程获取到CPU的时间片后进入到执行状态(Running);该线程调用yield方法或者时间片执行完毕后,从Running切换到Runnable状态;该线程调用wait(等待,释放锁)、sleep(休眠、不释放锁)、join(放弃执行让其他线程先执行)时会进入到阻塞(Blocked)状态;线程执行完毕后进入到死亡(Dead)状态。

    (2)yield方法

    使当前线程放弃时间片暂停执行,并执行其它线程;但由于CPU轮询速度较快,很可能马上又会轮询到当前线程,效果不明显

    注:yield方法不会释放锁

    (3)join方法

    主要作用是线程的同步,使得线程从并行改为串行执行,例如线程A中执行线程B的join方法,表示线程B执行完成后才会继续执行线程A

    eg:线程A、线程B、线程C三个线程,实现依次输出A、B、C中的内容

    image image

    join方法的实现原理:
    正常调用join方法实际上调用join(0)

    public final void join() throws InterruptedException {
            join(0);
        }
    

    join参数是一个delay时间,默认是0,但wait(0)不是等待0ms,而是一直等待。

    void join():当前线程等该加入该线程后面,等待该线程终止。
    void join(long millis):当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度。
    void join(long millis,int nanos):等待该线程终止的时间最长为 millis 毫秒 + nanos纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度。
    

    join的原理为当在线程A中调用了线程B的join方法,线程A会执行wait方法,释放锁并等待线程B的执行结束

    public final void join(long millis) throws InterruptedException {
            synchronized(lock) {
            long base = System.currentTimeMillis();
            long now = 0;
    
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    
            if (millis == 0) {
            //传入的是0 默认执行这个判断
                while (isAlive()) {//判断线程是否存活 native方法返回的boolean变量
                    lock.wait(0);//执行wait方法 释放锁 并等待
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    lock.wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
            }
        }
    

    (4)wait、notify、notifyAll方法

    wait、notify、notifyAll被用于线程之间的协作,等待和唤醒;这几个方法都是Object而不是Thread的
    注:为什么wait、notify、notifyAll要被设计成为Object下的方法而不是Thread的
    wait、notify、notifyAll使用的前提必须在同步代码块中,但Synchronized中的锁可以为任意对象,因此wait notify notifyAll放在所有类的父类Object中,方便管理。

    wait用于线程的等待,与sleep的区别在于wait会释放锁,sleep不会释放锁
    notify和notifyAll用于线程的唤醒,不会释放锁;notify用于唤醒一个线程,notifyAll会通知所有线程

    等待通知的标准范式
    1)等待方
    a 获取对象锁(同步)
    b 检查条件 条件不满足 wait
    c 条件满足 执行业务代码

    2)通知方
    a 获取对象的锁(同步)
    b 修改条件
    c 通知等待方

    以一个生产者消费者为例:

    package com.hzf.demo;
    
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.LinkedBlockingQueue;
    
    public class MainTest {
        public static void main(String[] args) throws InterruptedException,
                ExecutionException {
            LinkedBlockingQueue<Integer> linkedBolckingQueue = new LinkedBlockingQueue<>(
                    10);
            new ConsumerThread(linkedBolckingQueue).start();
            new ProducterThread(linkedBolckingQueue, 10).start();
        }
    
        private static class ConsumerThread extends Thread {
            private LinkedBlockingQueue<Integer> mQueue;
    
            private ConsumerThread(LinkedBlockingQueue<Integer> queue) {
                this.mQueue = queue;
            }
    
            @Override
            public void run() {
                super.run();
                while (true) {
                    //获取同步锁
                    synchronized (mQueue) {
                        while (!mQueue.isEmpty()) {
                            System.out.println("消费了1个");
                            mQueue.remove();
                            mQueue.notify();
                            try {
                                sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        try {
                            mQueue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    
        private static class ProducterThread extends Thread {
            private LinkedBlockingQueue<Integer> mQueue;
            private int mMaxSize;
    
            private ProducterThread(LinkedBlockingQueue<Integer> queue, int maxSize) {
                this.mQueue = queue;
                this.mMaxSize = maxSize;
            }
    
            @Override
            public void run() {
                super.run();
                while (true) {
                    //获取同步锁
                    synchronized (mQueue) {
                        while (mQueue.size() == mMaxSize) {
                            try {
                                mQueue.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        mQueue.add(1);
                        System.out.println("生产了1个");
                        // 如果生产量达到最大值 notify消费者消费
                        mQueue.notify();
                        try {
                            sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    
    }
    
    

    (5)ThreadLocal的使用

    4、Java线程锁的简单介绍

    Java锁分类.png

    (1)可重入锁&不可重入锁

    可重入锁:一个线程在外层方法中获取到了锁,进入内层方法后不需要再次获取锁
    Synchronized和ReentrantLock都是可重入锁

    (2)独享锁&共享锁

    ReentrantLock是独享锁;ReentrantReadWriteLock是共享锁
    独享锁也叫排它锁,指一个线程持有该锁之后,其它线程无法获取到该锁,持有锁的线程可以读、写数据
    共享锁是指该锁可以被多个线程持有,以ReentrantReadWriteLock为例,当为读操作时,所有的线程都可以并发执行进行读操作,但无法修改数据;当为写操作时,所有其它的读写线程都会被排斥,无法进行读写操作。

    (3)公平锁&非公平锁

    ReentrantLock可以通过其构造方法指定当前锁为公平还是非公平锁


    ReentrantLock.png

    公平锁指先申请的线程先拿到锁,按照申请顺序获取锁
    非公平锁不一定按照申请的顺序获取锁
    非公平锁的有点在于减少唤起线程的开销,整体的吞吐效率高,但处于等待的线程有可能会被饿死

    (4)乐观锁&悲观锁

    乐观锁和悲观锁是一种广义上的概念,乐观锁认为自己在操作数据的同时没有其它线程在修改数据,仅仅在更新数据的时候去判断有没有别的线程更改过当前数据(CAS算法,Java的原子类递增等操作都是通过CAS实现的);悲观锁认为自己在使用数据的同时一定会有别的线程修改数据,因此在获取数据的时候要先加锁,确保数据不会被其它线程锁修改,Java的Synchronized和Lock的实现类都是悲观锁。

    (5)偏向锁、轻量级锁、重量级锁

    偏向锁:是指同一段代码被一个线程多次获取,那么这个线程可以自动获取到锁,降低获取锁的代价;
    在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
    轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
    重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

    来自美团技术团队.png

    一篇美团技术团队关于锁的讲解:https://tech.meituan.com/2018/11/15/java-lock.html

    相关文章

      网友评论

          本文标题:Java多线程

          本文链接:https://www.haomeiwen.com/subject/hwnvvctx.html