多线程(并发编程)

简述线程,程序,进程的基本概念

程序:程序由数据和指令组成的文件,被存储在磁盘或其他数据存储设备中,也就是说程序是静态的代码。当一个程序需要运行,就需要将数据加载到内存,将指令加载到cpu。而指令运行的过程还需要磁盘,网络等设备。进程就是用来加载指令,管理内存,管理io的。(每个进程占有某些系统资源,如cpu时间,内存空间,文件,输入输出设备的控制权等。)

进程:系统运行程序的基本单位,当一个程序运行时,磁盘加载这个程序的代码到内存就是开启了一个进程,进程是程序的一次执行过程。

线程:(线程是比进程更小的执行单位)一个进程内可以分为多个线程,一个线程就是一个指令流,将指令流中一条条指令交给cpu去执行。

Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows中进程是不活动的,只是作为线程的容器

线程与进程的区别?
  • 进程是正在运行程序的实例,一个进程包含多个线程,每个线程执行不同的任务。

  • 不同的进程使用不同的内存空间,而当前进程下多个线程共享同一内存空间,和一组系统资源。所以系统产生一个线程,和在多个线程之间进行上下文切换时,负担比进程小。也正因如此线程也被称为轻量级进程。(上下文切换就是从一个线程切换到另一个线程)

线程有哪些基本状态
  • 新建(new),
  • 可运行runnable,(有资格但没有执行权,有资格有执行权)
  • 阻塞blocked,
  • 等待waiting,
  • 超时等待timed_waition,
  • 终止terminable
线程状态之间的切换:

当线程创建后,处于new(新建)状态,当执行start()方法后,处于可运行状态,当获得cpu控制权后处于运行转态,当线程执行wait()方法后,进入waiting(等待)状态。进入等待状态的线程会放弃cpu的控制权,且需要其他线程去唤醒(notify)方法。才能回到可运行状态。

超时等待状态是在等待状态的基础上加了时间限制,例如sleep(long millis)和wait(long millis)就会是java线程进入到超时等待状态。

当超时时间到达后java线程进入runnable可运行状态。当线程调用同步方法时,没有获得锁的情况下,线程会进入blocked(阻塞)状态。线程在执行完runnable的run()方法后会进入到terminated(终止)状态。

创建线程的四种方法:
  1. 继承Thread类
  2. 实现Runnable接口(无返回值)
  3. 实现Callable接口(有返回值)
  4. 线程池创建线程

创建线程代码案例icon-default.png?t=N7T8https://mp.csdn.net/mp_blog/creation/editor/139685135

Runnable与callable的区别:

1.runnable接口run()有返回值,callable接口call方法没有返回值,是个泛型,和Future,FutureTask配合,可以用来获取异步执行的结果。

2.callable接口的返回结果,需要调用FutureTask.get()得到,此方法会阻塞进程的继续往下执行,如果不调用不会阻塞。

3.Callable接口的call()方法允许抛出异常,而runnable的run方法,异常只能内部消化,不能继续上抛

线程run()与start()有什么区别:

start():的启动一个线程,该线程通过调用run方法,执行run方法中定义的逻辑代码,start只能调用一次,而run方法可以调用多次。

run():封装了要被线程执行的代码,可以被调用多次。

java中wait与sleep的区别

        首先sleep,wait执行的结果都是让当前线程放弃cpu执行权,进入等待状态。

        sleep是thread的静态方法,

        wait是object的成员方法,每个类都有

  • 醒来时机不同:

        (有限时等待)wait(long)和sleep(long)的线程都会在等待相应毫秒后醒来。

        wait()在没有其他线程使用notify或notifyall方法唤醒时,就会一直等待下去,

  • 锁特性不同:(重点)

        wait方法的执行必须获得wait对象的锁,而sleep则无此限制。

        wait方法执行后会释放掉对象锁,的其他线程可以获取到该对象锁

        而sleep()在synchronized代码块中执行后并不会释放对象锁,其他线程无法获取。

Synchronized关键字

Synchronized【对象锁】:采用互斥的方式让同一时刻最多有一个线程持有对象锁,其他线程获取这个对象锁会阻塞。

Monitor

底层是一个monitor监视器:有jvm提供,c++语言实现。

使用synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁。

monitor主要是与这个对象产生关联:

Monitor内部的存储结构:

1.Owner:存储当前获得锁的线程,只能有一个线程获取

2.EntryList:关联没有抢到锁定线程,处于阻塞Blocked状态的线程

3.WaitSet:关联调用了wait方法的线程,处于等待Waiting状态的线程。

具体的流程:

当一个线程的代码要进入cynchorized代码块,先让lock(对象锁)去关联monitor,让后判断owner是否有线程持有。

没有,则当前线程持有,表示线程获取锁成功,有,则进入entrylist队列阻塞,如果owner持有的线程释放了锁,在entrylist中线程去竞争锁的持有权(非公平)

如果代码块中调用了wait()方法,则会进入waitset中进行等地。

JMM(java内存模型)

java内存模型:是java虚拟机规范中所定义的一种内存模型。

描述了java程序中各种变量(线程共享变量)的访问规则,以及在jvm中将变量存储到内存和从内存中读取变量的底层细节。

特点:

1.所有共享变量都存储在主内存中,(变量:指实例变量,和类变量,不包含局部变量,局部变量时线程私有的,不存在竞争问题)

2.每个线程还存在自己的工作内存,工作内存保留被线程使用的变量的工作副本。

3.线程对变量的所有读写操作都是在工作内存中完成的,但不能直接读写主内存中的变量(CAS机制),不同线程也不可访问对方内存中的变量,线程间的值传递通过主内存完成的。

CAS:比较再交换)

CAS:Compare And Swap(比较再交换)。乐观锁的思想,无锁情况下保证线程间共享数据的原子性。

在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作:

AbstractQueuedSynchronizer(AQS框架)

AtomicXXX类

例:

线程1与线程2都从主存中获取变量int a=100,同时放入工作内存中,当线程1修改变量a为99(更新的值)时,会用工作副本中记录的{旧的预期值}与主存当前的值(当前内存值)是否相等,相等,则修改成功,主存值修改为更新的值。 但是当想要修改时,线程2已经先将变量修改了,此时工作副本中的值与主存中的变量值不一样,他就会修改失败,进入自旋再次尝试,直到成功为止。

底层实现:

CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,

都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现

在java中比较常见使用有很多,比如ReentrantLock和Atomic开头的线程安全类,都调用了Unsafe中的方法

  • ReentrantLock中的一段CAS代码

volatile

被volatile修饰后的共享变量(类的成员变量,类的静态成员变量),就具备两层语义:

1.保持线程间的可见性。

2.禁止进行指令重排序。

保持线程间的可见性:被volatile修饰的共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改立对另一个线程可见,强制将修改的值立即写入主存。

例:为使用volatile修饰的共享变量,在线程1,和线程2,执行后线程3while循环陷入死循环,对于线程1修改的stop=true不可见,使用volatile修饰后,线程3可见。

    static volatile boolean stop=false;
    public static void main(String[] args) {
        new Thread(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            stop=true;
            System.out.println(Thread.currentThread().getName()+": modify stop to true");
        },"t1").start();
        
        new Thread(()->{
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName()+":"+stop);
        },"t2").start();
        
        new Thread(()->{
            int i=0;
            while (!stop){
                i++;
            }
            System.out.println("stopped "+i);
        },"t3").start();
    }

执行结果对比:

原因: jvm虚拟机有一个jit编译器对代码做了优化,将while(!stop) 优化为了 while(true)

解决方案:

1.在程序运行时加入vm参数 -Xint表示禁止即使编译器,不推荐

2.在stop变量的时候加上volatile,告诉jit,不对volatile修饰的变量做优化

禁止指令重排序:volatile修饰共享变量后再读写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而阻止重排序的效果

导致并发程序出现问题的根本原因

Java并发编程三大特性:原子性,可见性,有序性

原子性:一个线程在cpu中的操作不可暂停,也不可中断,要么不执行,要么执行完成。

例:票的超卖,或者一张票卖给同一个人。

保持原子性:加锁

1.synchronized:同步加锁

2.juc里面的lock:加锁

内存可见性:让一个线程对共享变量的修改另一个线程可见。 解决:volatile(推荐)

有序性:指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中每个语句执行的先后顺序与代码中的顺序一致,但是他会保证程序最终执行结果与代码顺序执行的结果一致;解决volatile

AQS:抽象队列同步器

AQS: AbstractQueusedSynchronizer,即抽象队列同步器,它是构建锁或者其他同步的基础框架。

与Synchronized的区别?

synchronizedAQS
关键字,c++实现java 语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能锁竞争激烈的情况下,提供了多种解决方

AQS:常见的实现类:

  • Reentrantlock 阻塞式锁

  • Semaphore 信号量

  • CountDownLatch 倒计时锁

工作机制:

  • state: 在AQS中维护了一个用volatile修饰的state变量来表示资源的状态,0表示无锁,1,表示有锁

  • fifo双端队列: 还提供一个fifo的等待队列,类似于monitor的entrylist,

  • 条件变量实现等待,唤醒机制,支持多个条件变量,类似于Monitor的Waitset。

线程获取锁流程:

  • 线程1来了后尝试修改state,当state=0,将state修改为1,线程1获得锁,当state=1时,说明其他线程持有锁,线程1获取锁失败,会进入fifo队列中等待。

多个线程共同获取锁资源,如何保证原子性的呢?

  • 在去修改state时,cas自旋锁保证原子性,确保只有一个线程修改成功,修改失败的回到fifo队列中等待。

AQS是公平锁吗,还是非公平锁?

  • 新的线程与队列中的线程共同来抢资源是非公平锁,

  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

AQS典型的实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源。

ReentrantLock

Reentranlock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,

构造方法支持一个可选的公平参数(默认非公平锁),当设置为true是,表示公平锁。公平锁的效率往往比非公平锁低。

带参构造方法中NonfairSync()与FairSync这两个类父类都是Sync,而Sync的父类是AQS,所以ReentrantLock底层主要实现就是基于AQS来实现的。

工作流程

  • 当线程抢锁后使用cas的方式去尝试修改state状态,修改状态成功,则让exclusiveOwnerThread属性指向当前线程,获取锁成功。

  • 加入获取失败,则进入双向队列中等待,head执行双向队列头部,tail指向尾部。

  • 当exclusiveOwnerThread为null时,会唤醒队列中等待的线程。

  • 公平锁体现在按先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

synchronized和lock有什么区别

语法层面:

  • synchronized是关键字,源码在jvm中,由c++语言实现。

  • Lock是接口,由jdk提供,用java语言实现。

  • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,

  • 需要手动调用 unlock 方法释放锁

功能层面:

  • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能

  • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量

  • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock

性能层面

  • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖

  • 在竞争激烈时,Lock 的实现通常会提供更好的性能

死锁

死锁:一个线程同时需要获取多把锁,这时就容易发生死锁。

例如:线程1获取了a对象锁,还需要获取b对象锁

线程2获取了b对象锁,还需获取a对象锁,这是就会发生死锁。

t1持有a等待b,t2持有b等待a。

如何判断死锁?

1.jdk自带工具jps和jstack


2.其他解决工具,可视化工具

  • jconsole

    用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具

    打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行

  • VisualVM:故障处理工具

    能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈

    打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

线程池

线程池icon-default.png?t=N7T8https://blog.csdn.net/qq_61513126/article/details/139691953?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22139691953%22%2C%22source%22%3A%22qq_61513126%22%7D

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值