java高并发程序设计(一)基本概念

多核cpu的诞生

10年过去,cpu主频并无多大变化,还是停留在4ghz,已经不符合摩尔定律 既然无法继续提高单个cpu性能,就在一个cpu中塞很多核进去,因此多核cpu开始发展 目前多核cpu符合摩尔定律,1核-2核-4核-8核-16核

cpu,核心,线程

一个核心就是一个物理线程,核心数2就有两个物理线程,一个核心只能同时执行一个线程。但是英特尔的超线程技术可以把一个物理线程模拟出两个线程来用,充分发挥CPU性能。线程数4就是代表核心数2的两个物理线程可以模拟成四个线程来使用


对于个人用户而言,现在个人电脑的cpu基本都是多核心的(不是多cpu,是单cpu多核心),足以应付日常应用的数据计算量


查看自己电脑是几核可以右键我的电脑->管理->设备管理器中查看

同步和异步调用

同步调用:一个任务开始以后必须等待结束才可以做下一件事情
异步调用:一个任务开始,马上获得响应,这个任务跑在另一个线程上

临界区

是一种公共资源(所有线程都可以访问),共享数据,如果不控制临界区,就会出现多个线程同时进入临界区,一个线程往临界区写东西,还没写完,另一个线程又往临界区写东西这两个数据一叠加!!!就可能出现一个错误的结果。因此希望每次只有一个线程进入临界区,其他线程进入阻塞队列

上下文切换

扫描二维码关注公众号,回复: 1481810 查看本文章

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。

所以任务从保存到再加载的过程就是一次上下文切换。

这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度

如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

·无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

·CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

·使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

·协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

死锁,饥饿,活锁

  • 死锁:每个小车所占的路是资源,A堵塞D的路,B堵塞A的路,C堵塞B的路,D堵塞C的路,除非挪走一辆车
package com.thread;

public class DeadLock {

    private static final String LOCK1 = "A";
    private static final String LOCK2 = "B";

    public static void main(String[] args) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized(LOCK1) {
                    try {
                        System.out.println("线程1获取到LOCK1锁");
                        Thread.currentThread().sleep(500);  // 避免线程1直接获得锁LOCK2然后结束打印1导致死锁不产生
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized(LOCK2) {
                        System.out.println(1);
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized (LOCK2) {
                    System.out.println("线程2获取到LOCK2锁");
                    synchronized(LOCK1) {
                        System.out.println(2);
                    }
                }
            }
        }).start();
    }

}

上述代码会一直进行下去

如果线程1做轻微的修改如下

new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized(LOCK1) {
                    /*try {
                        System.out.println("线程1获取到LOCK1锁");
                        Thread.currentThread().sleep(500);  // 避免线程1直接获取B锁产生不了死锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }*/
                    System.out.println("线程1获取到LOCK1锁");
                    synchronized(LOCK2) {
                        System.out.println(1);
                    }
                }
            }
        }).start();

运行结果如下所示:

线程1获取到LOCK1锁
1
线程2获取到LOCK2锁
2
  • 饥饿:某个或者多个线程因为种种原因无法获得需要的资源导致一直无法执行
  • 活锁:电梯下来,想进去的人和想出来的人撞上,不停的把路堵上,线程不会,没有办法拿到全部资源,把自己已经获得的锁释放以确保不会产生死锁

并发级别

阻塞
一线程进入临界区,其他线程在外面等待

非阻塞
无障碍:线程可以自由进入,宽进严出
如果他发现一个线程在临界区中遇到了竞争,跟别人产生了冲突,就会回滚这条数据
读取一对坐标,x和y,先读x,读到y的时候发现另一个线程改了x,就会认为我这样读出来的y是错误的
因此他会重新读取x,再读y以保证读到正确的数据
无竞争时候,有限时间内完成操作
有竞争时候,回滚数据
无障碍是所有线程如果发生竞争,不保证临界区的线程能出来,如果发现有冲突会不停尝试,如果彼此都干扰,那么最后
所有线程都会卡死在这里

无锁:无障碍+保证至少有一个线程可以胜出 ** 使用最为广泛
这样下面9个线程再竞争,会再出去一个
这样下面8个线程竞争,再出去一个
这样保证系统正常运行
无等待:
无锁+所有线程必须有限步内完成,必然是无饥饿的,不会有线程永远待临界区
所有的读线程都是无等待的,没有写线程的话,写会干扰到读,并行的最高级别

阻塞调度策略是悲观的,认为大家一起修改这块数据,会把这块数据改坏

并行的2个定律
阿姆达尔定律:光加cpu没用,需要提供串行化比例
古斯塔夫森:并行化比例保证的情况下,加速比跟cpu个数正比
增加处理器个数不一定能导致加速比上升
即光加cpu是不行的
每个步骤消耗100个时间单位
步骤2采用2个cpu执行

并行比例:2/5
串行比例:3/5

2 = 1*()

F为穿行比例
只要有

猜你喜欢

转载自blog.csdn.net/bsfz_2018/article/details/79899776