多线程与高并发(一)-----基础知识

线程的历史-----是一部对于cpu性能压榨的历史

单进程人工切换
        – 纸带机
多进程批处理
        – 多个任务批量执行
多进程并行处理
        – 把程序写在不同的内存位置上来回切换
多线程
        – 一个程序内部不同任务的来回切换
        – selector - epoll
纤程/协程
        – 绿色线程,用户管理的(而不是OS管理的)线程


什么是进程?什么是线程?

什么是进程:资源分配的基本单位(静态概念)

什么是线程:资源调度的基本单位(动态概念)

通俗说:一个程序中不同的执行路径


常见问题 

程序是什么?--> QQ.exe PowerPoint.exe  可执行文件

进程是什么?--> 程序启动 进入内存 资源分配的基本单位

线程是什么?--> 程序执行的基本单位

程序如何开始运行?--> CPU 读指令 - PC(存储指令地址) ,读数据 Register ,计算, 回写, -> 下一条

线程如何进行调度?--> linux 线程调度器(OS)操作系统

线程切换的概念是什么?--> Context Switch CPU保存现场 执行新线程,恢复现场,继续执行原线程这样的一个过程


单线程与多线程

                                  


 计算机的组成


 线程切换

        两个线程不同时间段使用cpu资源,T2线程使用cpu时,T1线程先把数据记录到缓冲,然后让出cpu使用权,等到下一次获得cpu使用权时,再从缓存中拿出数据,继续执行。

        线程之间的切换通过操作系统调度


 常见线程问题

1.单核CPU设定多线程是否有意义?

有意义,如果A线程阻塞(如等待IO),则可以让出cpu资源给B线程,即使是单核,多线程也能提高cpu的利用率

2.工作线程是不是设置的越大越好?

并不是,如果线程设置的非常大,则每个线程获得的cpu执行时间片段很少,从而导致cpu不断的在进行线程间的上下文切换,而上下文切换也是需要消耗cpu资源的,反而导致cpu利用率不高

3.工作线程数(线程池中线程数量)设置多少合适

一般使用下面这个公式计算


 创建线程的5种方法

方法1---继承Thread
方法2---实现Runnable
方法3---Lambda
方法4---线程池
方法5---Future Callable FutureTask

static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello MyThread!");
        }
    }

    static class MyRun implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello MyRun!");
        }
    }

    static class MyCall implements Callable<String>{

        @Override
        public String call() throws Exception {
            System.out.println("Hello MyCall");
            return "success";
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //方法1---继承Thread
        new MyThread().start();
        //方法2---实现Runnable
        new Thread(new MyRun()).start();
        //方法3---Lambda
        new Thread(()->{
            System.out.println("Hello Lambda!");
        }).start();

        //方法4---线程池
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(() ->{
            System.out.println("hello ThreadPool");
        });

        Future<String> f = service.submit(new MyCall());
        String s=f.get();
        System.out.println(s);
        service.shutdown();

        //方法5---Future Callable FutureTask
        FutureTask<String> task = new FutureTask<>(new MyCall());
        Thread t = new Thread(task);
        t.start();
        System.out.println(task.get());

    }

 线程的状态

1. NEW : 线程刚刚创建,还没有启动
2. RUNNABLE :可运行状态,由线程调度器可以安排执行
    包括READY和RUNNING两种细分状态
3. WAITING:  等待被唤醒
4. TIMED WAITING: 隔一段时间后自动唤醒
5. BLOCKED:   被阻塞,正在等待锁
6. TERMINATED: 线程结束


线程的打断(interrupt) 

1. interrupt() :实例方法,设置线程中断标志(打扰一下,你该处理一下中断)
2. isInterrupted():实例方法,有没有人打扰我?
3. interrupted():静态方法,有没有人打扰我(当前线程)?复位!

interrupt和sleep() wait() join()

sleep()方法在睡眠的时候,不到时间是没有办法叫醒的,这个时候可以用interrupt设置标志位,然后呢必须得catch InterruptedException来进行处理,决定继续睡或者是别的逻辑,(自动进行中断标志复位)

interrupt是否能中断正在竞争锁的线程?

1.interrupt()不能打断正在竞争锁的线程synchronized(),ReentrantLock的lock()
2.如果想打断正在竞争锁的线程,使用ReentrantLock的lockInterruptibly()


如何优雅的控制线程的结束

 1. 自然结束(能自然结束就尽量自然结束)
2. stop() (太粗暴了,直接干掉,直接释放锁导致数据不一致)suspend() resume() (这两哥们不会释放锁,会产生死锁也被废掉了)
3. volatile标志
   1). 不适合某些场景(比如还没有同步的时候,线程做了阻塞操作,没有办法循环回去)
   2). 打断时间也不是特别精确,比如一个阻塞容器,容量为5的时候结束生产者,
      但是,由于volatile同步线程标志位的时间控制不是很精确,有可能生产者还继续生产一段儿时间
4. interrupt() and isInterrupted(比较优雅)
5. 如果要做到精确控制,多长时间和while循环多少次,就需要业务线程+锁的形式来控制


并发编程的三大特性

可见性(visibility)
有序性(ordering)
原子性(atomicity)


 可见性

多线程提高效率,本地缓存数据,造成数据修改不可见,

要想保证可见,要么触发同步指令,要么加上volatile,被修饰的内存,只要有修改,马上同步涉及到的每个线程

volatile 应用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

三级缓存

 缓存行对齐

缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高

缓存一致性协议和volatile无关


有序性

CPU的乱序执行

为什么会乱序?主要是为了提高效

线程的as-if-serial

单个线程,两条语句,未必是按顺序执行

单线程的重排序,必须保证最终一致性

as-if-serial:看上去像是序列化(单线程)

会产生的后果

多线程会产生不希望看到的结果

 如何保证有序性?

内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行

所有实现JVM规范的虚拟机,必须实现四个屏障

volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序

JVM内存屏障

LoadLoad屏障:

        对于这样的语句Load1; LoadLoad; Load2,

        在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:

        对于这样的语句Store1; StoreStore; Store2,

        在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:

        对于这样的语句Load1; LoadStore; Store2,

        在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:

        对于这样的语句Store1; StoreLoad; Load2,

        在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。 

LOCK 用于在多处理器中执行指令时对共享内存的独占使用。 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。 另外还提供了有序的指令无法越过这个内存屏障的作用。  


原子性

基本概念:

race condition => 竞争条件 , 指的是多个线程访问共享数据的时候产生竞争

数据的不一致(unconsistency),并发访问之下产生的不期望出现的结果

如何保障数据一致呢?--> 线程同步(线程执行的顺序安排好),

monitor (管程) ---> 锁

critical section -> 临界区

如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较

保障操作的原子性 

  1. 悲观的认为这个操作会被别的线程打断(悲观锁)synchronized(上一个小程序)

  2. 乐观的认为这个做不会被别的线程打断(乐观锁 自旋锁 无锁)cas操作

上锁的本质:

上锁的本质是把并发编程序列化 ,同时保障可见性

注意序列化并非其他程序一直没机会执行,而是有可能会被调度,但是抢不到锁,又回到Blocked或者Waiting状态(sync锁升级)

一定是锁定同一把锁(抢一个坑位)

JVM中的两种锁:

 重量级锁(经过操作系统的调度)synchronized早期都是这种锁(目前的实现中升级到最后也是这种锁)

轻量级锁(CAS的实现,不经过OS调度)(无锁 - 自旋锁 - 乐观锁)

两种锁使用场景:

临界区执行时间比较长 , 等的人很多 -> 重量级

时间短,等的人少 -> 自旋锁  


CAS深度解析

 CAS的ABA问题解决方案 - Version解决(数值类型和布尔类型)

CAS操作本身的原子性保障

应用:java的atomic包

最终实现:lock cmpxchg 指令 


对象结构

 1)前两行为markword,第三行记录的是这个对象的指针,第四行补偿行(将整个对象补偿成8字节的整数倍)

2)前面一部分为普通对象

3)后一部分为加上synchronize对象

4)红框的不同,表示加锁是由markword控制的

4)markword记录了锁信息,JC信息,HashCode信息


markword-64


锁升级过程

偏向锁、轻量级锁:用户态

重量级锁:内核态 

偏向锁什么时候升级为轻量级锁?

        有人竞争时就升级

轻量级锁什么时候升级为重量级锁?

        竞争加剧:有线程超过10次自旋

为什么有自旋锁还需要重量级锁?

        自旋是消耗cpu资源,如果锁时间长,或者自旋线程多,cpu会被大量消耗

        重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗cpu资源

偏向锁是否一定比自旋锁效率高?

        不一定,在明确知道会有多个线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

        JVM启动过程,会有很多线程竞争(明确),先不开启偏向锁,过段时间再开启


 锁重入

synchronize是可重入锁

重入次数必须记录,因为要解锁几次必须得对应

偏向锁、轻量级锁-->线程栈-->LR+1

重量级锁-->objectmonitor字段上

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值