美文网首页
“多线程”重点概念整理

“多线程”重点概念整理

作者: 落雨松 | 来源:发表于2018-12-09 14:35 被阅读0次

    一、volatile关键字内存可见性

    当程序运行时,JVM会为每一个执行任务的线程分配一个独立的缓存空间,用于提高效率。这个线程会先从主内存中拿到变量到自己的缓存中,然后将改变后的值提交到主内存,这是两个操作,如果在第一个操作后,第二个线程闯入,这个时候第二个线程从主内存中读取的变量就是未改变之前的变量,那么两个线程最后拿到的值便是不一样的,产生冲突。

    产生此种问题是因为这个变量并不是都在主内存中操作的,要解决这个问题,便是在“共享变量” 定义的时候在之前添加一个 volatile 关键字。

    图示:


    捕获.PNG

    二、原子变量-CAS算法

    volatile关键字保证内存可见性,也就是说可以保证变量都在主内存中进行,但是不能保证原子性。

    (一)那么什么是原子性问题?

    举例:i++操作
    i++操作实际上是“读-改-写”三个操作:

    int temp = i;
    i=i+1;
    temp = i;
    

    当我们运行程序:

    int i = 10;
    i=i++;  
    System.out.println(i); //这个时候输出打印的应该是 10 
    

    原因在于:此时 i++操作返回的是底层“读”的时候的 i ,而不是“写” 完后的 i
    : 这就是原子性问题

    (二)原子变量(解决原子性问题)

    为了解决这个问题,jdk 1.5之后,在java.util.concurrent.atomic包下提供了常用的数据类型的原子变量(最近更新:不过atomic...类也有它的局限性,比如AtomicLong的实现方式是内部有个value 变量,当多线程并发自增,自减时,均通过CAS 指令从机器指令级别操作保证并发的原子性。唯一会制约AtomicLong高效的原因是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicLong效率降低,Java8对此有了新的解决方案:LongAdder)。

    它的底层实现是:
    1、首先用volatile保证内存的可见性
    2、然后用CAS算法保证数据原子性,CAS算法是硬件对于并发操作共享数据的支持,CAS包括三个操作:
    ①内存值(从主存中读取值)、
    ②预估值(读取旧值)、
    ③更新值(如果满足条件就替换主内存中的值)
    只有当内存值==预估值的时候,内存值才能够等于更新值,否则将不作任何操作。
    demo:

    /**
     * @Author : WJ
     * @Date : 2018/11/18/018 12:26
     * <p>
     * 注释:
     */
    public class Test2 {
    
        public static void main(String [] a) throws InterruptedException {
            AtomicDemo atomicDemo = new AtomicDemo();
            for (int i = 0; i < 10; i++) {
                new Thread(atomicDemo).start();
            }
        }
    }
    class AtomicDemo implements Runnable {
    
        //用volatile保证内存可见性:无法保证原子性
        //private volatile int number = 0;
    
        //使用原子变量解决原子性问题
        private AtomicInteger number = new AtomicInteger();
    
        public void run() {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getAdd());
        }
    
        public int getAdd(){
            //使用原子变量提供的相关API进行对原子变量数据的操作,API文档里面有详细介绍
            //这里使用“从主内存中获取和自增”一起的方法返回number自增的值。
            return number.getAndIncrement();
            //return number++;
        }
    }
    

    三、ConcurrentHashMap锁分段机制

    Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。
    ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对与多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用“锁分段”机制替代 Hashtable 的独占锁。进而提高性能。
    此包还提供了设计用于多线程上下文中的 Collection 实现:
    ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

    JDK1.8以后ConcurrentHashMap由锁的分段机制变为CAS。
    CopyOnWriteArrayList "写入并复制" 是个复合操作,当每次写入时,都会复制。添加操作比较多时效率较低。并发迭代操作多时,可以提高效率。

    这里推荐一篇博客详细介绍了ConcurrentHashMap:
    https://blog.csdn.net/yansong_8686/article/details/50664351

    四、CountDownLacth 闭锁

    闭锁是一个同步工具类,它是用来保证一组线程全部执行完成才能进行下一步操作的工具。就比如一组多线程,我们想要获取他们全部执行的时间,寻常操作时无法完成的,因为获取时间存在于主线程,随时可能拿到cpu的使用权利,所以这个工具类,就可以实现要全部线程执行完,才执行下一步。

    闭锁状态包含一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示已经有一个事件已经发生了。而await方法等待计数器达到0,这表示所有需要等待的事件都已经发生。如果计数器的值非0,那么await会一直阻塞直到计数器为0,或者等待中的线程中断或者超时。

    当然采用join方式也可以完成,但是效率是远远不及的。

    demo:

    /**
     * @Author : WJ
     * @Date : 2018/11/18/018 12:26
     * <p>
     * 注释:
     */
    public class Test2 {
    
        public static void main(String [] a) throws InterruptedException {
            //闭锁工具类,设需要等待事件数完成的数量为 5
            final CountDownLatch countDownLatch = new CountDownLatch(5);
            DownLatchDemo downLatchDemo = new DownLatchDemo(countDownLatch);
    
            //开始时间
            long start = System.currentTimeMillis();
    
            for (int i = 0; i < 5; i++) {
                new Thread(downLatchDemo).start();
            }
            //等待上面的5个线程执行完成,也就是事件数为 0 后才执行await之后main线程的代码
            countDownLatch.await();
    
            //结束时间
            long end = System.currentTimeMillis();
            System.out.println("执行时间为:"+(end - start));
    
    
        }
    }
    class DownLatchDemo implements Runnable {
        private CountDownLatch latch;
        DownLatchDemo(CountDownLatch latch){
            this.latch = latch;
        }
        public void run() {
            synchronized (this){
                try{
                    //执行一个耗时的操作
                    for (int i = 0; i < 5; i++) {
                        System.out.println(Thread.currentThread().getName());
                    }
                }finally {
                    //每一次线程的调用,使闭锁操作事件数减 1
                    latch.countDown();
                }
            }
        }
    }
    

    五、创建实现线程的方式

    创建实现线程的方式有四种:
    1、继承Thread接口
    2、实现Runable接口
    3、实现Callable接口
    4、线程池

    对于Callable接口:

    1、需要实现Callable接口,这里可以实现泛型接口Callable<?>
    2、重写call方法,call方法可以返回泛型接口里面的数据类型数据
    3、然后在启动这个线程的时候需要FutureTask类的支持,当然这个类就是获取线程返回值的类

    demo:

    /**
     * @Author : WJ
     * @Date : 2018/11/18/018 12:26
     * <p>
     * 注释:
     */
    public class Test2 {
    
        public static void main(String [] a) throws InterruptedException, ExecutionException {
            CallableDemo callableDemo = new CallableDemo();
    
            //要启动实现Callable 接口的线程类 需要 FutureTask 类的支持,用于接收运算结果
            FutureTask futureTask1 = new FutureTask(callableDemo);
    
            //启动线程
            new Thread(futureTask1).start();
    
            //获取线程返回值
            System.out.println(Thread.currentThread().getName()+"得到返回值:"+futureTask1.get());
        }
    }
    //实现Callable泛型接口,也可不实现泛型,call返回的将是一个Object类型的数据
    class CallableDemo implements Callable<Integer> {
    
        private volatile int number;
    
        //重写call方法
        public synchronized Integer call() throws Exception {
            number = number +1;
            System.out.println(Thread.currentThread().getName()+"为:"+number);
            return number;
        }
    }
    

    六、java中实现同步的两种方式: syschronized 和 lock

    syschronized 实现同步的方式分为:同步方法 和 同步代码块
    lock 是一个接口 ,通过:

    private Lock lock = new ReentrantLock();
    
    //然后lock调用:  
    lock.lock();
    //....同步代码 
    lock.unlock();  
    //获得锁和释放锁
    

    还可以:

    private Condition condition = lock.newCondition();
    

    调用:

    condition.await();
    condition.signal(); 
    condition.signalAll();
    

    等待和唤醒单个或所有线程,与wait 和notify、notifyAll不同的是:可以实现多路分用,也就是说将多个线程拆分等待,可以唤醒某一个确定同步线程。

    但是syschronized 可以自动的获取和释放锁,而lock则需要显示的获取和释放,释放锁lock.unlock(); 必须放在try ... finally 的finally里面执行。

    当线程竞争较激烈的话,Lock 性能优于 syschronized 。两者取决于业务的需求。

    七、读写锁ReadWriteLock

    保证 :读读、读写不是互斥的,写写是互斥(事件不能同时发生)的。

    /**
     * @Author : WJ
     * @Date : 2018/11/18/018 12:26
     * <p>
     * 注释:
     */
    public class Test2 {
    
        public static void main(String [] a) throws InterruptedException, ExecutionException {
            final DemoClass demoClass = new DemoClass();
            for (int i = 0; i < 5; i++) {
                new Thread(new Runnable() {
                    public void run() {
                        demoClass.get();
                    }
                }).start();
            }
    
            new Thread(new Runnable() {
                public void run() {
                    double number = Math.random()*100;
                    demoClass.set((int)number);
                }
            }).start();
        }
    }
    
    /**
     * 读写锁实例
     */
    class DemoClass  {
    
        private int number;
        //创建读写锁实例
        private ReadWriteLock lock = new ReentrantReadWriteLock();
    
        //读
        public void get() {
            lock.readLock().lock();
            try{
                System.out.println("读--操作:number = "+ number);
            }finally {
                lock.readLock().unlock();
            }
        }
        //写
        public void set(int number){
            lock.writeLock().lock();
            try{
                this.number = number;
                System.out.println("写++操作:number = "+ number);
            }finally {
                lock.writeLock().unlock();
            }
        }
    }
    

    八、线程池

    1、为什么要用线程池?
    当我们想要多次启动同一线程时,每一次启动都有创建和销毁操作,这样对于高并发的情况是不利的,所以就有了线程池的概念。

    2、什么是线程?
    线程池底层是实现一个对列,这个对列里面存放着多个线程,这样就不要每次创建都要销毁,影响效率。

    3、线程池核心接口:Executor (位于Java.util.concurrent包下)

    4、线程池体系结构
    java.util.concurrent.Excutor :负责线程的使用与调度的接口
    |------ExecutorService 子接口:线程池主要接口
    |-------------ThreadPoolExecutor :线程池的实现类
    |-------------ScheduledExecutorService:子接口,负责线程的调度
    |--------------------ScheduledThreadPoolExecutor:继承ThreadPoolExecutor ,实现 ScheduledExecutorService

    5、工具类:Executors
    |----ExecutorService newFixedThreadPool(); 创建固定大小的线程池
    |----ExecutorService newCachedThreadPool(); 缓存线程池,线程池数量不确定,可以根据需要自动的更改数量
    |----ExecutorService newSingleThreadPoolExecutor(); 创建固定大小的线程,可以延迟或定时的执行任务。

    6、线程池的使用:

            //实现Runable的线程类
            final DemoClass demoClass = new DemoClass();
            //创建固定大小为5的线程池
            final ExecutorService pool = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 10; i++) {
                //为线程池中的线程分配任务
                pool.submit(new Thread(demoClass));
            }
            //关闭线程池
            pool.shutdown();
    

    九、线程调度(这里摘自百度百科)

    计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权。

    有两种调度模型:分时调度模型和抢占式调度模型。

    分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。

    java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。

    一个线程会因为以下原因而放弃CPU。

    1 java虚拟机让当前线程暂时放弃CPU,转到就绪状态,使其它线程获得运行机会。
    2 当前线程因为某些原因而进入阻塞状态
    3 线程结束运行

    相关文章

      网友评论

          本文标题:“多线程”重点概念整理

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