简述线程,程序,进程的基本概念
程序:程序由数据和指令组成的文件,被存储在磁盘或其他数据存储设备中,也就是说程序是静态的代码。当一个程序需要运行,就需要将数据加载到内存,将指令加载到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(终止)状态。
创建线程的四种方法:
- 继承Thread类
- 实现Runnable接口(无返回值)
- 实现Callable接口(有返回值)
- 线程池创建线程
创建线程代码案例https://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的区别?
synchronized | AQS |
---|---|
关键字,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就行