zoukankan      html  css  js  c++  java
  • 一文看尽Java-多线程概念

    一、前言

        主要讲解一下多线程中的一些概念,本文之后就开始针对JUC包的设计开始解读;

    二、概念

        线程安全

        1.存在共享数据(临界资源);2.多个线程同时操作共享数据;只有同时出现这两种情况的时候才会造成线程安全问题;

        解决线程安全

        同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据以后在对共享数据进行操作;

        多线程特性

        原子性

        现在的操作系统主要是通过时间分片的形式来管理线程或者进程,Java编程语言一句语言需要多条CPU指令来完成,Java在多线程切换的时候由于不满足原子性的特征,导致共享变量产生意料之外的结果;典型的count+=1,如下图,共享变量的count的指最终结果是1而不是2;

       

        可见性

        在多处理器(CPU)系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存,整体结构如下图:

        

        在单核CPU的情况下,CPU缓存与内存数据一致性问题容易处理,因为所有线程的操作都是针对同一个CPU的缓存,一个线程对缓存的写对于另外的线程一定是可见的,整体的执行情况如下图:

        

        在多CPU的情况下,每个CPU都有自己的缓存,这个时候每个共享变量在CPU中的缓存都是不可见的,这个时候就产生了CPU缓存与内存数据一致性的问题,整体执行的情况如下图,由于count变量分别在不同的CPU上执行,相互看不到对方的操作,这个时候变量count就会不一致,产生意料之外的结果,针对这种我也写了一个demo;

       

    /**
     * @author wtz
     *
     * 线程可见性demo
     */
    public class ThreadVisiable {
    
        private int count = 0;
    
        private void add() {
            int retry = 0;
            while (retry < 10000) {
                count += 1;
                retry++;
            }
        }
    
        public int sumCount() throws InterruptedException {
    
            Thread thread1 = new Thread(() -> {
                add();
            });
    
            Thread thread2 = new Thread(() -> {
                add();
            });
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            return count;
        }
    
        public static void main(String[] args) throws InterruptedException {
            ThreadVisiable threadVisiable = new ThreadVisiable();
            int count = threadVisiable.sumCount();
            System.out.println(count);
        }
    
    }
    View Code

        有序性

        编译器为了优化性能,有的时候会改变程序中语句的执行顺序,在Java经典的双重检查创建单例模式,就是其中的一个体现,代码如下图:

    /**
     * @author wtz
     * <p>
     * 双重锁定单例模式 指令重排
     */
    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
    }
    View Code

        代码整体上看起来无任何瑕疵,但是实际这个方法并不完美,问题出在new的操作上,正常情况下new Singleton()执行的操作如下步骤:

        1.在内存中分配一块空间;

        2.在内存上初始化Singleton对象;

        3.将内存地址赋值给singleton变量;

        经过编译器优化以后可能是这个样子:  

        1.在内存中分配一块空间;

        2.将内存地址赋值给singleton变量;

        3.在内存上初始化Singleton对象; 

        优化以后当CPU时间片切换时间刚好是线程B判断为空的时候,这个时候singleton此时不为空,不需要进入锁中,这个时候就返回为初始化的singleton,整体性执行过程如下图:

       

         

        竞态条件&临界区

        当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区;

        互斥锁

        互斥锁解决了并发程序中的原子性问题,保证同一时刻只有一个线程执行,保证了一个或多个操作在CPU执行的过程中不被中断,Java原生语言主要是通过synchronized实现互斥锁;

        Java内存模型 

        Java内存模型主要是为了解决内存可见性的问题,使用内存模型约束了CPU缓存和编译优化;Java内存模型(JMM)从不同的角度来说都可以说很多的东西,比如从线程角度来说,JMM规范不同线程之间线程通信的问题,从操作系统的角度来说,JMM规范了工作线程与内存之间访问的问题;我们主要从程序员角度来看这个问题的话我认为可以从三方面说起:

        1.volatile、synchronized和final语义;

        2.JUC并发包;

        3.happens-before;

        我们主要说第3点,第1,2点以后在补充,我们先要明白一些概念,才能更好的理解后面的一些内容;happens-before主要有8个原则,我们通俗的话来讲讲:

        1.程序的顺序性,单线程的每个前面的操作优先于后面的操作;

        2.volatile,对于volatile修饰的变量,写的操作一定优先于读的操作,也就是说对变量写操作对于后续的读操作都是可见的;

        3.锁,解锁的操作优先于加锁的操作,在Java锁指的就是synchronized,变量在解锁之前的操作,在重新加锁之后一定可以看到;

        4.传递性,A优先于B,B优先于C,则A优先于C;

        5.线程开始原则,主线程A启动子线程B,则子线程B能够看到主线程A在启动B子线程之前的操作;

        6.线程终止原则,主线程A等待子线程B完成,当子线程B完成以后,主线程A能够看到子线程的B的操作;

        7.线程中断原则,对线程interrupt()方法优先于发生被中断线程检测到中断事件的发生;

        8.对象终结规则,构造函数的执行一定优先于它的finalize方法;

        等待-通知机制

        等待-通知机制主要是为了处理循环等待造成的CPU消耗问题,主要有以下两个步骤:

        1.线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;

        2.当要求的条件满足时,通知等待的线程,重新获取互斥锁;

        Java原生语言主要是通过synchronized + wait + notify/notifyAll实现;

        活跃性

        死锁

        死锁的定义一组相互竞争资源的线程因相互等待,导致永久阻塞的现象,发生死锁必备的四个条件:

        1.互斥,共享资源同时只有占用一个线程;

        2.占有且等待,线程A获取共享资源X,在等待共享资源Y的时候,不是释放共享资源Y;

        3.不可抢占,其他线程不能抢占线程A获取的共享资源;

        4.循环等待,线程A等待线程B获取的共享资源,线程B等待线程A获取的共享资源;

        只要破坏其中任意一个条件就可以跑坏死锁;

        活锁

        线程之间互相谦让,导致线程无法执行下去,解决方案通过给线程随机等待一个时间;

        饥饿

        线程不能正常的访问共享资源,并且无法执行下去,解决线程饥饿的办法:

        1.保证资源的公平性,也就线程的优先级一样;

        2.保证资源充足;

        3.避免线程长时间占用锁执行;

     三、结束

      欢迎大家加群438836709!欢迎大家关注我!

         

  • 相关阅读:
    MongoDB数据库学习总结
    windows及linux环境下永久修改pip镜像源的方法
    enumerate枚举函数
    sum求和函数
    zip拉链函数
    python实现面对面快传
    redis实现发布订阅
    python自定义classmethod
    Nginx为什么支持那么高的并发量?
    java进阶(28)--Map集合
  • 原文地址:https://www.cnblogs.com/wtzbk/p/11489059.html
Copyright © 2011-2022 走看看