进程和线程

进程是操作系统分配资源的最小单位,是一个独立的程序实例,拥有独立的地址空间和系统资源

线程是进程内的一个执行单元,是CPU调度的基本单位,不单独拥有系统资源,而是共享进程的资源

进程拥有独立的地址空间、堆、全局变量等,进程不共享资源。因此,创建和销毁进程的开销大,切换进程因为需要保留和恢复进程的全部状态,开销大

线程共享进程的资源,只拥有独立的私有栈和程序计数器。创建和销毁进程只需要对私有栈和程序计数器进行控制,切换线程也只需要对私有栈和程序计数器进行控制

进程通信由于地址空间独立,需要专门机制(如管道、消息队列、信号量、socket网络通信等等)

线程通信因共享进程资源,可以通过共享内存等,但需要同步机制(锁、信号量、屏障等等)

1.Java线程的6个状态

NEW:创建,未调用start()方法,

  1. 反复调用同一个线程的 start 方法是否可行?
    不行,在调用 start 之后,threadStatus 的值会改变(threadStatus !=0),再次调用 start 方法会抛出 IllegalThreadStateException 异常。
  2. 假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start 方法是否可行?
    threadStatus 为 2 代表当前线程状态为 TERMINATED

Runnable:包括了操作系统的就绪态和运行态

BLOCKED:等待锁的释放进入同步区

Waiting:等待唤醒进入Runnable

TIMED_WATING:超时等待,等待具体的时间,到时间被唤醒

TERMINATED:终止状态

2.死锁

互斥:该资源任意一个时刻只由一个线程占用

占有并等待:一个线程因请求资源而阻塞时,对已获得的资源保持不放

不可剥夺:线程已获得的资源在未使用完之前不能被其他线程强行剥夺

循环等待:若干线程之间形成一种头尾相接的循环等待资源关系

3.volatile

保证可见性,禁止指令重排序

当写一个 volatile 变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去

执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见

4.Synchronized

被它修饰的方法或者代码块在任意时刻只能有一个线程执行

原理:在JVM层面synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权

问题

1.不论读还是写,都只允许一个线程操作

2.无法知道线程是否获取到锁

3.如果出现阻塞,当前线程没有释放锁,所有线程都会等待

锁优化

jdk1.6之前,所有的锁都是”重量级“锁,涉及到了线程上下文切换和用户态与内核态的切换,因此效率较低

JDK 1.6 引入了“偏向锁”和“轻量级锁” 的概念,对 synchronized 做了一次重大的升级

无锁->偏向锁->轻量级锁->重量锁

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生;锁降级发生的条件就比较苛刻了,锁降级发生在 STW,当 JVM 进入安全点的时候,会检查是否有闲置的锁,然后进行降级。

偏向锁在资源无竞争情况下消除了同步语句,一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID

轻量级锁是通过 CAS 操作和自旋来实现的,如果自旋失败,则会升级为重量级锁。

重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU

5.CAS

硬件级别的原子操作,新值,预期值,旧值

将预期值与旧值比较,如果相同就把新值覆盖旧值;否则一直等待

Unsafe 对 CAS 的实现是通过 C++ 实现的,还有一些atomic原子类,之后说

三大问题:

ABA:

一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。追加上版本号或者时间戳

长时间自旋:

如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。让 JVM 支持处理器提供的pause 指令

多个共享变量的原子操作:

一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性。

使用AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作

加锁

6.AQS

简单说,AQS是一个抽象类,为同步器定义了通用的执行框架,定义了资源的获取和释放的通用流程,具体的资源获取释放逻辑通过具体同步器通过重写模板方法来实现。

由Volatile的state变量和先进先出的双向队列组成

获取资源:CAS尝试修改state,失败就进入队列,可能会自旋

释放资源:通过CAS修改state,无需自旋,通过unpark()唤醒后续线程

7.锁和JUC

由于synchronized的问题,我们可以用JUC包下的锁弥补

分类:

1.悲观锁、乐观锁

2.自旋锁、适应性自旋锁(不阻塞),阻塞

3.无锁,偏向锁、轻量级锁、重量级锁

4.公平锁、非公平锁 (是否先来后到)

5.可重入锁(ReentrantLock,Synchronized)、非可重入锁

6.共享锁、排他锁(synchronized,reentrantlock)

JUC的locks包下的3个接口:Condition,Lock,ReadWriteLock

锁王:StampedLock,有读写锁的功能,读锁分为乐观读锁和悲观读锁,在读的时候如果发生了写,应该通过重试来获取新的值,而不是阻塞,读多写少非常适合,避免写饥饿

写锁在释放的时候,不是减WBIT,而是加WBIT,解决ABA问题

8.ReentrantLock

支持重入,能够对共享资源重复加锁,当前线程获取该锁之后再次获取不会阻塞

支持公平锁和非公平锁,公平锁意思是锁的获取顺序按照请求上的绝对时间顺序,FIFO

公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁

内部类内部类Sync继承AQS

ReentrantLock 可以实现多路选择通知(可以绑定多个 Condition),而 synchronized 只能通过 wait 和 notify/notifyAll 方法唤醒一个线程或者唤醒全部线程(单路通知)

9.线程池

核心线程数、最大线程数、空闲线程存活时间、时间单位、工作队列、线程工厂、拒绝策略

1.当前线程数<核心线程数,通过ThreadFactory创建一个线程执行任务,执行完去阻塞队列获取任务

2.线程数>核心线程数,将任务放入阻塞队列

3.阻塞队列满了,当前线程数<最大线程数,创建非核心线程执行提交的任务

4.当前线程数达到最大线程数,执行拒绝策略:AbortPolicy(丢弃任务,抛出异常),CallerRunsPolicy(由提交任务的线程执行任务,不是线程池内部的线程,DiscardPolicy(丢弃任务,不抛出异常),DiscardPolicy(剔除最先进入队列的任务,再次提交任务)

复用:线程池的线程是被封装成了work对象,work对象继承了AQS,调用 runWorker 方法来让线程执行任务,runWorker会一直while循环,不断getTask()执行任务

shutdown:执行队列中的任务

shutdownnow:尝试停止所有正在执行的任务,停止处理等待的任务,并返回等待处理的任务列表

        ThreadPoolExecutor executor=new ThreadPoolExecutor(
                10,
                10,
                200,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<>(1000)
        );

        Runnable task=()->{
            //System.out.println("wasd");
        };

        AtomicInteger atomicInteger=new AtomicInteger(0);
        for(int i=0;i<1000;i++){
            executor.execute(task);
            Thread.sleep(1);
            atomicInteger.incrementAndGet();
        }
        System.out.println("次数"+atomicInteger.get());
        List<Runnable> runnables = executor.shutdownNow();
        System.out.println(runnables.size());

不推荐使用 Executors 来创建线程池,newFixedThreadPool 线程池由于使用了 LinkedBlockingQueue,队列的容量默认无限大,实际使用中出现任务过多时会导致内存溢出;newCachedThreadPool 线程池由于核心线程数无限大,当任务过多的时候会导致创建大量的线程,可能机器负载过高导致服务宕机。

10.并发工具类

AtomicInteger提供线程安全的原子操作,无需加锁,核心基于CAS实现的,适用于需要频繁对数值进行无锁操作

AtomicInteger atomicInt = new AtomicInteger(0);
        atomicInt.incrementAndGet();
        atomicInt.decrementAndGet();
        atomicInt.compareAndSet(1,2);

Semaphore控制访问资源的线程数量

Semaphore semaphore=new Semaphore(50);
        try{
            semaphore.acquire();
        }catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
        }

CountDownLatch适用于主线程需要等待多个子线程完成任务的场景

AtomicInteger a=new AtomicInteger(3);
        CountDownLatch countDownLatch=new CountDownLatch(3);
        Runnable Task=()->{
            try{
                a.decrementAndGet();
            }finally {
                countDownLatch.countDown();
            }
        };

        new Thread(Task).start();
        new Thread(Task).start();
        new Thread(Task).start();
        countDownLatch.await();
        System.out.println(a.get());

11.创建线程的几种方法

        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {

            }
        });
        thread1.start();

        FutureTask<Integer> futureTask=new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                return null;
            }
        });
        Thread thread2=new Thread(futureTask);
        thread2.start();

        //3.继承Thread类,重写run方法,创建对象调用start方法
        //4.使用线程池
        //5.CompletableFuture

12.sleep&wait

sleep释放CPU,不释放锁,超时自动恢复

wait释放锁,需要notify唤醒

13. 多线程之间怎么通信?

共享变量(volatile/synchornized/Lock)

等待通知(wait/notify/notifyall)

并发工具类(CyclicBarrier/CountDownLatch/Semaphore)

阻塞队列(BlockingQueue)

原子类(AtomicInteager)

14. 多进程直接怎么通信?

分为同一台主机和不同主机

不同主机:socket通信,方式有3种 TCP/UDP/同一进程内部通信
同一主机:

1.管道:匿名/命名管道 本质是内核缓冲区

2.消息队列:实际上是保存在内核的消息链表

3.共享内存:虽然不同进程无法直接共享内存,通过多进程访问同一共享地址空间,再通过信号量保证共享资源互斥访问

4.信号:异步通信机制,用于应用进程和内核直接交互

问题:

为什么多进程不能共享内存?

操作系统通过虚拟内存和页表机制隔离进程地址空间,需要通过内核显示映射内存才能共享

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐