进程、线程
-
进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。可以理解为一个java应用。
-
线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
一个进程中有多个线程,多个线程共用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈。但会导致内存泄漏、上下⽂切换、死锁。
多线程和锁的关系
只有拿到锁的线程才能访问共享资源,多线程之间的通信和协作,通常使用锁和等待/通知机制来实现。
线程死锁
线程 A 持有资源 2 ,线程 B 持有资源 1 ,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
并发、并行
简单说,轮流做是并发,一起做是并行。
线程创建方式
-
继承Thread类,重写run()方法,调用start()方法启动线程
一个例子
这段代码输出结果可能是ab或者ba。
-
实现 Runnable 接口,重写run()方法
上面两种都是没有返回值的。
-
实现Callable接口,重写call()方法,这种方式可以通过FutureTask获取任务执行的返回值
JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。直接执行run方法就相当于执行一个普通的方法。直接执行thread中的run方法也是相当于顺序执行run方法。
线程等待、休眠与通知
等待
休眠
等待和休眠区别
通知
实例
执行顺序
停止线程
- 使用退出标志,使线程正常退出。
volatile boolean flag = false ;t1.flag = true ; - stop强行终止t1.stop();
- interrupt
Thread t2 = new Thread(()->{ while(true) { Thread current = Thread.currentThread(); boolean interrupted = current.isInterrupted(); if(interrupted) { System.out.println("打断状态:"+interrupted); break; } } }, "t2"); t2.start(); Thread.sleep(500);
线程上下文切换
使用多线程的目的是为了充分利用CPU,但是我们知道,并发其实是一个CPU来应付多个线程。
为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。
守护线程
Java中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。
在JVM 启动时会调用 main 函数,main函数所在的线程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。
那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程束时, JVM会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM退出。换而言之,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
线程间有哪些通信方式(操作系统)
volatile
的作用主要
- 保证可见性: 当一个变量被声明为
volatile
后,对该变量的写操作会立即被其他线程所看到,保证了多个线程之间对该变量的可见性。换句话说,一个线程对volatile
变量的修改对其他线程是可见的,不会出现线程间的数据不一致问题。例:
static volatile boolean stop = false;
- 禁止指令重排序:
volatile
变量的读写操作会插入内存屏障,防止编译器和处理器对其进行指令重排序优化,保证了代码执行的顺序性。这样可以确保对volatile
变量的写操作先于后续的读操作,避免出现因指令重排序导致的意外结果。
写变量让volatile修饰的变量的在代码最后位置
读变量让volatile修饰的变量的在代码最开始位置
总的来说,volatile
主要用于在多线程环境中确保变量的可见性和一致性,它适用于一种场景:变量被多个线程共享,并且这些线程可能会同时读写这个变量。通过使用 volatile
关键字,可以有效地避免由于线程间数据不一致导致的并发问题。
并发锁
synchronized【对象锁】关键字的使用
-
synchronized (this)
:这种方式是在非静态方法中使用的,它将当前对象(即调用该方法的对象)作为同步锁。只有在同一对象实例上获取锁的线程才能进入同步代码块,其他线程需要等待当前对象锁释放后才能继续执行。因此,同一对象的不同方法之间也是同步的。 -
synchronized (lock)
:这种方式是在静态方法或者普通代码块中使用的,它将指定的对象作为同步锁。通常情况下,会使用一个静态对象作为锁。多个线程在获取到相同的锁对象时才能进入同步代码块,其他线程需要等待锁释放后才能执行。因此,不同对象实例上的同步代码块之间是独立的,不会相互影响。
- synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是给 Class
- synchronized 关键字加到实例⽅法上是给对象实例上锁。
- 尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!
对象单例模式
1.懒汉模式
2.饿汉模式
synchronized底层实现原理
- synchronized 关键字底层原理属于 JVM 层⾯。
- 当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
- 在monitor内部有三个属性,分别是owner、entrylist、waitset。其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
- 在执⾏ monitorenter 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
- 在执⾏ monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。
synchronized和Lock区别
锁升级
Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
对象的内存结构
对象在内存中存储的布局:
MarkWord:
-
Monitor重量级锁:每个对象的对象头都可以设置monoitor的指针,让对象与monitor产生关联。
-
轻量级锁:
-
偏向锁
锁升级
CPU ⾼速缓存
CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。 CPU Cache 缓存的是内存数据⽤于解决 CPU 处理速度和内存不匹配的问题,内存缓存的 是硬盘数据⽤于解决硬盘访问速度过慢的问题。
JMM(Java 内存模型)(JVM)
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
CAS
CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
旧的预期值就是希望从哪个值开始变的,需要跟内存值做对比,如果一样的话就更新内存值为B的数值。
一般思路是通过自旋锁实现。
并发程序出现问题的根本原因
Java并发编程三大特性
原子性:加锁
synchronized或LOCK
可见性:让一个线程对共享变量的修改对另一个线程可见
synchronized、volatile(推荐)、LOCK
有序性
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保
证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终
执行结果和代码顺序执行的结果是一致的。volatile
AQS
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架
AQS常见的实现类:
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待.
AQS是公平锁吗,还是非公平锁?
- 新的线程与队列中的线程共同来抢资源,是非公平锁
- 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁
- 比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源
ReentrantLock
翻译过来是可重入锁,CAS+AQS队列实现。
可中断;可以设置超时时间;可以设置公平锁;支持多个条件变量;与synchronized一样,都支持重入.