线程详解——>
一、操作系统概念——进程与线程
1、进程
①进程是具有独立功能的程序关于某个数据集合上的一次运行活动,是系统进行分配资源和调度的独立单位,
②是可与其他程序并发执行的程序,在一个数据集合上的运行过程。它是系统进行资源分配和调度的一个独立单位。
2、线程与进程关系
①线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
②一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。
③进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见。
④调度和切换:线程上下文切换比进程上下文切换要快得多。
⑤以去理发店为例:理发整体是一个任务(进程),理发店内洗剪吹等是一个个子任务(线程)。
3、线程与进程之间的对比
对比 | 进程 | 线程 |
---|---|---|
定义 | 进程是程序运行的一个实体的运行过程,是系统进行资源分配和调配的一个独立单位 | 线程是进程运行和执行的最小调度单位 |
系统开销 | 创建撤销切换开销大,资源要重新分配和收回 | 仅保存少量寄存器的内容,开销小,在进程的地址空间执行代码 |
拥有资产 | 资源拥有的基本单位 | 基本上不占资源,仅有不可少的资源(程序计数器,一组寄存器和栈) |
调度 | 资源分配的基本单位 | 独立调度分配的单位 |
安全性 | 进程间相互独立,互不影响 | 线程共享一个进程下面的资源,可以互相通信和影响 |
地址空间 | 系统赋予的独立的内存地址空间 | 由相关堆栈寄存器和线程控制表TCB组成,寄存器可被用来存储线程内的局部变量 |
二、操作系统概念——并行、并发、串行
1、串行
多个任务,执行时一个执行完再执行另一个。
2、并发
多个进程在单个核心运行,同一个时间一个线程运行(时间分片),系统不停地切换线程,看起来像同时运行。
3、并行
每个线程分配给独立的核心,线程同时运行。
ps:单CPU多核:一条高速公里上多条车道。(1n)
多CPU多核:多条高速公路(且每条路上有多条车道)。(nn)
三、线程
1、生命周期
①创建
a.Thread类继承
不能抛异常,无返回值
package com.CSDN.csdn210615;
public class ThreadDemo01 extends Thread{//第一种方法
@Override
public void run(){
for (int i = 0;i <10;i++){
System.out.println(Thread.currentThread().getName()+"线程运行:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new ThreadDemo01();
Thread t2 = new ThreadDemo01();
Thread t3 = new ThreadDemo01();
t1.start();
t2.start();
t3.start();
}
}
b.Runnable接口的实现
不能抛异常,无返回值
package com.CSDN.csdn210615;
public class ThreadDemo02 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName()+"线程运行:"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadDemo02());
Thread t2 = new Thread(new ThreadDemo02());
Thread t3 = new Thread(new ThreadDemo02());
t1.start();
t2.start();
t3.start();
}
}
c.Callable接口的实现
可抛异常,有返回值
package com.CSDN.csdn210615;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadDemo03 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int k = 0;
for (int i = 0;i < 10;i++){
k+=i;
System.out.println(Thread.currentThread().getName()+"线程运行:"+k);
Thread.sleep(100);
}
return k;
}
public static void main(String[] args) {
Thread t1 = new Thread(new FutureTask<Integer>(new ThreadDemo03()));
Thread t2 = new Thread(new FutureTask<Integer>(new ThreadDemo03()));
Thread t3 = new Thread(new FutureTask<Integer>(new ThreadDemo03()));
t1.start();
t2.start();
t3.start();
}
}
②就绪
[.start()]——start后进入线程池等待执行。
③运行
[.run()]——该线程被调用进入运行状态。
④阻塞
a.[.sleep(n)]
强制当前正在执行的线程休眠(暂停执行),以“减慢线程” ,当线程睡眠时,它睡在某个地方,在苏醒之前不会返回到可运行状态。
b.[.yield()]
让当前线程从运行状态转为就绪状态,以允许具有相同优先级的其他线程获得运行机会。但是其实就是将当前线程重新再放进下个运行的一堆准备抢占的队列中,即:当前线程让步后,还有可能被再次选中。
c.[.join()]
例如t.join();作用就是等当前线程t运行完在运行下个线程B。
d.[.wait()]
让本线程进入等待状态直到被用notify(All())方法唤醒。
⑤自然死亡
四、CAS机制与ABA问题
1、CAS(compare and swap)
比较并进行交换。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
当不同线程修改同一个值时,很有可能会出现如下的情况:
在内存地址V中,存储着值为10的变量,此时线程1想将变量值增加1,对线程1来说,此时预期值A=10,要修改的新值为B=11,可在线程1要提交更新之前,另一个线程线程2抢先一步将内存地址V中的变量值更新为11,然后此时线程1开始提交更新,首先将A和地址V的实际值比较,发现不相等,提交失败;然后线程1再次重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12若恰好此次没有其他线程改变地址V的值,线程1进行比较,发现A与地址V的实际值相等,线程1进行交换,把地址V的值替换为B(12)。
Atomic操作(原子操作)类的底层正是用到“CAS机制”。
2、AtomicInteger——incrementAndGet()
AtomicInteger中的方法incrementAndGet(),其内部其实就是CAS的自旋
①获取当前值
②当前值加1,计算出目标值
③进行CAS操作,成功则跳出循环,失败则重复上述步骤
AtomicInteger类及其IncrementAndGet()方法代码:
package com.CSDN.csdn210615;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo04 implements Runnable{
public static AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for(int i = 0;i < 10;i++){
count.incrementAndGet();
System.out.println(Thread.currentThread().getName()+"线程运行后:" + count);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadDemo04());//里面要runnable类型
Thread t2 = new Thread(new ThreadDemo04());
Thread t3 = new Thread(new ThreadDemo04());
t1.start();
t2.start();
t3.start();
}
}
3、ABA问题
假设内存中有一个值为A的变量,存储在地址V中,此时有3个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。
①第一次
线程1和线程2已获取当前值,线程3还未获取当前值。
内存地址V:A
线程1:获取当前值A,期望更新为B
线程2:获取当前值A,期望更新为B
线程3:期望更新为A
②第二次
接下来,线程1先执行成功,将A更新为B;线程2因为某种原因阻塞,没有做更新操作;线程3在线程1更新后获取了当前值B。
内存地址V:B
线程1:获取当前值A,成功更新为B;
线程2:获取当前值A,期望更新为B*【BLOCK】*;
线程3:获取当前值B,期望更新为A。
③第三次
然后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新为A。
内存地址V:A
线程1:获取当前值A,成功更新为B,已返回;
线程2:获取当前值A,期望更新为B,BLOCK;
线程3:获取当前值B,成功更新为A。
④第四次
最后线程2终于恢复了运行状态,由于阻塞前已获得了当前值A,并且compare比较发现实际值也为A,所以成功将A更新为B。
内存地址V:B
线程1:获取当前值A,成功更新为B,已返回;
线程2:获取当前值A,成功更新为B。
线程3:获取当前值B,成功更新为A;
解决方法
给变量加上版本号,每次更新变量值时都更新版本号,使得当变量值虽然一样但版本号不一样无法再更新变量值。
五、线程的三大特性
1、原子性
一个或多个操作一旦开始执行之后其过程要么全部执行,要么全部不执行。
2、可见性
当多个线程访问同一个变量的时候,一个线程改变了这个变量的值,其他线程能立即看到值的变化。
【注】若两个线程在不同的CPU,那么线程1改变了某值还没来得及刷新到主内存,线程2又开始使用该值,那么此值肯定还是以前的,线程1对该值的操作就没有可见性了。
3、有序性
程序按照代码的先后顺序执行
可在进程中的线程里,不同线程对代码调用都是抢占式的,并没有按顺序对代码进行调用执行,所以此时可以通过【.join()】方法保证线程顺序执行和安全性。
六、死锁的四个必要条件
死锁概念及产生原理
1、概念
①当前线程拥有其他线程等待的资源。
②当前线程等待其他线程已有的资源。
③它们都不放弃已拥有的资源。
2、产生原因
①由于资源不足而导致的资源竞争:多个线程所共享的资源不足,引起它们对资源的竞争而产生死锁。
②由于并发执行的顺序不当:线程执行过程中,请求和释放资源的顺序不当而导致进程死锁。
3、产生的必要条件
a.互斥
一个资源一次只能分配给一个进程使用。
b.占有且等待
一个线程在等待其他线程释放资源的同时,继续占有已经拥有的资源。
c.不可抢占
一个线程不能强行占有其他线程已占有的资源。
d.循环等待
若干线程由于等待其他线程释放资源,而形成相互等待的循环。如线程A等待线程B,线程B等待线程C,线程C等待线程A。
七、volatile与synchronized
1、volatile
①是变量修饰符,使其修饰的变量具有可见性。
②一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。
③在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的;之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。
④volatile不能保证原子性。
在自加运算中:
变量定义 | 结果 |
---|---|
int | 小于目标值 |
volatile int | 小于目标值 |
AtomicInteger + incrementAndGet() | 等于目标值 |
2、synchronized
修饰一个方法或者一个代码块时,能够保证在同一时刻最多只有一个线程执行该段代码。
加锁方法
方法一
public synchronized void test(){
...
...
}
方法二
public void test(){
synchronized(this){
...
...
}
}
八、synchronized与ReentrantLock
Synchronized | ReentrantLock |
---|---|
①centered 文本居中 | ①使用灵活,但必须有释放锁动作 |
②对象之间是互斥锁 | ②手动释放和开启锁 |
—— | ③只适用于代码块锁 |
官方给出的ReentrantLock用法
class X{
private final ReentrantLock LOCK = new ReentrantLock();
public void m(){
lock.lock();//block until condition holds
try{
//...method body
}finally{
lock.unlock();
}
}
}