进程和线程
一个程序至少一个进程,一个程序至少一个线程。线程不能单独执行运行的,他一定是运行在进程的内部的.
-
进程[正在执行中的应用程序]:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念。
-
竞争计算机系统资源的基本单位。-“多任务操作系统”-“多个进程在同时在运行” - CPU分配资源-“分时分片”
处理器[cpu]调度的基本单位
-
线程:是进程的一个执行单元,是进程内部调度实体。比进程更小的独立运行的基本单位。线程也被成为轻量级进程。
一个进程可以拥有多个进程的,同一个进程内部的多个线程是可以共享进程资源的
比如启动Java程序
- 启动Java程序实际上就是启动JVM[对字节码文件进行逐行翻译 - 翻译成底层的机器语言]
开启一个JVM进程 - jvm.exe[windows中的可执行文件.C语言写的程序直接编译成了.exe可执行文件] - 当JVM进程启动之后,同时开启俩个线程 - 分别是main主线程以及一个GC线程[后台守护线程]
守护线程 - 如果后台只剩下守护线程在执行的时候,那么进程就会结束.
进程的结束不需要等到守护线程全部执行完才会结束. - main线程负责执行main方法中的程序 - main方法中的程序全部执行完之后,main线程运行结束
GC线程负责进行垃圾对象的回收
创建线程的传统的方式
-
第一种方式:extends Thread方式 - 共享代码,不共享资源
只有将资源设置成静态的 - 也是进行一个资源的共享的
-
第二种方式:implements Runnable接口 - 共享代码,共享资源
Thread和Runnable区别
-
第一种方式extends Thread方式 - 共享代码,不共享资源
只有将资源设置成静态的 - 也是进行一个资源的共享的
-
第二种方式implements Runnable接口 - 共享代码,共享资源
-
第三种方式Callable接口
推荐配合Future+线程池一起使用
Callable和Runnable接口的区别
- Callable可以通过Future来得到异步计算的结果 - 拿到线程执行之后的结果.
- Callable调用的是call方法,Runnable调用的是run方法.
- call方法是可以抛出一个异常列表的,但是run方法是不允许抛出异常列表的
Callable例子
//拿到线程执行之后的结果 Callable+FutureTask -> 线程执行的结果
class C1 implements Callable<Integer>{
public static void main(String[] args) {
System.out.println("main-begin...");
Callable<Integer> c = new C1();
//FutureTask - 异步任务
FutureTask<Integer> task = new FutureTask<>(c);
//利用异步任务来构建Thread对象
Thread t = new Thread(task);
t.start();//启动线程
System.out.println("线程已经启动了...");
try {
//拿线程计算的结果
//get()方法一定是要等到线程执行结束之后 - 才会停止
System.out.println("result:"+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("main-end...");
}
@Override
public Integer call() throws Exception {
int total = 0;
for (int i = 1; i <= 100 ; i++) {
total+=i;
}
//故意模拟一个比较费时费力的任务
Thread.sleep(2000);
return total;//5050
}
}
public class CallablePoolDemo {
public static void main(String[] args) {
//创建一个可缓存的线程池对象
ExecutorService executorService = Executors.newCachedThreadPool();
//向这个缓存池中提交任务
//第一个任务 第二个任务
Future<Integer> future1 = executorService.submit(new C1());
Future<Integer> future2 = executorService.submit(new C2());
//任务的数量和线程的数量一定一样吗?
//有个效果 -> 两个线程 - 同时执行这两个异步任务
//需要将两个线程执行的结果进行一个相加的操作
try {
//get方法肯定是会等到线程执行完毕之后 - 才会继续走
Integer res1 = future1.get();
Integer res2 = future2.get();
int result = future1.get() + future2.get();
//如果两个线程的get 方法不走完,main线程也是不会继续执行的
System.out.println("开始整合....");
System.out.println(Thread.currentThread().getName() + " --- " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class C1 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "正在计算 " + "1~10:");
Integer total = 0;
for (int i = 0; i <= 10; i++) {
total+=i;
}
Thread.sleep(1000);
System.out.println("计算结果为:"+total);
return total;
}
}
class C2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "正在计算" + "1~100:");
Integer total = 0;
for (int i = 0; i <= 100; i++) {
total+=i;
}
Thread.sleep(1000);
System.out.println("计算结果为:"+total);
return total;
}
}
线程安全的类和线程非安全的类
-
StringBuilder - 线程非安全的字符串类,StringBuffer - 线程安全的字符串类
-
ArrayList - 线程非安全的集合,Vector - 线程安全的集合
-
HashMap - 线程非安全的集合,Hashtable - 线程安全的集合
以上线程安全的类 - api方法使用到了synchronized方法进行了修饰 - 在某个时刻,只能由一个线程去访问,其他线程都是出于等待状态
Thread提供的常用方法
-
static Thread currentThread();//返回当前正在执行的线程的引用对象
-
String getName();//返回线程的名称
-
void start();//启动线程,本质当t1.start()方法的时候底层会让JVM去启动线程,我们的程序是没有资格和能力去真正的把一个线程给启动起来的。CPU调度JVM进程—调用t1线程
-
void setName(String name);//给线程设置名称
-
void setPriority(int n);//设置线程的优先级,数字[1~10].注意点:数字越大,优先级越高.但是并不是优先级越高的线程就一定会优先执行.只是希望它先执行.最终还是要cpu的.
-
void setDaemon(boolean on);//设置成true,这个线程成为了一个后台守护线程了.
//如果后台只剩下守护线程在执行 - 可以结束了
//不需要等到所有的守护线程运行结束才结束
synchronized关键字
- java语言的关键字
- 可用来给对象和方法或者代码块加锁
- 当他锁定一个方法[同步方法]或者一个代码块[同步代码块]的时候,同一时刻最多只有一个线程执行这段代码
- 当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 非公平锁
- 如果同步代码块中出现了异常,那么仍然还是会自动释放锁资源的。
基础方法
在java,每个对象有且仅有一个同步锁,并且同步锁是依赖于对象存在的。当我们调用对象的synchronized修饰的同步方法时候,就是获取了该对象的同步锁。
- 修饰普通方法 - 对象锁 - 不同的对象拥有独立的“一把锁”,每个对象的”锁“是不冲突的- “自助餐”
- 修饰静态方法 - “类锁”- 作用于这个类下的所有的对象 - 这个类实例化出来的所有的对象竞争的是同一把“锁” - ”类锁“ - “一个桌子上吃饭饭”
- 修饰代码块synchronized(this) - 对象锁
- 修饰代码块(XXX.class) - “类锁”
synchronized特性
-
原子性
所谓的原子性代表一个操作或者多个操作,要么执行全部并且执行的过程中不能被任何因素打断.要么就不执行. 比如i++,i+=2,i=i+1;这些操作都不是原子操作[地区,计算,赋值]。这三个步骤不是原子性 - 三个步骤中的任何一个步骤在执行的过程。 其他都可能去打断他。 int x = 10; //原子操作 特殊的 - 了解一下 double x = 3.0d 或者 long x1 = 20L - 不具备原子性的。
-
可见性
原因:遇见synchronized之后,清空本地工作内存,重新从主存去拷贝最新的值
多个线程访问同一个资源时,这个资源的状态,信息等对于其他线程都是可见的
-
有序性
在同一个时刻,只能由一个线程进入.
-
可重入性
当一个线程申请到锁资源并且执行完毕之后[释放],仍然还有机会再去继续申请曾经申请过的锁资源。
JMM
JMM就是java内存模型(java memory model) - 不是JVM内存模型
java内存模型规定所有的变量都存储在主内存中,包括实例变量[类中的非静态属性],静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
描述i++过程 – 多线程安全问题
前提:
-
线程是不能对主内存中的数据进行直接的操作的,必须是在本地的工作内存中去完成的,完成之后,刷回主内存的。
-
线程之间是隔离的,每个线程去执行方法的时候,都会在本地开辟一块区域[栈帧-线程栈],每个线程在本地工作内存中
对资源的修改,那么这个资源的信息、状态,对其他线程都是不可见的。
add方法没有使用synchronized进行修饰的流程
脏数据-本地工作内存中的变量的数据和主内存中的变量的数据的值已经不一样,违背了“JMM中的缓存的一致性”的原则.
无锁资源情况下的add
i++ 不是一个原子性操作的意思就是在它的中间的过程中,可以被被其他线程打断
①T1线程从主存中拷贝变量的副本到本地的工作内存中 - read读取(i=0) => T1
`T1线程让出了CPU执行权力.T0线程获取执行权力 - 从主存拷贝变量的副本(i=0)` => T0
`T0线程直接进行了计算,赋值,重新刷回到主存[i=1]` => T0
`T0线程刷回i到主存之后,此时主存的i=1,T0线程让出cpu执行权`
②在本地的工作内存中对i进行计算,计算完之后,再把自增的数据重新赋值个i => T1
`T1线程继续执行②,由于之前T1线程并没有执行结束,因此它不会从已经更新的主存中重新去拷贝i的副本`
`所以T1线程继续执行的时候,使用的仍然是自己本地工作内存中的那个变量i[read下来的,初始值仍然是0]`
`T1线程i = i + 1,刷回到主存[i=1]`
`因为线程是隔离的,T1线程它是不知道T0线程对i进行了修改的.`
③把最终的本地工作内存中的i的计算结果 - [当前线程执行完毕之后]刷回到主存中[时间是不固定的.但是当前线程执行完毕之后,它肯定会刷回到主存] => T1
[如果是单线程环境下,当T1线程把最终的结果刷回到主存之后,第二次再次调用add()方法的时候,那么T1线程仍然会再次从主存中
获取变量的副本,重复执行①②③)
隐约的信号:如果线程T1没有执行结束,那么它是不会从主存中重新拉取值的
看到的效果是 - 俩个线程都同时操作了i++,但是主存中的i实际上是只加了1次的结果.
如何解决问题
使用synchronized对i++所在的地方进行修饰.
当某个对象调用add方法,得到锁资源的时候,会先清空本地工作内存,清空的目的是为了让后面的操作重新从主存中开呗最新的值!
public synchronized void add(){
i++;
}
- 因为add方法进行了同步 - synchronized进行了修饰了.在某个时刻,只能由一个线程【获得锁资源的】进去执行
- [去读、计算、赋值] - 三个步骤.其他线程是不可能介入的.因为没有获取锁资源的线程都在外部进行同步阻塞
- 当正在执行的线程释放锁资源之前,他会将本地工作内存中的改变刷回到主内存
什么时候工作内存中的改变会同步到主存中
单线程
- 当前的单线程执行方法结束的时候
多线程
- 线程释放锁资源之间
- 线程切换
分析可见性代码
先写再读
-
读线程为什么可以终止循环
public class VisibilityDemo { //实例变量 - 主内存中存在的 private volatile int x; public void writeX(){ x=5; } public void readX(){ while (x!=5){ } if (x==5){ System.out.println("==stop=="); } } public static void main(String[] args) { VisibilityDemo vd = new VisibilityDemo(); Thread t1 = new Thread(new Runnable() { //创建一个写线程 @Override //使用匿名内部类 public void run() { vd.writeX(); } }); Thread t2 = new Thread(()->vd.readX()); //创建一个读的线程 //使用lambda表达式 //线程的执行顺序和你先启动哪个是无关的. //大部分的场景是 -概率更高 - 先启动哪个线程,哪个线程优先执行的机会更大 //如果先写后读 t1.start(); try { //此处的睡一秒,已经足够让我们的写线程把x=5的最终结果刷回到主存了呀 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); //发现x变成了5,所以会执行stop } }
先读后写
-
多个线程访问同一个资源时,这个资源的状态,信息等,对于其他线程都是不可见的
//省略代码与上述相同 //如果先读后写 t2.start();//读 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } t1.start();//写
synchronized深入
-
特点:对象重新获得锁资源的时候,会先清空本地工作内存.强制主存中去拷贝已经更新的变量.
//先读再写,仅仅是在循环体中添加了一行代码 public void readX(){ //写线程过了许久之后,x=5的改变刷回到主存 //但是读线程在执行循环 - 循环体中出现了同步代码块 - 遇到synchronized //一旦遇到synchronized - 尝试去获取锁资源 - 会清空本地工作内存[把x清空掉了] //继续循环又会使用到x,由于本地工作内存已经清空了呀,所以读线程只能到主存中去强制再 // 去拷贝一份x的变量到本地工作内存 //拿到的肯定是写线程已经更新之后的那个值x=5 while(x!=5){ // System.out.println();//增加的代码 - read线程跳出了循环 - 发现了新的x,重新拷贝最新的x } if(x==5) { System.out.println("-----stopped---"); } } 原因: //System.out.println()这个println方法的内部底层是 - 同步代码块 - synchronized private void newLine() { try { synchronized (this) { //... } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } }
如何保证可见性 - 解决方案
-
使用synchronized来保证可见性
-
使用volatile来修饰实例变量
作用1:强制让程序遵守”缓存一致性“协议.如果主存中的变量一旦发生了改变.线程就会强制从主存中重新拷贝这个更新的数据到自己的本地工作内存中去.
作用2:禁止指定重排的 - 单例
x Student s = new Student(); 指令重排 - JVM指令优化之后 ①给对象分配空间 ②空间地址立即给s,s保存到栈 ③对象的初始化 volatile Student s = new Student();//禁止指令重排 ①给对象分配空间③对象的初始化 ②空间地址立即给s,s保存到栈
volatile关键字的作用
-
保证可见性
-
volatile是不会造成阻塞的
-
禁止指令重排
-
不能保证原子性
volatile int i = 0; //i对于俩个线程而言都是可见的.主存中一旦改变了.另外一个线程肯定就能够"看到" - //自己会强制再去重新拷贝一份到本地缓存中 @Override public void run(){ add(); } public void add(){ i++; } t1.start(); t2.start(); //诱导 - //强制让程序遵守"缓存一致性"协议.如果主存中的变量一旦发生了改变. //线程就会强制从主存中重新拷贝这个最新的数据到自己的本地工作内存中去. //最终的i出来的结果<200000 -> 不能保证原子性 //t1执行到最后一步,在自己的工作内存中已经计算结束了, //i已经自增完毕,i=1<---其他线程介入了--->但是还没有来得及刷回到主存. /* t0线程直接计算完,i=1,刷回到主存,结束 但是t1已经对i操作已经结束了,t1的内部已经不会再去操作i 只剩下最后一个动作->i=1也会刷回到主存中 */
volatile和synchronized的区别!
- volatile只能用于变量,而synchronized可以作用于变量、方法和代码块
- 多线程访问volatile不会发生阻塞,而synchronized关键字会发生阻塞
- volatile能够保证数据的可见性,就是在多个线程之间是可见的,不能保证原子性,而synchronized关键字可以保证
- volatile关键字主要解决的是多个线程之间的可见性,而synchronized关键字保证的是多个线程访问资源的同步性
- volatile是可以禁止jvm指令重排的,但是synchronized是不能的
synchronized的底层原理
-
了解
-
每一个锁资源对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)
线程和进程的区别
-
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
进程与进程之间是独立的. 同一个进程内部的多个线程是可以共享进程资源的.
比如:没有国[进程]就没有家[线程] 家[进程] - 拥有多个家庭成员的[线程]
-
资源拥有:同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的。
-
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
-
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
-进程重量级的单位(创建,切换,销毁 - 费时)s
-线程轻量级的单位(创建,切换,销毁 - 比较高)
-
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
线程是处理器调度的基本单位,但是进程不是。
-
两者均可并发执行。
一个线程只属于一个进程,但是一个进程可以拥有多个线程,但至少一个线程资源分配给进程,同一进程中所有线程共享该进程的所有资源。
线程状态 - 线程生命周期
-
-
简介:线程的生命周期.
- New:新建状态/瞬态,当线程对象创立后,即进入了新建状态,如:Thread t = new MyThread()
- Runnable:就绪状态,当调用线程对象的start()方法(t.start()),线程就进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待cpu调度执行,并不是说执行了t.start()此线程立即就会执行
- Running:运行状态,当cpu开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
- Blocked:阻塞状态,处于运行状态中的线程由于某种原因,暂时放弃对cpu的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被cpu调用以进入到运行状态
- Dead:死亡状态(结束状态),线程执行完了或者因异常退出了run()方法,该线程结束生命周期
- (1)就绪状态是进入到运行状态的唯一入口 (2)线程想要进入到运行状态执行,首先必须处于就绪状态中 (3)根据阻塞产生的原因,阻塞状态又可以分为三种: 【1】等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态 【2】同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态 【3】其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态.之前获取键盘输入.
守护线程
-
GC - 运行在后台的 - 负责回收垃圾对象的.
-
核心:线程结束的时候不需要关心后台的守护线程是否也运行结束.线程是不会等后台的守护线程全部运行结束才结束.
-
当后台只有守护线程在执行的时候,就可以认为线程可以结束了.
public class DaemonDemo { public static void main(String[] args) { Thread t1 = new T1(); Thread t2 = new T2(); //将打印数字的线程 - 设置成后台守护线程 //如果后台只剩下守护线程在执行 - 可以结束了 //不需要等到所有的守护线程运行结束才结束 t2.setDaemon(true); //如果t1和t2都不是守护线程 - main线程一定是等待两个线程全部执行完之后,才会结束 t1.start(); t2.start(); } } class T1 extends Thread{ @Override public void run() { for (int i = 65; i <100 ; i++) { System.out.println((char)i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class T2 extends Thread{ @Override public void run() { for (int i = 0; i <100 ; i++) { System.out.println(i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
Lock - 同步代码
简介:它是一个接口,下面有很多实现类,笔试题lock和synchronized的区别!
-
lock是接口,synchronized它是一个关键字
-
lock锁是一个显示锁(手动申请锁,手动释放锁),synchronized隐式锁(自动申请/释放锁)
-
lock手动申请锁**(对象锁)**
-
lock是锁代码块
-
lock出现异常的时候,是不会主动释放资源的.
/** * 本类用于演示: Lock同步代码 - 同步代码块 * 申请的是对象锁 - 相同的对象才会去竞争一把锁. * 如果出现了异常 - 是不会主动释放锁的. */ public class LockHelloDemo { //构建lock对象 - 接口 Lock lock = new ReentrantLock(); public void add(){ try { //begin.. //多个线程就会去竞争"锁资源" lock.lock();//手动申请"锁资源" - 显示锁 //在某个时刻只能由一个线程进入去执行... //同步代码开始 System.out.println(Thread.currentThread().getName()+":0"); try { //sleep如果出现同步代码中,它并不会释放锁资源,只会让出cpu时间片段 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":1"); //end... //同步代码结束 }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public static void main(String[] args) { LockHelloDemo lockHelloDemo = new LockHelloDemo(); Thread t1 = new Thread(()->lockHelloDemo.add()); Thread t2 = new Thread(()->lockHelloDemo.add()); t1.start(); t2.start(); } }
死锁
死锁产生的条件
-
**互斥条件:**指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
-
请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
-
**不剥夺条件:**指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
-
**环路等待条件:**指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
只要打破四个条件的一个,就可以防止死锁.
死锁是不可避免,但是需要写程序的破坏四个条件中的一个.
静态域容易产生死锁.
-
四种常见的线程池 - 必考
线程池的返回值ExecutorService简介
ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程
- Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务
- Executors.newFixedThreadPool(int n):创建一个可重用固定个数的线程池,以共享的*队列方式来运行这些线程。
- Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行
- Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。