进程和线程
一个程序至少一个进程,一个进程至少一个线程。
线程不能单独执行运行的,它一定是运行在进程的内部的
进程[正在执行中的应用程序]:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,
竞争计算机系统资源的基本单位。 - “多任务操作系统” - “多个进程在**”同时”**在运行” - CPU分配资源 - “分时分片”
处理器[cpu]调度的基本单位
线程:是进程的一个执行单元,是进程内部调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
一个进程可以拥有多个线程的同一个进程内部的多个线程是可以共享进程资源的.
比如启动Java程序
1. 启动Java程序实际上就是启动JVM[对字节码文件进行逐行翻译 - 翻译成底层的机器语言]
开启一个JVM进程 - jvm.exe[windows中的可执行文件.C语言写的程序直接编译成了.exe可执行文件]
2. 当JVM进程启动之后,同时开启俩个线程 - 分别是main主线程以及一个GC线程[后台守护线程]
守护线程 - 如果后台只剩下守护线程在执行的时候,那么进程就会结束.
进程的结束不需要等到守护线程全部执行完才会结束.
3. main线程负责执行main方法中的程序 - main方法中的程序全部执行完之后,main线程运行结束
GC线程负责进行垃圾对象的回收
创建线程的传统的方式
写一个类去继承java.lang.Thread类 - 重写里面的run方法
写一个类去实现java.lang.Runnable接口 - 重写里面的run方法
需要用到java.lang.Thread类中的构造方法 Thread(Runnable r);
Thread和Runnable区别
第一种方式extends Thread方式 - 共享代码,不共享资源
只有将资源设置成静态的 - 也是进行一个资源的共享的
第二种方式implements Runnable接口 - 共享代码,共享资源
第三种方式Callable接口
推荐配合Future+线程池一起使用
Callable和Runnable接口的区别
- Callable可以通过Future来得到异步计算的结果 - 拿到线程执行之后的结果.
- Callable调用的是call方法,Runnable调用的是run方法.
- call方法是可以抛出一个异常列表的,但是run方法是不允许抛出异常列表的
Callable+FutureTask
package tech.aistar.day15.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 本类用来演示: 拿到线程执行之后的结果
* Callable+FutureTask -> 线程执行的结果
*
* @author: success
* @date: 2021/8/12 8:40 上午
*/
public class CallableDemo {
}
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
}
}
Callable+Future+线程池
package tech.aistar.day15.callable;
import java.util.concurrent.*;
/**
* 本类用来演示: Callable+Future<T> + 缓存线程池
*
* @author: success
* @date: 2021/8/12 8:53 上午
*/
public class CallablePoolDemo {
public static void main(String[] args) {
//构建一个可缓存的线程池对象
ExecutorService executorService = Executors.newCachedThreadPool();
//向这个缓存池中提交任务
//第一个任务 - 负责计算1~100
Future<Integer> future1 = executorService.submit(new C2());// 准备启动一个线程
//再向这个缓存池中提交一个任务
//第二个任务 - 负责计算1~10
Future<Integer> future2 = executorService.submit(new CC());
//任务的数量和线程的数量一定一样吗???
//有个效果 -> 俩个线程 - 同时执行这俩个异步任务
//pool-1-thread-1-正在计算1~100
//pool-1-thread-2正在计算1~10
//需要将俩个线程执行的结果进行一个相加的操作
try {
//get()方法肯定是会等到线程执行完之后.才会继续走
Integer result1 = future1.get();
Integer result2 = future2.get();
//如果俩个线程get()方法不走完,那么main线程也是不会继续执行的
System.out.println("开始整合...");
int result = result1 + result2;
//main线程
//5050+55 = 5105
System.out.println(Thread.currentThread().getName()+":"+result);
//System.out.println(future1.get()+future2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
class C2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"-正在计算1~100");
int total = 0;
for (int i = 1; i <= 100 ; i++) {
total += i;
}
Thread.sleep(2000);
return total;
}
}
class CC implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"正在计算1~10");
int total = 0;
for (int i = 1; i <=10; i++) {
total+=i;
}
Thread.sleep(2000);
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中的缓存一致性”的原则.
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进行了修饰了.在某个时刻,只能由一个线程[获得锁资源的]进去执行
- [读取,计算,赋值] - 三个步骤中.其他线程是不可能介入的.因为没有获取锁资源的线程都在外部进行同步阻塞.
- 当正在执行的线程释放锁资源之前,它会将本地工作内存中的改变刷回到主存.
什么时候工作内存中的改变会同步到主存中
单线程
- 当前的单线程执行方法结束的时候
###多线程
- 线程释放锁资源的时候
- 线程切换
分析可见性代码
先写再读
读线程为什么可以终止循环.
//如果先写后读 t1.start(); //x=5 //此处的睡一秒,已经足够让我们的写线程把x=5的最终结果刷回到主存了呀 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } t2.start();//发现x变成了5
先读再写
多个线程访问同一个资源时,这个资源的状态,信息等对于其他线程都是不可见的
//如果先读后写
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:禁止指令重排的 - 单例
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++实现)。
进程和线程区别
线程是存在于进程内部的.一个进程内部可以拥有多个线程的.
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
进程与进程之间是独立的. 同一个进程内部的多个线程是可以共享进程资源的.
比如:没有国[进程]就没有家[线程] 家[进程] - 拥有多个家庭成员的[线程]
资源拥有:同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的。
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
-进程重量级的单位(创建,切换,销毁 - 费时)
*-**线程轻量级的单位(*创建,切换,销毁 - 比较高)
线程是处理器调度的基本单位,但是进程不是。
程序在执行过程中分配和管理资源的基本单位 - 系统是把资源[内存]分给给进程,这个进程内部的线程才会去共享进程得到的这些资源.
两者均可并发执行。
一个线程只属于一个进程,但是一个进程可以拥有多个线程,但至少一个线程资源分配给进程,同一进程中所有线程共享该进程的所有资源。
线程状态 - 线程生命周期
简介:线程的生命周期.
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处理完毕时,线程重新转入就绪状态.之前获取键盘输入.
作业-synchronized
Account(id,accno,balance=100) - “临界资源” - “多个线程需要竞争的共享 - 多个线程需要操作的同一个对象”
写一个线程类 - 同步方法[取钱-10] - 一定是只能在某个时刻只能由一个线程进去执行
创建俩个线程对象 - 老公和老婆 -> 调用同步取钱的方法
无论哪个线程在执行 - 100 - 90 - 80 - …. 0
效果
老公正在取钱....余额为:90 老公正在取钱....余额为:80 老公正在取钱....余额为:70 老婆正在取钱....余额为:60 老公正在取钱....余额为:50 .... ... 0
守护线程
GC - 运行在后台的 - 负责回收垃圾对象的.
核心:线程结束的时候不需要关心后台的守护线程是否也运行结束.线程是不会等后台的守护线程全部运行结束才结束.
当后台只有守护线程在执行的时候,就可以认为线程可以结束了.
package tech.aistar.day15.thread02; /** * 本类用来演示: 后台的守护线程 * * @author: success * @date: 2021/8/11 9:03 上午 */ 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 <=90 ; 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出现异常的时候,是不会主动释放资源的.
demo
package tech.aistar.day15.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 本类用来演示: Lock同步代码 - 同步代码块
*
* 申请的是对象锁 - 相同的对象才会去竞争一把锁.
*
* 如果出现了异常 - 是不会主动释放锁的.
*
* @author: success
* @date: 2021/8/11 1:32 下午
*/
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 hello = new LockHelloDemo();
//1. 创建第一个线程对象
Thread t1 = new Thread(()->{
hello.add();
});
Thread t2 = new Thread(()->{
hello.add();
});
t1.setName("one");
t2.setName("two");
t1.start();
t2.start();
}
}
面试题-synchronized和Lock有什么区别?
synchronized和lock都是属于独占锁.
实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁
是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
Lock接口中提供的方法 1. void lock();//手动上锁 2. boolean tryLock();//如果获取锁失败 - 直接返回false,如果获取锁成功,返回true if(lock.tryLock()){ } 3. boolean tryLock(long time, TimeUnit unit);//如果在尝试获取锁的过程中等待超过了time,那么就会导致获取锁失败
获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可中断、可公平和不公平、细分读写锁提高效率
读锁 - java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock - 共享锁 - 允许多个线程去读.
写锁 - java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock - 互斥锁 - 一次只能由一个线程去写.
不可中断synchronized - A线程竞争到锁资源 - 顺利进入同步代码块执行 - 只要A资源不释放这个锁资源.B线程只能在外面等待. 可中断Lock -> A线程顺利执行同步代码,B线程如果tryLock方法话,那么B线程不断尝试获取锁资源.如果设置了超时等待时间.B线程等太久.已经超过了设置的时间.B线程将不会再傻乎乎继续等待下去[可中断],B线程就有可能去干别的事情了.
锁
乐观锁,悲观锁[synchronized] - 数据库的时候
互斥锁 - synchronized和lock
读写锁
锁升级
锁粗化
偏向锁
分布式锁
线程的通信 - 重点
同程公司笔试题 - 循环打印abc ABC 123 abc ABC 123
打印小写字母abc的线程,打印大写字母ABC线程,打印数字123的线程
保证必须按照上面的顺序进行输出 - synchronized - wait和notify/notifyAll不能解决原因是
notify随机唤醒,notifyAll唤醒所有
使用Lock中的同步队列 - 解决方案 - 唤醒指定的等待队列上的某个线程.
Lock接口中提供的构建等待队列的方法 - Condition newCondition();
Contion提供的关于线程通信的俩个方法 - 出现在同步代码内部 - lock()->unlock()内部
- void await();//当前线程会进入到阻塞状态.必须要等其他线程唤醒的
- void signal();//哪个线程的Conition对象去调用signal()方法,哪个线程就会被唤醒
demo
package tech.aistar.day15.lock; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 本类用来演示: * * 同程公司笔试题 - 循环打印abc ABC 123 abc ABC 123 * * 打印小写字母abc的线程,打印大写字母ABC线程,打印数字123的线程 * * 使用Lock中的同步队列 - 解决方案 - **唤醒指定的等待队列上的某个线程.** * * @author: success * @date: 2021/8/11 2:29 下午 */ public class LockTongxinDemo { private Lock lock = new ReentrantLock(); //有几个线程,就构建几个队列 //打印小写字母的 private Condition c1 = lock.newCondition(); //打印大写字母的 private Condition c2 = lock.newCondition(); //打印数字的 private Condition c3 = lock.newCondition(); //定义了一个标记 private int count = 0; //count = 0,打印小写字母的执行 //count = 1,打印大写字母的执行 //count = 2,打印数字的执行 //1. void await();//当前线程会进入到阻塞状态.必须要等其他线程唤醒的 //2. void signal();//哪个线程的Conition对象去调用signal()方法,哪个线程就会被唤醒 public void printLower(){ while(true){ try { lock.lock(); if(count!=0){ //等待 //一开始,第一次count=0,说明打印小写字母的线程正常执行 c1.await(); } System.out.print("abc"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count=1; c2.signal();//唤醒打印大写字母的线程 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public void printUpper(){ while(true){ try { lock.lock(); if(count!=1){ try { c2.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print("ABC"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count=2; c3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public void printNum(){ while(true){ try { lock.lock(); if(count!=2){ c3.await(); } System.out.print(123); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count = 0; c1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { LockTongxinDemo demo = new LockTongxinDemo(); Thread t1 = new Thread(()->{ demo.printLower(); }); Thread t2 = new Thread(()->{ demo.printUpper(); }); Thread t3 = new Thread(()->{ demo.printNum(); }); t1.start(); t2.start(); t3.start(); } }
面试题 - 常用方法区别
wait方法和sleep方法的区别?
a. wait方法一定是出现在synchronized同步代码内部的.[sleep是可以放在同步/不同步的代码内部的]
b. wait方法,执行这个方法的线程就会释放锁,释放cpu.但是sleep是不会释放锁的.只是释放了cpu - 核心的区别
c. sleep - 线程会自动进入到阻塞状态[其他阻塞状态],一旦睡眠时间结束.会自动从阻塞状态恢复到可就绪态 - 等到cpu到来 - 运行态.
wait - 线程会自动进入到等待阻塞状态,自己不会主动”醒过来”,必须是其他线程调用notify或者notifyAll,进入到锁定状态[再次尝试 去获取锁]
- sleep:Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU(阿里面试题 Sleep释放CPU,wait 也会释放cpu,因为cpu资源太宝贵了,只有在线程running的时候,才会获取cpu片段),提供其他线程运行的机会且不考虑优先级,但如果有同步锁则sleep不会释放锁即其他线程无法获得同步锁 可通过调用interrupt()方法来唤醒休眠线程。
- yield:让出CPU调度,Thread类的方法,类似sleep只是不能由用户指定暂停多长时间 ,并且yield()方法只能让同优先级的线程有执行的机会。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳。
- wait:Object类的方法(notify()、notifyAll() 也是Object对象),必须放在循环体和同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁
join:一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
package tech.aistar.day15.thread02; /** * 本类用来演示: * join:一种特殊的wait[等待],当前运行线程[mother]调用另一个线程[son]的join方法, * 当前线程[mother]进入阻塞状态直到另一个线程[son]运行结束等待该线程终止。 * 注意该方法也需要捕捉异常。等待调用join方法的线程结束,再继续执行。 * * @author: success * @date: 2021/8/11 9:21 上午 */ public class JoinDemo { public static void main(String[] args) { Thread mother = new Mother(); mother.start(); } } class Mother extends Thread{ @Override public void run() { System.out.println("妈妈正在做饭..."); System.out.println("妈妈发现酱油了..."); System.out.println("妈妈让熊孩子打酱油..."); //应该是希望儿子的线程介入进来了吧... Thread son = new Son(); son.start();//启动儿子线程 //希望妈妈的线程先暂停一下,然后等儿子的线程跑完,妈妈的线程才继续跑. try { son.join(); } catch (InterruptedException e) { e.printStackTrace(); } //期望的是这条语句一定是出现在最后的... System.out.println("妈妈可以继续做饭了~~~"); } } class Son extends Thread{ @Override public void run() { System.out.println("熊孩子接到任务...屁颠颠打酱油去了..."); for (int i = 5; i >=0; i--) { System.out.println("熊孩子还有"+i+"分钟就回来了..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("熊孩子把酱油打回来了~~~"); } }
线程之间的通信
wait:Object类的方法(notify()、notifyAll()),必须放在循环体和同步代码块中,执行该方法的线程会释放锁,
进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁
生产者和消费者
单个生产者和单个消费者
生产者线程 - 负责生产产品-box[把产品放入到box]
消费者线程 - 负责消费产品-box[从box中去获取产品]
通信的流程
俩条线程并发执行 - 调用的是不同的方法
假设消费者线程先执行 - 直接从box中去获取产品.但是此时此刻box中尚无产品.
消费者线程就需要调用wait方法 - 自己会释放锁资源,消费者线程进入到线程等待池[意味着当前的线程会进入到等待阻塞的状态]
-getter方法
假设生产者线程 - setter方法
先执行了.发现box中没有产品的,顺利放入一个产品到box中的.但是放完之后.生产者还是会继续执行.继续执行的时候又会调用
setter方法.这个时候,不能允许生产者线程连续再放 - 就得调用生产者线程的wait方法,同时还需要唤醒消费者线程过来消费.
消费者线程一旦消费成功,它还是会继续执行自己的getter方法.发现box中没有了.回到第2个步骤
demo
package tech.aistar.day15.thread02;
/**
* 本类用来演示: 线程之间的通信 - 生产者和消费者
*
* 效果是:必须是生产者线程先进去执行.
* 放1个
* 取1个
* 放2个
* 取2个
* 放3个
* 取3个
*
* 一次性放,一次性取.
* 不能连续出现俩次放/取.
*
* @author: success
* @date: 2021/8/11 9:55 上午
*/
public class ThreadTongXinDemo {
public static void main(String[] args) {
//典型的不共享代码,但是共享资源.
Box box = new Box();
Thread product = new ProductThread(box);
Thread customer = new CustomerThread(box);
product.setName("生产者");
customer.setName("消费者");
product.start();
customer.start();
}
}
//定义一个临界资源
class Box{
private int content;//产品
//假设的是是要调用getter方法 - 消费了 - box中没有东西了.
//如果box中有东西 - 刚生产完,flag为true
//如果box中没有东西 - 刚消费完 - flag为false
private boolean flag;//标志
//生产者线程去调用的
//线程通信的方法wait,notify,notifyAll方法必须存在于循环体的同步方法中.
public synchronized void setContent(int content) {
if(flag){//box中是有东西的
//一旦调用wait方法之后,线程会释放锁.并且会自动进入到线程等待池中
//进入到一个阻塞的状态.
//它自己是不会主动被唤醒的.必须由其他线程调用notify或者notifyAll才能够
//notify - 随机唤醒线程等待池中的一个线程
//notifyAll - 唤醒所有的等待的线程
try {
wait();//为了防止生产者线程连续执行..
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产者顺利去执行.
//设置flag
flag = true;
this.content = content;
//通知一下消费者来消费.消费者很有可能已经处于等待阻塞的状态.
notifyAll();//唤醒全部等待阻塞的线程
}
//消费者线程去调用的
public synchronized int getContent() {
if(!flag){//1. 保证消费者第一次执行,肯定会进来
try {
wait();//释放锁,自己是不会主动"醒过来的"
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = false;//消费完了...
//唤醒一下生产者
notifyAll();
return content;
}
}
//定义生产者线程
class ProductThread extends Thread{
private Box box;
private int i = 0;//产品
public ProductThread(Box box){
this.box = box;
}
@Override
public void run() {
//不断去生产
while(true){
System.out.println(Thread.currentThread().getName()+"=>放"+(++i)+"个");
box.setContent(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//定义一个消费者线程
class CustomerThread extends Thread{
private Box box;
public CustomerThread(Box box){
this.box = box;
}
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName()+"->取"+box.getContent()+"个");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
死锁 - 笔试题
“哲学家吃饭的问题”
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程.
死锁产生的条件
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{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, 优先级)执行。
作业
Lock和Condition - 生产者和消费者的demo
编写俩个线程,一个线程用来计算2
100000之间的素数的个数.第二个线程用来计算1000000200000之间的素数的个数将俩个线程计算的结果相加.
面试题 - 请你谈谈java.util.concurrent下的api的认识!
主题 - 围绕线程池去问题了.
import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future;