下面就让我们了解一下什么是线程
目录
目录
前台线程 VS 后台线程/精灵线程(daemon)/守护线程
进程线程(thread)的领域
1.现在暂时介绍的都是操作系统层面上的线程
2.进程(proce)和线程(thread)的关系
进程 - 线程 是 1:m 的关系
一个线程一定属于一个进程;一个进程下可以允许有多个线程。
一个进程内至少有一个线程,通常把这个一开始就存在线程,称为主线程(main thread),主线程和其他线程之间地位是完全相等的,没有任何特殊性
3.为什么要引入thread这一个概念
由于进程这一个概念天生就是资源隔离的,所以进程之间进行数据通信注定是一个高成本的工作。现实中,一个任务需要多个执行流一起配合完成,是非常常见的。所以,需要一种方便数据通信的执行流概念出来,线程就承担了这一职责。
4.什么是线程:
线程是操作系统进行调度的基本单位
线程变成了独立执行流的承载概念,进程退化成只是资源(不含CPU)的承载概念。
比如:运行一个程序,没有线程之前,OS创建进程,分配资源,给定一个唯一的PC,进行运行。有了线程之后,OS创建进程,分配资源,创建线程(主线程),给定一个唯一的PC,进行运行。
OS针对同一个进程下的线程实现“连坐”机制;一旦一个线程异常退出,OS会关闭该线程所在的整个进程
进程VS线程
1.概念的区别
2.由于进程把调度单位这一个职责让渡给线程了,所以,使得单纯进程的创建销毁适当简单
3.由于线程的创建和销毁不涉及资源分配,回收的问题,所以,通常理解,线程的创建/销毁成本要低于进程的成本
JVM中规定的线程
Java线程 VS OS线程(原生线程)
不同的JVM有不同的实现,它们的外在表现基本一致,除了极个别的几个现象。Java线程中一个线程异常关闭,不会连坐。
我们使用的HotSpot实现(JVM)采用,使用一个OS线程来实现一个Java线程
Java中由于有JVM的存在,所以使得Java中做多进程级别的开发基本很少,Java中的线程还克服了很多OS线程的缺点,因此,在Java开发中,我们使用多线程模型来进行开发,很少使用多进程模型。
Java线程在代码中的体现
1.java线程在代码中是如何体现的
每一个线程都被抽象为java.lang.Thread类(包括其子类)的一个对象
2.如何在代码中创建线程(最基本)
1.通过继承Thread类,并且重写run方法
实例化该类的对象 -> Thread对象
public class MyFirstThreadClass extends Thread {
@Override
public void run() {
//这个方法下写的所有代码,如果正确创建线程的话,都会运行在新的线程执行流中
System.out.println("这是我的第一个线程");
}
}
2.通过实现Runnable接口,并且重写run方法。
实例化Runnable对象
利用Runnable对象去构建一个Thread对象。
public class MyFirstTask implements Runnable{
@Override
public void run() {
System.out.println("这是我的第一个任务的第一句话");
}
}
3.启动线程
当手中有一个Thread对象时调用其start()方法
注意:1.一个已经调用过start()不能再次调用start(),否则就会有异常发生
2.千万不要调用成run()
public class MyThread extends Thread{
@Override
public void run() {
//打印当前执行语句的线程的名字
System.out.println("我是" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
// t.start();
t.run();
}
}
调用run方法,就和线程没关系了,完全是在主线程下在运行代码。
怎么去理解t.start()做了什么?
线程状态:
新建 —— 就绪 —— 运行 —— 结束
t.start()只做了一件事情,就是把线程的状态从新建变成了就绪,不负责分配CPU。线程把加入到线程调度器(不区分是OS还是JVM实现的)的就绪队列中,等待被调度器选中分配CPU。从子线程进入到就绪队列这一刻起,子线程和主线程在地位上就完全平等了。所有哪个线程被选中去分配CPU,就完全听天由命了,先执行子线程中的语句还是主线程中的语句理论上都是可能的,大概率是主线程中的语句先执行,因为t.start()是主线程中的语句,换言之,这条语句被执行了,说明主线程现在正在CPU上(主线程是运行状态),所以,主线程刚刚执行玩t.start()就马上发生线程调度的概率不大。
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("我是MyThread 类下的 run方法中的语句,会运行在子线程中");
}
}
public class Main {
public static void main(String[] args) {
System.out.println("请猜猜是主线程的打印先出现还是子线程下的先出现?这个顺序是确定的吗?");
//实际上,先打印哪个是不确定的
//虽然大概率情况下是main中的语句先打印的
MyThread t = new MyThread();
t.start();
System.out.println("我是Main 类下的 main静态方法中的语句,会运行在主线程中");
}
}
线程和方法调用栈的关系
每个线程都有自己独立的调用栈,由于每个线程都是独立的执行流,A在调用过哪些方法,和B根本就没关系。表现为每个线程都有自己的独立的栈。
线程中最常见的属性
1. id 本进程(JVM进程)内部分配的唯一的 id ,只能get不能set
public void run() {
System.out.println(this.getId());
}
2.name(名字)为了方便开发者看,JVM并不需要这个属性,默认情况下,如果没有给过名字,线程名字遵守Thread—……;第一个是Thread-0,Thread-1,Thread-2.可以set也可以get,可以通过setName()设置,也可以通过Thread()构造方法设置
public void run() {
System.out.println(this.getName());
}
public Mythread(){
setName("我是小红");
}
public Mythread(){
super("我是小红");//调用父类的(thread)的构造方法
}
id就是一个线程的身份证号(出生的时候被分配,无法被修改,不能重复);
name就是一个线程的名字(可以重复,可以修改)
3.在 Java 代码中看到的线程状态
1)理论中的状态
2)Java代码中实际看到的状态
只能获取不能设置,状态的变更时JVM内部控制的
new:新建
runnable:就绪 + 运行
terminated:结束
blocked + waiting + timed_waiting:阻塞
线程中可以get/set自己的优先级
注意:这个优先级的设置,只是给JVM一些建议,不能强制让哪个线程先被调度。
public class Main {
public static void main(String[] args) {
Thread main = Thread.currentThread();
System.out.println(main.getPriority());
main.setPriority(Thread.MAX_PRIORITY);
System.out.println(main.getPriority());
main.setPriority(Thread.NORM_PRIORITY);
System.out.println(main.getPriority());
main.setPriority(Thread.MIN_PRIORITY);
System.out.println(main.getPriority());
}
}
前台线程 VS 后台线程/精灵线程(daemon)/守护线程
后台线程一般时做一些支持工作的线程
前台线程一般是做一些有交互工作的
举个栗子:
写了一个音乐播放器
1.线程响应用户点击动作(前台)
2.线程去网络上下载歌曲(后台)
public class Main {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println(t.isDaemon()); //返回 true 代表是后台(daemon)线程
t.setDaemon(true); //修改字迹是前台还是后台线程
}
}
JVM进程什么时候才能退出:所有的前台线程退出了,JVM进程就退出了
1.必须要求所有前台线程都退出,和主线程没关系
2.和后台线程没关系,即使后台线程还在工作,也正常退出
控制另外的线程
A线程:
1.创建B线程,并启动B线程(中间A可以做点其他的工作)
2.等待B线程完成所有工作(B线程运行结束)
3.打印B线程已经退出了
B线程:
计算一个比较耗时的任务
我们用Thread.join()方法来实现
举个栗子:
1.b = new B(); b.start();
2.吃饭
3.b.join(); //这个方法会阻塞,直到B运行结束才会返回
4.这个时候B一定已经退出了
5.打印b结束了
public class Main {
private static class B extends Thread{
@Override
public void run() {
//模拟B要做很久的任务
try{
TimeUnit.SECONDS.sleep(5);
}catch (InterruptedException e){
e.printStackTrace();
}
println("B说:我的任务已经完成");
}
}
private static void println(String msg){
Date date = new Date();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(format.format(date) + ":" + msg);
}
public static void main(String[] args) {
B b = new B();
b.start();
println("A自己先去吃饭" );
//有join和没有join的区别
println("A 说 :B 给我把钱送来了,结账走人");
}
}
这样明显是不对的,说明A吃了霸王餐
public static void main(String[] args) throws InterruptedException {
B b = new B();
b.start();
println("A自己先去吃饭" );
//有join和没有join的区别
b.join();
println("A 说 :B 给我把钱送来了,结账走人");
}
}
加入join();后A 一定是等B把钱送来之后再结账走人
利用join实现线程的一个小练习:
并发对一个数组进行归并排序
四个线程分别对各自的每一段进行排序(Arrays.sort)
当t1,t2,t3,t4的工作全部完成之后
执行4路归并(省略)
前提:一个进程下的线程,共享的是同一块内存资源
public class ConcurrentSort {
//进行排序的线程
static class SortWorker extends Thread{
private final long[] array;
private final int fromIndex;
private final int toIndex;
//利用构造方法,将待排序的数组区间情况传入
//对array 的[fromIndex , toIndex]进行排序
SortWorker(long[] array, int fromIndex, int toIndex){
this.array = array;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@Override
public void run() {
//具体的排序过程,这里使用Arrays.sort做模拟
Arrays.sort(array,fromIndex,toIndex);
}
}
//记录排序耗时
public static void main(String[] args) throws InterruptedException {
long[] array = ArraysHelper.generateArray(40000000);
//分别是
// [0,10000000]
//[10000001,20000000)
//[20000001,30000000)
//[30000001,40000000)
long s = System.currentTimeMillis();
Thread t1 = new SortWorker(array,0,10000000);
t1.start();
Thread t2 = new SortWorker(array,10000001,20000000);
t2.start();
Thread t3 = new SortWorker(array,20000001,30000000);
t3.start();
Thread t4 = new SortWorker(array,30000001,40000000);
t4.start();
//4个线程开始分别的进行排序
//等待4个线程全部排序完毕
t1.join();
t2.join();
t3.join();
t4.join();
//4个线程一定全部结束了
//TODO:进行4路归并,将4个有序数组,归并成一个有序数组
long e = System.currentTimeMillis();
long elapsed = e - s ;
System.out.println(elapsed);
}
}
在单线程的情况下实现
public class SingleSort {
//记录排序耗时
public static void main(String[] args) throws InterruptedException {
long[] array = ArraysHelper.generateArray(40000000);
//分别是
// [0,10000000]
//[10000001,20000000)
//[20000001,30000000)
//[30000001,40000000)
long s = System.currentTimeMillis();
Arrays.sort(array,0,10000000);
Arrays.sort(array,10000001,20000000);
Arrays.sort(array,20000001,30000000);
Arrays.sort(array,30000001,40000000);
//TODO:进行4路归并,将4个有序数组,归并成一个有序数组
long e = System.currentTimeMillis();
long elapsed = e - s ;
System.out.println(elapsed);
}
}
我们可以明显看出单线程的耗时比多线程长
小Tips:
1.多核环境下,并发排序的耗时 < 串行排序的耗时(我们在上面看到的现象)
单线程一定能跑在一个CPU(核)上,多线程意味着可能工作在多个核上(核亲和性)
2.单核环境下,并发排序的耗时也能小于么?
即使在单核环境下,并发的耗时也可能较少。
本身计算机下就有很多线程在等待分配CPU,比如,现在有100个线程。意味着公平的情况下,我们的排序主线程,只会被分配1/100的时间。当并发时,我们使用4个线程分别排序,除其他的99个之外,计算机中共有 99 + 4 = 103 个线程,我们的4个线程同属于一个进程,分给我们的进程的时间占比4/103 > 1/100。
所以,即使单核情况下,我们一个进程中的线程越多,被分配到的时间片是越多的
线程越多越好吗?
当然不是
1)创建线程也是需要耗费时间的
2)即使理想情况下,不考虑其他耗时,极限也就是100%
3)线程调度也需要耗时(OS从99个线程中挑一个的耗时 和 从 9999 个线程中挑一个的耗时不同)
CPU是一个公共资源,写程序的时候也是要考虑公德心的。如果是好的OS系统,可能会避免这个问题。
3.并发排序的耗时就一定小于串行的吗?
不一定
串行的排序:t = t(排区间1)+ t(排区间2)+ t(排区间3)+ t(排区间4)
并发的排序:t = 4*t(创建线程)+ t(排区间1)+ t(排区间2)+ t(排区间3)+ t(排区间4)+ 4*t(销毁)
所以我们要写多线程代码的原因之一:提升整个进程的执行速度(尤其是计算密集性的程序)
Thread下的几个常见静态方法
1.Thread.sleep(…) 让线程休眠几毫秒
TimeUnit.SECONDS.sleep == Thread.sleep(1000)
从线程状态的角度来说,调用sleep(…),就是让当前线程从运行态 -> 阻塞态(等待某个条件:要求时间过去……之后),当条件满足时(时间真的过去了…)线程从 阻塞态 -> 就绪。开始接着之前的指令执行。外部表现就是让线程休眠了一段时间。
2.Thread.currentThread();
Thread引用,指向一个线程对象,执行的就是在哪个线程中调用的该方法,返回哪个对象。
3.Thread.yield()
让线程让出CPU
线程从 运行 ——> 就绪状态,随时可以继续被调度回CPU
public class Main {
static class PrintWhoAmI extends Thread{
private String who;
public PrintWhoAmI(String who){
this.who = who;
}
@Override
public void run() {
while (true){
System.out.println("我是" + who);
if (who.equals("张三")){
Thread.yield();
}
}
}
}
public static void main(String[] args) {
PrintWhoAmI 张三 = new PrintWhoAmI("张三");
PrintWhoAmI 李四 = new PrintWhoAmI("李四");
张三.start();
李四.start();
}
}
张三让出了CPU所以打印出来的基本上是李四,但是张三还是会出现,只是几率较小。
yield主要用于执行一些耗时较久的计算任务时,为让防止计算机处于”卡顿“的现象,时不时的让出一些CPU资源给OS的其他进程
线程的控制之通知线程停止
举个栗子:
A叫来B干活
一些突发情况发生了,需要让B停止工作(即使分配它的任务还没有完成)
所以A需要让B停止
1.暴力停止,直接kill掉B (目前基本上已经不采用了,原因是直接杀掉B,不知道B把工作进行的如何了,不可控)
2.更优雅地(garceful)的方式就是,和B进行协商。
A给B主动发一个信号,代表B应该停止了(发信息)
B在一段时间里,看到了停止信号之后,把手头的工作做到一个阶段完成,主动退出。(需要我们写代码完成)
A主动让B停止。 b.interrupt();
只是发了一个消息(官方:给B设置了一个停止标志)实际上并不会影响B的运行
B如何感知到有人让它停止。
情况1:B正在正常执行代码,可以通过一个方法来判定
静态方法 Thread.interrupted()检测当前线程是否被终止
true:有人让我们停止 false:没人让我们停止
B的代码类似:
while(true){
//写代码
//看一眼手机,有没有人让我们停止
if(Thread.interrupted()){
// 有人让我们停止
break;也可以是其他方式,至于B要不要停,完全是代码控制的
}
情况2:B可能正处于休眠状态(比如 sleep,join),意味着B无法立即执行Thread.interrupted()此刻,JVM的处理方式是,以异常的形式通知B:InterruptedException,当B处于休眠状态时,捕获了nterruptedException,代表有人让我们停止,具体要不要停,什么时候停,怎么停,完全我们自己做主。
多线程的应用场景
1.计算密集性任务,为了提升整体速度,可以引入多线程
2.当一个执行流因故阻塞时,为了还能处理其他事务,可以引入多线程
举个栗子:看饭店,如果只有一个人,既要当前台又要当厨师,如果用户点了耗时比较久的菜(把我占用了),导致没法接待新顾客,有了线程(厨师)之后我只负责前台工作,做菜交给另一个人,即使做菜比较慢也不影响其他用户的体验。
public class Main {
static long fib(int n){
if (n == 0 || n == 1){
return 1;
}
return fib(n - 1) + fib(n - 2);
}
// 没有线程的情况下
// public static void main(String[] args) {
// Scanner scanner = new Scanner(System.in);
// while (true){
// System.out.println("请输入一个数字");
// int n = scanner.nextInt();
// long r = fib(n);
// System.out.printf("fib(%d) = %d\n",n,r);
// }
// }
//引入一个线程来计算,主线程只负责读取用户输入
static class CalcFib extends Thread{
private final int n;
public CalcFib(int n){
this.n = n;
}
@Override
public void run() {
long r = fib(n);
System.out.printf("fib(%d) = %d\n",n,r);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("请输入一个数字:");
int n = scanner.nextInt();
//读取输入后创建一个线程去计算
Thread t = new CalcFib(n);
t.start();
//主线程直接读取下一个输入
}
}
}
线程之间的数据共享问题——线程通信
大部分场景下,几个线程之间是需要协调配合工作(线程之间需要进行数据交换,由于我们的线程都属于同一个进程,所以,共享有OS分配过来的同样的资源,我们优先关注内存资源),一起完成一个总目标的。
JVM下的内存区域划分:pc保存区(PC),栈(虚拟机栈,本地方法栈),堆,方法区,运行时常量池
以上这些内存资源是属于进程的,理论上来讲,其实是这个进程下的所有线程。
举个例子:财产权是以家庭为单位进行分配的。家里的钱是属于家里每一个人的,家庭内部也有分配机制。
堆,方法区,运行时常量池是整个进程(JVM)只有一份,对象和加载的类是大家共有的。
PC(保存PC的值),栈(虚拟机栈,本地方法栈)是每个线程独一份(各自有各自的)。
在我们代码中:
局部变量,保存在栈帧中,也就是保存在栈中,所以是线程私有(A创建的局部变量,只有A在用)
类对象(Class对象 关于类的对象),静态属性,保存在方法区中,所以是线程之间共享的,前提是有访问权限。
对象(对象内部的属性),保存在堆中,所以是线程之间共享的,前提是,线程持有该对象的应用
public class Main {
static class MyThread extends Thread{
@Override
public void run() {
int r = 0; //子线程的run方法的栈帧
for (int i = 0; i < 1000 ; i++) {
r++;
}
System.out.println(Thread.currentThread().getName() + ":" + r);
}
}
public static void main(String[] args) throws InterruptedException {
int r = 0; //主线程的main方法的栈帧中
MyThread t = new MyThread();
t.start();
t.join();
System.out.println(r);
}
}
线程安全(Thread Safe)
线程安全:
1.代码的运行结果应该是100%符合预期(这个标准无法实操,只是为了解释)
2.Java语境下,经常说某个类,对象是线程安全的:
这个类,对象的代码中已经考虑了处理多线程的问题了,如果只是”简单”使用,可以不考虑线程安全的问题。
比如:ArrayList就不是线程安全的,在ArrayList的实现中,完全没考虑过线程安全的任何问题,无法直接使用在多线程环境中(多个线程同时操作同一个ArrayList)
线程不安全现象出现的原因:
1.多个线程之间操作同一块数据了(共享数据)——不仅仅是内存数据
2.至少有一个线程在修改这块共享数据
在多线程的代码中,哪些情况下不需要考虑线程安全问题?
1.几个线程之间互相没有任何数据共享的情况下,天生是线程安全的
2.几个线程之间即使有共享数据,但都是做读操作,没有写操作时,也是天生线程安全的
会出现线程安全的几种情况:
1.原子性
先给大家演示一个栗子:
public class Main {
//定义一个共享属性——静态属性的方式来体现
static int r = 0;
//定义加减的次数
static final int COUNT = 1000000;
//定义两个线程,分别对 r 进行 加法 + 减法 - 操作
static class Add extends Thread{
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r++;
}
}
}
static class Sub extends Thread{
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Add add = new Add();
add.start();
Sub sub = new Sub();
sub.start();
add.join();
sub.join();
//理论上r被加了COUNT次也被减了COUNT次
//所以,结果应该是0
System.out.println(r);
}
}
我们可以看到输出的结果和我们的预期是不一样的,单看代码“没有问题”的情况下,但结果是错误的(无法100%得到预期结果)
为什么会出现这种情况呢?让我们从系统的角度来看看
前置知识:
1.java代码(高级语言)中的一条语句,很可能对应的多条指令r++,实质就是r = r + 1。
2.线程调度是可能发生在任意时刻的,但是不会切割指令
有了以上知识我们就可以来解释上述这种情况了,我们程序员预期r++或者r--是一个原子性的操作(全部完成or全部没完成)但实际执行起来,保证不了原子性,所以就会出错。
那为什么COUNT越大,出错的概率就越大呢?
因为COUNT越大,线程执行需要跨时间片的概率越大,导致中间出错的概率越大。
所以原子性被破坏是线程不安全的最常见的原因!
最常见违反原子性的场景:
1.read-write场景
i++;
array[size] = e; size++;
2.check-update场景
if(a == 10){
a = ...;
}
2.内存可见性问题
前置知识:
CPU中为了提升数据获取速度,一般在CPU中设置缓存(Cache)
我们先来看一个栗子:班级中有一个账本,记录着班费的情况,有一天班长出去采购了,过程中会花一些钱,班费减少了,这个事情记录在班长的脑袋里(工作内存)中,账本(主内存)上还没有减少。这时学习委员按照账本上的班费去采购其他东西,就可能出现花费超支的情况。
内存可见性:一个线程对数据的操作很可能其他线程是无法感知的,甚至某些情况下会被优化成完全看不到的结果。
3.代码重排序问题(从系统角度看)
前置知识:
我们的程序可以看作是一个状态机
程序A
状态1(a = 0,b = 1 )—语句/指令—>状态2 (a = 2 , b = 3) —语句/指令—> 状态3(a = 100 , b = 100)
这样就留给编译器很大的空间做优化
程序B
状态1(a = 0,b = 1 )—语句/指令—>状态3(a = 100 , b = 100)
如果我们站在用户的角度来看,不把时间要素考虑在内的话,程序A和程序B是等价的。
在我们写程序时,往往是经过中间很多环节优化的结果(这些优化主要有:编译器,类加载器,JVM,硬件,但是作为应用开发,我们是无从得知做了哪些优化的),并不保证最终执行的语句和我们写的语句是一模一样的。
那么我们的重排序就是指:执行的指令和书写的指令并不一致
JVM规定了一些重排序的基本原则:happend-before规则
简要的解释:JVM要求,无论怎么优化,对于单线程的视角,结果不应该有变化,但并没有规定多线程环境的情况(并不是不规定,而是不能规定,会导致多线程环境下可能出问题)
举个栗子:
我们作为程序员如何考虑程序安全的问题:
1.尽可能让几个线程之间不做数据共享,各干各的,就不需要考虑线程安全问题了。比如上文提到的归并排序:4个线程虽然处理的是同一个数组,但提前画好范围,各做各的,就没什么问题了。
2.如果非要有共享操作,尽可能不去修改,而是只读操作
举个例子:static final int COUNT = 10;即使多个线程同时使用这个COUNT也无所谓
3.一定会出现线程问题了,问题的原因从系统角度讲:
1.原子性被破坏了
2.由于内存可见性问题,导致某些线程读取到“脏数据”
3.由于代码重排序导致的线程之间关于数据的配合出问题了
所以,接下来需要学习一些机制,目标和JVM进行沟通,避免上述问题发生
一些常见类的线程安全问题:
Array List,LinkedList,PriorityQueue,TreeMap,TreeSet,HashSet,StringBuilder都不是线程安全的
Vector,Stack,Dictionary,StringBuffer都是线程安全的,这几个类都是Java设计失败的产品。以后大家代码中不要出现这些类。
锁(lock)
语法:
1.修饰方法(普通,静态方法)——>同步方法
普通方法(视为对“当前对象”加锁)
synchronized int add(...){...}
静态方法(视为对静态方法所在的类加锁)
synchronized (类.class){……}
2.同步代码块
synchronized(引用){
...
}
所谓的锁,理论上来说,就是一段数据(一段被多个线程之间共享的数据)
为了让大家方便理解,static boolean lock = false;
锁的状态:(锁上(locked));打开(unlocked))false:unlocked true:locked
尝试加锁的内部操作
1.整个尝试加锁的操作已经被JVM保证了原子性
if(lock == false){
//说明整个锁没有被锁上
lock = true;//当前线程把锁锁上
return; //正常向下执行
}
//如果锁已经locked(true)了
Queue<线程> 该锁的阻塞队列 queue = ……;
queue.add(Thread.current Thread());等待者把锁释放
//既然我们无法获取到锁,所以就应该让出CPU
Thread.currentThread().state = 阻塞;
Thread.yield() //理解让出CPU
一个要知道的小现象
语句1;语句2
宏观来看应该是先执行完语句1接着执行语句2
语句1:sync(ref){语句2;…}
但事实上语句1的执行时间和语句2的执行时间相隔很久,甚至极端情况下,语句2再也不会被执行都有可能
释放锁的操作(纯理论):
这个过程由系统保证了原子性
释放锁:lock = false;
从等待锁的阻塞队列中,选择一个线程出来,恢复CPU
Thread t = queue.poll();
t.state = 就绪; //等待被分配CPU
当多个线程:都有加锁操作时并且申请的是同一把锁时,会造成,加锁 代码s(临界区) 解锁
临界区(临界区的代码不一定是同一份代码)的代码会互斥(互相排斥)着进行
public class Main {
//这个对象用来当锁对象
static Object lock= new Object();
static class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
System.out.println("我是张三");
}
}
}
static class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
System.out.println("我是李四");
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread1();
t1.start();
Thread t2 = new MyThread2();
t2.start();
}
}
当我们没有加锁的时候张三和李四会交替出现
public class Main {
//这个对象用来当锁对象
static Object lock= new Object();
static class MyThread1 extends Thread{
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 10000000; i++) {
System.out.println("我是张三");
}
}
}
}
static class MyThread2 extends Thread{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 10000000; i++) {
System.out.println("我是李四");
}
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread1();
t1.start();
Thread t2 = new MyThread2();
t2.start();
}
}
当我们加锁之后这两段代码一定是互斥进行的,一段代码执行完后才会执行另一段代码,但是不能保证谁先抢到。
加锁操作使得互斥有了以下的性质(synchronized 和我们一起配合,我们需要正确的使用synchronized)
1.保证了临界区的原子性
2.在有限程度上保证内存可见性
加锁:加锁成功之前,清空当前线程的工作内存
临界区代码:读某些变量时(主内存上的数据时)保证读到的是最新的,但是临界区期间的数据读写,不做保证(可能读到的数据再次被别的线程更改了,就看不到了。或者是期间有数据同步回主内存)
解锁:保证把工作内存中的数据全部同步回主内存
3.也可以给代码重排序增加一定的约束
举个栗子:
s1;s2;s3;加锁;s4;s5;s6;解锁;s7;s8;s9;
s1;s2;s3;之间先后顺序不做保证
s4;s5;s6;之间先后顺序不做保证
s7;s8;s9;之间先后顺序不做保证
s4;s5;s6;不会被重排序到加锁之前/解锁之后
s1;s2;s3;不会被重排序到加锁之后
s7;s8;s9;不会被重排序到解锁之前
volatile
修饰变量
JVM中线程要读变量,每次从主内存读,写入保证写回主内存。
90%的功能就是保证内存可见性的
保护对象的内存可见性
public class WithOutVolatile {
volatile static boolean quit = false;
static class MyThread extends Thread {
@Override
public void run() {
long r = 0;
while (quit == false){
r++;
}
System.out.println(r);
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(5);
quit = true;
}
}
我们可以试着运行一下上述代码,当没有volatile修饰时,r是不会被打印的因为quit读取的是工作内存的值,所以quit一直是false,循环不会结束,如果我们加入volatile修饰,quit就会读取主内存的值,也就是会读取到主线程修改后的值,所以在五秒后会打印r的值。
特殊情况下可以保证原子性
JVM基本操作长度是32位
所以int ,short,byte,char,float,以及引用的赋值都是原子的(String s = b)
而long,double由于是64位,所以不是原子的。但是如果被volatile修饰后他们就可以成为原子的
保证代码重排序
SomeObject so = new SomeObject(…);
1.根据类计算对象的大小;在内存中(堆)分配内存空间给该对象;memset(0x0);
2.对象的初始化过程
1. 构造代码块
2.属性的初始化赋值
3.构造方法
3.这样我们才可以获得一个正确的对象,并把引用交给so
理论上的顺序 1 -> 2 -> 3
实际中重排序成 1 -> 3 -> 2
在多线程中会出现问题:1 -> 3 -> B:使用对象(错误),这个时候我们就可以用volatile修饰(volatile SomeObject so = new SomeObject(…);)保证顺序是理论上的数据,不会被重排序。
单例模式
通过代码保护一个类,使得类在整个进程(应用)运行过程中有且只有一个对象。
一开始就初始化(饿汉模式——等不及)
public class StarvingMode {
//是线程安全的
private static StarvingMode instance = new StarvingMode();
public static StarvingMode getInstance(){
return instance;
}
private StarvingMode(){
}
}
等用到的时候才初始化(懒汉模式)
单线程
public class LazyModeV1 {
private static LazyModeV1 instance = null;
public static LazyModeV1 getInstance() {
//当第一次调用这个方法是,说明我们应该实例化对象了
if (instance == null) {
instance = new LazyModeV1();//只在第一次的时候执行
}
return instance;
}
private LazyModeV1(){}
}
多线程
public class LazyModeV3 {
private static volatile LazyModeV3 instance = null;
private LazyModeV3() {}
public static LazyModeV3 getInstance() {
if (instance == null) {
synchronized (LazyModeV3.class) {
if (instance == null) {
instance = new LazyModeV3();
}
}
}
return instance;
}
}
阻塞队列
举个栗子:
我们有两个线程一个线程是前台,另一个线程是厨师,他们之间用一个队列来传递顾客点的菜,前台从队列中放入用户点的菜,厨师从队列中取出
厨师从队列中取数据会存在两种现象
1.取到了,直接做菜
2.没有取到(队列为空):厨师不知道该干什么了
可以让厨师sleep一会儿,但是在sleep是可能会有人点菜
所以我们需要将这个队列改造成阻塞队列
在阻塞队列中队列为空的情况下,poll()不会返回
当我们用队列时,队列为空则会返回null
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
String poll = queue.poll();
System.out.println(poll);
}
}
若我们使用阻塞队列时,队列为空则不会返回
public class Main {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
String take = queue.take(); //只要队列中取不到元素就会一直阻塞
System.out.println("永远到达不了");
}
}
BlockingQueue介绍
Blocks | Times out |
put(e) | offer(e,time,unit) |
take() | poll(time,unit) |
put(e)有两种结束方式:
1.队列中有位置了,放入元素(最常见的)
2.当有人让线程结束时,放入失败了,也会结束,以InterruptExcepition形式体现
take(),offer(e,time,unit),poll(time,unit)与put(e)一样
生产者消费者模型
一个(多个线程)只负责向队列中放入元素 ——生产者
一个(多个线程)负责从队列中取元素——消费者
实现阻塞队列
前置知识:
线程和线程之间需要互相等待通知
Object.wait()
Object.notify()
public class Demo1 {
static class MyThread extends Thread{
private Object o;
MyThread(Object o){
this.o = o;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o) {
System.out.println("唤醒主线程");
o.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
MyThread t = new MyThread(o);
t.start();
synchronized (o) {
o.wait();
System.out.println("永远不会到达");
}
}
}
2.要使用wait和notify,必须首先对”对象“进行synchronized加锁
阻塞队列实现代码
public class MyArrayBlockingQueue {
private long[] array;
private int frontIndex;//永远在第一个元素的位置
private int rearIndex;//永远在队列最后一个的下一个位置
private int size;
public MyArrayBlockingQueue(int capacity){
array = new long[capacity];
frontIndex = 0;
rearIndex = 0;
size = 0;
}
public synchronized void put(long e) throws InterruptedException {
//判断队列是否已经满了
while (array.length == size);{
this.wait();
}
//预期:队列一定不是满的
array[rearIndex] = e;
rearIndex ++;
if (rearIndex == array.length){
rereIndex = 0;
}
size++;
notifyAll();
}
public synchronized long take() throws InterruptedException {
while (size == 0) {
wait();
}
long e = array[frontIndex];
frontIndex++;
if (frontIndex == array.length) {
frontIndex = 0;
}
size--;
notifyAll();
return e;
}
}
定时器
代码实现
public class UseTimer {
public static void main(String[] args) {
Timer timer = new Timer(); //闹钟
TimerTask task = new TimerTask() { //闹钟到时间后要做的事情
@Override
public void run() {
System.out.println("闹钟响了");
}
};
// timer.schedule(task,5000);
timer.scheduleAtFixedRate(task,1000,2000);
while (true){
}
}
}
Timer类——任务调度(闹钟)
抽象类TimerTask——任务
我们要做的就是继承这个类重写run方法即可,指定要执行的任务
多长时间之后,执行一次任务
周期性的执行任务
定时器执行任务时是不会占用我们的当前执行流的
线程池
矛盾:创建/销毁线程都是有成本的
有新的任务——创建新线程(无意义成本)——执行任务——销毁线程(无意义成本)
线程池模式会提前创建好很多线程(按需创建),有新任务交给储备的线程去处理。
Java中提供的线程池
Executor(接口) -> ExecutorService(接口) -> ThreadPoolExecutor(…)(线程池版本的实现类)
ThreadPoolExecutor的构造方法
举个栗子:一个公司会有正式工(core)和临时工(tmp),正式工和临时工的名额是有上限的,正式员工是不会被解雇的,但是临时员工一旦空闲了就会被解雇。
corePoolSize:正式员工的名额上限
maximumPoolSize:正式 + 临时的名额上限
keepAliveTime + unit :临时工允许空闲时间的上限
ThreadPoolExecutor(按需创建的方式创建线程)
1.一开始,线程池里一个工作线程都没有
2.随着任务提交
if(正式员工数量 < 正式工的上限){
创建一个新的正式员工(无论是否有员工处于空闲状态)
}
//数量 ==上限
暂时把任务放到队列中(任务不是立即被执行)
如果队列的容量也满了
雇佣临时工来处理(所有员工的总数 < maxSize)
//员工达到上线了&&队列也满了
执行拒绝策略
public class Demo {
public static void main(String[] args) {
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
ThreadFactory tf = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "饭店厨师");
return t;
}
};
ExecutorService service = new ThreadPoolExecutor(
3, // 正式员工 3
9, // 临时员工 9
10, TimeUnit.SECONDS,
queue,
tf,
new ThreadPoolExecutor.AbortPolicy()
);
// 定义任务
Runnable task = new Runnable() {
@Override
public void run() {
try {
TimeUnit.DAYS.sleep(365);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 把任务提交给线程池对象(公司)
Scanner s = new Scanner(System.in);
for (int i = 1; i < 100; i++) {
s.nextLine();
service.execute(task);
System.out.println(i);
}
}
}
关于各种各样的锁
读锁(共享锁) vs 写锁(独占锁)
我们目前使用的锁都是独占锁(只有一个线程能持有锁)
独占锁 | 共享锁 | |
读 + 读 | 互斥 | 不互斥 |
读 + 写 | 互斥 | 互斥 |
写 + 写 | 互斥 | 互斥 |
当业务中,读的次数远远大于写的次数,共享锁 优于 独占锁
重入锁 vs 不可重入锁
是否允许持有锁的线程成功请求到同一把锁
举个栗子:t1 线程
lock1.lock(); t1成功锁上了lock1
…
lock1.lock() 注意:此时lock1处于已经锁上的状态&&请求锁的还是t1线程
若请求成功则为可重入锁,否则为不可重入锁
公平锁 vs 不公平锁
这里的公平就是指:是不是按照请求锁的顺序获得到锁。
juc下的ReentranLock可以通过传入fair = true | false来控制是否是公平的