多线程与高并发(一)-- 基础概念

作者: 我犟不过你 | 来源:发表于2020-08-19 15:56 被阅读0次

    1、什么是线程

    推荐一个不错的进程、线程关系连接

    Java monitor机制,写的很详细,本文不介绍了https://www.cnblogs.com/qingshan-tang/p/12698705.html

    com.lang.Thread类,感兴趣同学自行阅读该源码

    1.1 进程

    进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程参考资料

    1.1.1 进程切换

    进程从硬盘读取我们的程序代码,这个时候是比较费时的,CPU不会阻塞在这里等着,而是切换到其他进程,当数据加载完成是,CPU会收到个中断,继续执行这个请求。

    对于单核CPU而言,短时间内会执行多少进程,造成并行的错觉,实际可以理解为是并发的操作。

    1.1.2 内核态与用户态

    进程分为用户进程和内核进程两种。为了安全,用户进程是受限的,它不能随意访问资源、获取资源。所以,由内核进程负责管理和分配资源,它具有最高权限,而用户进程使用被分配的资源。且,操作系统必须能够在有需要的时候能立即切换回内核进程(通过中断),只有这样,操作系统才能有安全感。

    1.1.3 用户态向内核态切换

    1、发生系统调用时

    2、产生异常时

    3、外设产生中断时

    这里不做过多解释了,感兴趣同学自行学习linux原理。

    1.3 线程

    线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。百度百科

    1.3.1 进程与线程的关系

    1、一个进程中可以同时存在多个线程;

    2、各个线程之间可以并发执行;

    3、各个线程之间可以共享当前线程中的地址空间和文件等资源;

    4、线程是调度的基本单位,而进程则是资源拥有的基本单位;

    5、当进程只有一个线程时,可以认为进程就等于线程;

    1.3.2 线程内存模型

    (1) 在看线程内存模型之前,我们首先看下计算机的硬件内存模型。


    硬件内存模型

    寄存器:寄存器部件,包括通用寄存器、专用寄存器和控制寄存器。通用寄存器又可分定点数和浮点数两类,它们用来保存指令执行过程中临时存放的寄存器操作数和中间(或最终)的操作结果。 通用寄存器是中央处理器的重要部件之一。

    高速多级缓存:用于解决CPU核心与内存的速度差异问题,CPU核心速度快,内存相比要慢很多,而缓存要比内存速度快。

    缓存协议:多级缓存的引入在多核CPU时代导致了缓存不一致的问题,这里需要引入MESI协议缓存解决这一问题,保证缓存中的信息与内存中的信息一致。MESI协议缓存这里不做讲解。

    (2)下一步我们看下java内存模型与线程模型的关系
    java程序的一次编写到处运行如何体现的?jvm内存模型屏蔽了不同硬件的内存模型。jvm内存模型规定,所有的变量都存储在jvm主内存中,即堆内存,这里指可共享的变量(new出来的实例化对象,数组等)。这里提出主内存与工作内存的概念,每个线程有自己的工作内存,保存自己私有的变量,线程从主内存获取变量的副本作为自己的私有变量,不允许直接操作主内存的变量。线程的工作内存也是独立的,无法操作其他线程的变量。


    jvm线程内存关系图

    (3)硬件内存模型与jvm内存模型的关系
    如下图所示,jvm内存模型和硬件内存模型相似,但并不完全相同。但是jvm的数据大部分会存储到硬件主内存中,部分会存储到寄存器或缓存中,这里只做简单说明。


    硬件内存模型与jvm内存模型关系

    2、线程实现

    2.1 继承Thread类

    static class ExtendThread extends Thread {
        @Override
        public void run() {
            System.out.println("继承Thread类方式");
        }
    }
    
    public static void main(String[] args) {
        //实例化一个对象
        ExtendThread extendThread = new ExtendThread();
        //start()执行
        extendThread.start();
    }
    

    结果:

    继承Thread类方式
    

    2.2 实现runnable接口

    static class ImplRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("实现Runnable接口方式");
        }
    }
    
    public static void main(String[] args) {
        new Thread(new ImplRunnable()).start();
    }
    

    结果:

    实现Runnable接口方式
    

    2.3 实现callable接口

    static class ImplCallable implements Callable {
    
        private String str;
    
        public ImplCallable(String str) {
            this.str = str;
        }
    
        @Override
        public Object call() throws Exception {
            System.out.println("实现callable接口方式");
            return str;
        }
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建目标对象
        ImplCallable mc = new ImplCallable("实现callable接口方式");
        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(2);
        //提交执行
        Future<String> result = ser.submit(mc);
        //获取结果
        String msg = result.get();
        //关闭服务
        ser.shutdownNow();
        System.out.println("线程执行结果:" + msg);
    }
    

    结果:

    实现callable接口方式
    线程执行结果:实现callable接口方式
    

    2.4 lambda表达式简化实现

    提供一个jdk8的方式, lambda表达式。

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("lambda表达式简化实现");
        }).start();
    }
    

    结果:

    lambda表达式简化实现
    

    3、常用方法

    3.1 start()

    用于启动线程,具体请看以下源码内容

    public synchronized void start() {
        //判断线程状态是否为0,默认初始化为0
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        //通知组此线程即将启动,以便可以将其添加到组的线程列表中,修改未启动和已启动数量
        group.add(this);
        //启动状态默认为false
        boolean started = false;
        try {
            //调用native方法,底层jvm开启异步线程,并调用run方法
            start0();
            //线程已启动
            started = true;
        } finally {
            try {
                if (!started) {
                    //移除线程组中的该线程,并修改未启动线程数
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
               //将start0方法抛出的异常加入堆栈
            }
        }
    }
    

    3.2 run()

    通常需要重写该方法,线程需要执行的具体内容。

    public void run() {
        if (target != null) {
            //这里执行我们自己重写的run方法
            target.run();
        }
    }
    

    3.3 currentThread()

    返回当前执行线程的Thread对象

    public static native Thread currentThread();
    

    3.4 getName()

    获取当前线程名称

    public final String getName() {
        return name;
    }
    

    3.5 setName()

    给线程设置自定义名称,一定要在start方法前执行。

    public final synchronized void setName(String name) {
        //健康检查,这不做过多研究
        checkAccess();
        //非空验证
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;
        //当状态不为0,表示线程已启动,不能设置名字
        if (threadStatus != 0) {
            //调用底层方法,不做研究
            setNativeName(name);
        }
    }
    

    3.6 yield()

    放弃当前处理器的占用

    public static native void yield()
    

    3.7 join()

    在当前执行线程a中,另一个线程b调用该方法,则线程a进入阻塞状态,直到线程b执行完毕,线程a继续执行。参照下面示例:

    public class Join extends Thread {
    
    public static void main(String[] args) throws InterruptedException {
        Join join = new Join();
        join.setName("我是join线程");
        join.start();
        //调用join方法
        join.join();
    
        System.out.println("我是main线程");
    }
    
    @Override
    public void run() {
        for (int i=0; i<3; i++) {
            System.out.println(Thread.currentThread().getName() + "i=" + i) ;
        }
    }
    

    }
    执行结果:

    我是join线程i=0
    我是join线程i=1
    我是join线程i=2
    我是main线程
    

    3.8 sleep()

    阻塞当前线程指定的毫秒数

    public static native void sleep(long millis) throws InterruptedException;
    

    3.9 isAlive()

    判断当前线程是否存活

    public final native boolean isAlive();
    

    4、线程状态

    Thread类中定义了state枚举,可以通过getState()获取

    public enum State {
        /**
         * 初始
         */
        NEW,
        /**
         * 可运行
         */
        RUNNABLE,
        /**
         * 阻塞
         */
        BLOCKED,
        /**
         * 等待
         */
        WAITING,
        /**
         * 超时等待
         */
        TIMED_WAITING,
        /**
         * 终止
         */
        TERMINATED;
    }
    

    下图是通过idea集成plantuml插件画的,无法控制布局,请各位忍受,后面的图使用visio。


    线程状态流转图

    5、线程同步

    多个线程同时调用同一个对象,或者修改同一个变量,为了保证安全性与准确性,引入了同步的概念。关于线程同步大概归纳整理以下几种方式。

    举个例子:
    有一个库存类,原本100个库存,现在启动多线程增加100个库存,在没有线程同步的情况下。

    /**
     * 库存
     */
    static class Inventory {
    
        //库存数量
        private int num = 100;
    
        //增加库存
        public void add(int n) {
            num += n;
            System.out.println("增加库存后的数量=" + num);
        }
    }
    
    public static void main(String[] args) {
        //初始化库存
        Inventory inventory = new Inventory();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                inventory.add(1);
            }).start();
        }
    }
    

    取最后几条结果,发现库存最终并没有达到预想的200个:

    增加库存后的数量=179
    增加库存后的数量=177
    增加库存后的数量=177
    增加库存后的数量=175
    增加库存后的数量=174
    增加库存后的数量=174
    增加库存后的数量=171
    增加库存后的数量=169
    增加库存后的数量=167
    

    Process finished with exit code 0

    5.1 synchronized同步方法

    使用同步方法的方式看看能否解决这个问题,在add方法增加synchronized,锁住该方法。

    /**
     * 库存
     */
    static class Inventory {
    
        //库存数量
        private int num = 100;
    
        //增加库存
        public synchronized void add(int n) {
            num += n;
            System.out.println("增加库存后的数量=" + num);
        }
    }
    
    public static void main(String[] args) {
        //初始化库存
        Inventory inventory = new Inventory();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                inventory.add(1);
            }).start();
        }
    }
    

    查看结果,发现此时结果按照顺序展示,达到预期。

    ...
    增加库存后的数量=192
    增加库存后的数量=193
    增加库存后的数量=194
    增加库存后的数量=195
    增加库存后的数量=196
    增加库存后的数量=197
    增加库存后的数量=198
    增加库存后的数量=199
    增加库存后的数量=200
    

    Process finished with exit code 0

    5.2 synchronized同步代码块

    使用同步代码块的方式解决该问题,在add方法内部加入同步代码块,如下代码所以

    /**
     * 库存
     */
    static class Inventory {
    
        //库存数量
        private int num = 100;
    
        //增加库存
        public void add(int n) {
    
            synchronized (this) {
                num += n;
                System.out.println("增加库存后的数量=" + num);
            }
        }
    }
    
    public static void main(String[] args) {
        //初始化库存
        Inventory inventory = new Inventory();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                inventory.add(1);
            }).start();
        }
    }
    

    取部分结果,能够得到预期结果

    ...
    增加库存后的数量=196
    增加库存后的数量=197
    增加库存后的数量=198
    增加库存后的数量=199
    增加库存后的数量=200
    

    5.3 volatile关键字

    网上很多说通过volatile关键字解决线程同步的问题,但是我想说,这个关键字虽说能够阻止指令重排序,让变量线程间可见,但并不能完全实现线程同步,代码来看下。

    /**
     * 库存
     */
    static class Inventory {
    
        //库存数量
        private volatile int num = 100;
    
        //增加库存
        public void add(int n) {
                num += n;
                System.out.println("增加库存后的数量=" + num);
        }
    
        //减少库存
        public void sub(int n) {
                num -= n;
                System.out.println("减少库存后的数量=" + num);
            }
    }
    
    public static void main(String[] args) {
        Inventory inventory = new Inventory();
        for(int i = 0;i<100;i++) {
            new Thread(() -> {
                inventory.add(1);
            }).start();
        }
        for(int i = 0;i<100;i++) {
            new Thread(() -> {
                inventory.sub(1);
            }).start();
        }
    }
    

    结果,后面会专门针对volatile进行讲解:

    ...
    减少库存后的数量=110
    减少库存后的数量=111
    减少库存后的数量=105
    减少库存后的数量=103
    减少库存后的数量=101
    减少库存后的数量=100
    减少库存后的数量=102
    

    5.4 重入锁

    /**
     * 库存
     */
    static class Inventory {
    
        //初始化ReentrantLock实例
        Lock lock = new ReentrantLock();
    
        //库存数量
        private int num = 100;
    
        //增加库存
        public void add(int n) {
            //加锁
            lock.lock();
            try {
                num += n;
                System.out.println("增加库存后的数量=" + num);
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    
        //减少库存
        public void sub(int n) {
            //加锁
            lock.lock();
            try {
                num -= n;
                System.out.println("减少库存后的数量=" + num);
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }
    
    public static void main(String[] args) {
        Inventory inventory = new Inventory();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                inventory.add(1);
            }).start();
        }
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                inventory.sub(1);
            }).start();
        }
    }
    

    5.5 阻塞队列

    定义LinkedBlockingQueue阻塞队列,简单实现增加一个,消费一个的线程同步功能

    /**
     * 定义一个元素的队列,通过阻塞达到新增一个消费一个的效果
     * 数组元素定义多个时,因消费线程是100个独立线程,执行时间顺序存在差异,不能保证完全顺序执行
     */
    final static LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(1);
    
    /**
     * 库存
     */
    static class Inventory {
    
        //增加库存
        public void add(int n) {
            try {
                System.out.println("开始增加第" + n + "库存。。。。。");
                //压入队列
                queue.put("第" + n + "库存。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        //减少库存
        public void sub() {
            try {
                //取出数据
                System.out.println("[取出]====" + queue.take());
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    }
    
    public static void main(String[] args) {
        Inventory inventory = new Inventory();
    
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                inventory.add(i);
            }
        }).start();
    
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                inventory.sub();
            }).start();
        }
    }
    

    结果:

    ...
    增加库存后的数量=101
    取出队列中的数量=101
    减少库存后的数量=100
    增加库存后的数量=101
    取出队列中的数量=101
    减少库存后的数量=100
    增加库存后的数量=101
    

    5.6 原子类

    使用原子类的形式实现线程同步,内部实现使用了CAS(compareAndSwapInt自旋锁)机制。

    /**
     * 库存
     */
    static class Inventory {
    
        private AtomicInteger num = new AtomicInteger(100);
        //增加库存
        public void add(int n) {
            //增加库存
            num.incrementAndGet();
            System.out.println("增加库存后的数量=" + num);
        }
    }
    
    public static void main(String[] args) {
        Inventory inventory = new Inventory();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                inventory.add(1);
            }).start();
        }
    }
    

    结果:

    ...
    增加库存后的数量=196
    增加库存后的数量=197
    增加库存后的数量=198
    增加库存后的数量=199
    增加库存后的数量=200
    

    Process finished with exit code 0

    5.7 局部变量

    使用局部限量形式,模拟两个线程分别处理,相互之间不进行影响。

    /**
     * 库存
     */
    static class Inventory {
    
        private ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 100);
    
        //增加库存
        public void add(int n, String threadName) {
            //增加库存
            num.set(num.get() + n);
            System.out.println("线程:" + threadName + ",增加库存后的数量=" + num.get());
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                Inventory inventory = new Inventory();
                for (int j = 0; j < 10; j++) {
                    inventory.add(1,Thread.currentThread().getName());
                }
            }).start();
        }
    }
    

    结果:

    线程:Thread-0,增加库存后的数量=101
    线程:Thread-0,增加库存后的数量=102
    线程:Thread-0,增加库存后的数量=103
    线程:Thread-0,增加库存后的数量=104
    线程:Thread-0,增加库存后的数量=105
    线程:Thread-0,增加库存后的数量=106
    线程:Thread-0,增加库存后的数量=107
    线程:Thread-0,增加库存后的数量=108
    线程:Thread-0,增加库存后的数量=109
    线程:Thread-0,增加库存后的数量=110
    线程:Thread-1,增加库存后的数量=101
    线程:Thread-1,增加库存后的数量=102
    线程:Thread-1,增加库存后的数量=103
    线程:Thread-1,增加库存后的数量=104
    线程:Thread-1,增加库存后的数量=105
    线程:Thread-1,增加库存后的数量=106
    线程:Thread-1,增加库存后的数量=107
    线程:Thread-1,增加库存后的数量=108
    线程:Thread-1,增加库存后的数量=109
    线程:Thread-1,增加库存后的数量=110
    

    6、synchronized锁升级

    在jdk1.6之前,synchronized的锁只有一种方式,即重量级锁;在之后引入了偏向锁,轻量级锁来缓解锁竞争问题。从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。

    6.1 java对象

    在讲锁之前,先简单介绍下java对的概念。如下图,java对象分为对象头,对象体,对齐字段。


    java对象

    6.1.1 对象头

    当对象为普通对象时,对象头只包含Mark Word 和Klass Word,当时数组对象时,会包含数组长度。
    Mark Word:先看下面一张图,图片来源:https://www.cnblogs.com/ZoHy/p/11313155.html

    Mark Word 不同锁下的信息

    从上图看到,在Mark Word中,包含以下主要信息:
    1.locked:两位二进制的锁状态标志位,配合biased_lock表示java对象各阶段不同锁状态。
    2.biased_lock:只占一位的二进制偏向锁标记,1表示启用偏向锁,0表示未启用。
    3.identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor(也可称为监视器)中。
    4.thread:54位的持有偏向锁的线程id
    5.epoch:偏向锁的时间戳。
    6.ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
    7.ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
    8:age:4位的Java对象年龄。

    Klass Word:存储一个地址,长度取决于系统位数,32位和64位,该地址指向方法区中类的元数据信息。
    这里放个jdk1.8的jvm模型图,看一下堆与方法区的关系。
    可参考后面的链接学习jvm模型知识:https://www.cnblogs.com/paddix/p/5309550.html

    jvm内存模型

    数组长度:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

    6.1.2对象体

    主要存放对象的属性值。

    6.1.3对齐字节

    也可称为补齐区域,如果对象总大小不是4字节的整数倍,会填充上一段内存地址使之成为整数倍。

    6.3 锁升级

    大概了解什么是对象头以及Mark Word后,我们来讨论下锁升级的过程。在jdk1.6之前,只有重量级锁,通过阻塞和唤醒的形式,需要cpu不停地切换状态,这个过程有可能比用户代码执行的时间还长,这即是1.6之前效率低下的原因。1.6之后引入偏向锁和轻量级锁解决效率低下这一问题。


    锁升级过程及状态流转

    6.3.1 四种锁Mark Word

    无锁时Mark Word的内容 偏向锁时Mark Word的内容 轻量级锁时Mark Word的内容 重量级锁时Mark Word的内容

    6.3.2 四种锁的优缺点对比

    优缺点对比

    6.3.3 锁升级过程分析

    图片来源https://tech.souyunku.com/?p=37386
    偏向锁

    偏向锁升级
    1.当线程访问同步块,首先检查当前对象头是否包含线程1的id,如果没有,则通过CAS替换Mark Word,替换成功则升级为偏向锁。
    2.升级为偏向锁时,会将Mark Word中的内容设置为当前线程id,继续执行同步代码块。这是线程2也来访问同步块,首先检查对象头是否包含线程2的id,如果没有,则会将Mark Word的内容替换为线程2的id,失败的话则意味着存在锁竞争。
    3.存在竞争意味着需要升级偏向锁为轻量级锁,在此之前需要先撤销对象的偏向锁(在安全点,没有线程操作字节码),变为无锁的状态,然后升级为轻量级锁。

    轻量级锁

    轻量级锁膨胀
    1、在偏向锁撤销后,需要升级为轻量级锁,线程1和线程2同时访问同步代码块,通过CAS修改对象的Mark Word,将其修改成每个线程自己的LockRecord,成功则得到锁。失败则表示已经被其他线程占用,继续通过自旋获取锁。
    2、当自旋满足以下两个条件之一时,发生锁膨胀,升级为重量级锁。
    1)在jdk1.6前,默认10次,可通过-XX:PreBlockSpin来修改,或者自旋线程数超过CPU核数的一半。
    2)jdk1.6之后,引入了自适应自旋锁,次数并非一成不变。根据获取锁的成功率来决定是否能有更长的等待时间。
    升级重量级锁后,会修改对象Mark Word,同时阻塞,并等待占有锁线程释放锁并唤醒其他线程,阻塞线程被唤醒后,继续开始争夺锁访问同步块。

    7、synchronized静态同步方法与非静态同步方法

    1.同步锁和对象锁是否互斥
    同步静态方法,是类锁
    同步非静态方法,是对象所
    这两种锁是不同的,所以相互之前不会产生互斥。
    通过一段代码进行验证:

    public class SynchronizedStaticAndNonStatic {
    
    public static int count = 0;
    
    public static synchronized void inc() throws InterruptedException {
        count++;
        //这里将静态方法阻塞,验证是否能访问到inc2()
        Thread.sleep(10000);
        System.out.println("结果1: " + count);
    }
    
    public synchronized void inc2() {
        count++;
        System.out.println("结果2: " + count);
    }
    
    public static void main(String[] args) throws InterruptedException {
        //调用静态同步方法
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                //同步静态方法
                try {
                    SynchronizedStaticAndNonStatic.inc();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        //调用非静态同步方法前先进行阻塞一秒,确保静态同步方法在执行阻塞过程中
        Thread.sleep(1000);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                //同步动态方法
                SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
                t.inc2();
            }).start();
        }
      }
    }
    

    结果分析:

    结果2: 3
    结果2: 4
    结果2: 3
    结果2: 5
    结果2: 6
    结果1: 6
    结果1: 7
    结果1: 8
    结果1: 9
    结果1: 10
    

    当结果一进入sleep后,并没有对非静态方法inc2进行阻塞,inc2率先执行完毕。得出在静态方法加同步锁,并不会影响其他对象的非静态同步方法。类锁和对象锁并不互斥。
    2.对象锁的同步方法是否互斥

    public synchronized void inc3(String tName) throws InterruptedException {
        System.out.println("结果3: " + tName);
        Thread.sleep(10000);
    }
    
    public synchronized void inc4(String tName) {
        System.out.println("结果4: " + tName);
    }
    
    public static void main(String[] args) {
        SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    t.inc3(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                t.inc4(Thread.currentThread().getName());
            }).start();
        }
    }
    

    结果分析:在第一条结果3出来之前,休眠了10秒,结果4才进行打印,同一个对象在两个线程中分别访问该对象的两个同步方法是互斥的。

    结果3: Thread-0
    sleep 10s...
    结果4: Thread-3
    结果4: Thread-5
    结果4: Thread-4
    结果3: Thread-2
    结果3: Thread-1
    

    3.非静态锁方法,不同对象在两个线程中调用同一个同步方法

    public synchronized void inc5(String tName) throws InterruptedException {
        System.out.println("结果5: " + tName);
        Thread.sleep(10000);
        System.out.println("结果5操作完成: " + tName);
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                //同步动态方法
                SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
                try {
                    t.inc5(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    

    结果分析:“结果5”的提示几乎同时输出,然后sleep了10秒后,才出现操作完成,证明一个对象阻塞后,并不会与其他对象的同步方法互斥。

    结果5: Thread-0
    结果5: Thread-4
    结果5: Thread-3
    结果5: Thread-2
    结果5: Thread-1
    结果5操作完成: Thread-3
    结果5操作完成: Thread-4
    结果5操作完成: Thread-0
    结果5操作完成: Thread-1
    结果5操作完成: Thread-2
    

    4.用类直接在两个线程中调用两个不同的同步方法

     public static synchronized void inc() throws InterruptedException {
        count++;
        Thread.sleep(10000);
        System.out.println("结果1: " + count);
    }
    
    public static synchronized void inc6() throws InterruptedException {
        count++;
        Thread.sleep(10000);
        System.out.println("结果6: " + count);
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    SynchronizedStaticAndNonStatic.inc();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    SynchronizedStaticAndNonStatic.inc6();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    

    结果分析:无论结果1还是结果6,每条结果间隔都是10秒,用类直接在两个线程中调用两个不同的同步方法是互斥的。

    结果1: 1
    结果6: 2
    结果6: 3
    结果6: 4
    结果6: 5
    结果6: 6
    结果1: 7
    结果1: 8
    结果1: 9
    结果1: 10
    

    8、synchronized锁重入

    问题提出:当一个线程获取了一个对象的锁,在方法内部是否能够获取其他方法的锁?答案是肯定的,Synchronized是可重入锁。
    实现原理:通过monitor机制,monitor通过维护一个计数器来记录锁的获取,重入,释放情况。
    看个例子:

    static class Inventory {
    
        //库存数量
        private int num = 100;
    
        //增加库存
        public synchronized void add(int n) {
            num += n;
            System.out.println("增加库存后的数量=" + num);
            sub(n);
        }
    
        //增加库存
        public synchronized void sub(int n) {
            num -= n;
            System.out.println("减少库存后的数量=" + num);
        }
    }
    
    public static void main(String[] args) {
        //初始化库存
        Inventory inventory = new Inventory();
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
    

    结果:线程调用同步方法add,其内部又调用了同步方法sub,看到结果正常返回,仍然能够获取到锁。

    增加库存后的数量=101
    减少库存后的数量=100
    

    还有另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。

    static class Inventory extends InventoryParent{
    
        //增加库存
        public synchronized void add(int n) {
            super.num += n;
            System.out.println("增加库存后的数量=" + super.num);
            super.sub(n);
        }
    }
    
    static class InventoryParent {
    
        //库存数量
        private int num = 100;
    
        //增加库存
        public synchronized void sub(int n) {
            num -= n;
            System.out.println("减少库存后的数量=" + num);
        }
    }
    
    public static void main(String[] args) {
        //初始化库存
        Inventory inventory = new Inventory();
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
    

    结果:

    增加库存后的数量=101
    减少库存后的数量=100
    

    9、volatile

    参考https://baijiahao.baidu.com/s?id=1663045221235771554&wfr=spider&for=pc
    很详细

    volatile 的特性

    1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
    2、禁止进行指令重排序。(实现有序性)
    3、volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

    10、wait notify (面试高频)

    是java.lang.Object下提供的方法,其中有五个相关方法

    //随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
    public final native void notify();
    //使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
    public final native void notifyAll();
    //超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
    public final native void wait(long timeout) throws InterruptedException;
    //对于超时时间更细力度的控制,单位为纳秒
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        if (nanos > 0) {
            timeout++;
        }
        wait(timeout);
    }
    //使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
    public final void wait() throws InterruptedException {
        wait(0);
    }
    

    使用wait和notify实现数字和英文字母的交替打印

    static class Print {
        /**
         * 当值为1时打印数字,当值为2时打印字母
         */
        private int flag = 1;
        private int count = 1;
    
        public synchronized void printNum() {
            if (flag != 1) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print(count);
            flag = 2;
            notify();
        }
    
        public synchronized void printChar() {
            if (flag != 2) {
                //打印字母
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print((char) (count - 1 + 'A'));
            count++;
            flag = 1;
            notify();
        }
    }
    
    public static void main(String[] args) {
        Print print = new Print();
        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                print.printNum();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                print.printChar();
            }
        }).start();
    }
    

    结果:

    1A2B3C4D5E6F7G8H9I10J11K12L13M14N15O16P17Q18R19S20T21U22V23W24X25Y26Z

    相关文章

      网友评论

        本文标题:多线程与高并发(一)-- 基础概念

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