目录
(2).多个线程修改一个变量(这个在count++说,11 )
1>:介绍:和闹钟类似,达到⼀个设定的时间之后, 就执⾏某个指定好的代码.
一.处理器(cpu)
1>:主频(基频)
代表每秒能进行的基础操作次数:换而言之,主频越高,处理速度越快
2>:睿频:
衡量cpu工作/运行速度的快慢
当系统需要更多性能,就会使用睿频从而临时提高运行频率,这个是动态的
3>:单元:
cpu内部有许多"计算单元"进行数据处理,每个单元非常小,所以一个cpu上有海量的"计算单元";
即:单元越多,cpu数据处理的速度越快
4>:核
随着一个cpu的更新迭代到瓶颈了,为了提高运行速率,就会在电脑上多装几个cpu核心
而为了更加优化运行速度,Intell 研发的超线程技术可以让一个核心当2个用
这里: 内核 = 处理核心 逻辑核心 = 逻辑处理器
我这个只有8个cpu核心,逻辑上来说有16个,但有的人逻辑核心可能是 14个
因为内核分为:大核心与小核心,大核心等于2个逻辑核心,小核心等于1个逻辑核心,具体可以看自己电脑
不过,对于程序员来说主要关注 睿频 和 逻辑核心数
5>:缓存:
cpu上安排的一组存储数据的设备
特点: 内存小,访问快,一般就是三级缓存
6>:指令:
(1).介绍
由0 1组成一串计算机能识别的二进制内容;
是指导 CPU 进⾏⼯作的命令,也是cpu上执行任务的基本单位,
主要由操作码 + 被操作数组成。
(2).内存指针:
指令是保存在内存中的,而内存指针可以指出内存哪部分放数据 哪部分放指令
7>:小知识:
能设计cpu的不止一个 厂商,如:x86/x86_64 这是一套架构,intel AMD的cpu都是这套架构;ARM这是另外的架构,苹果 高通 联发科等的cpu都是这;但底层的指令都是一样
8>:指令表:
指令遵循的一种执行规则所构成的表格
这里只是一部分
9>:寄存器:
cpu内部的存储 数据/指令等 的硬件设施
特点:内存小,主要用于进行临时数据的计算与保存
一般每个核心有十几---几十不等的寄存器数量(具体看架构),每个差不多8bit,总空间不一定到1kb
10>:cpu的基本工作流程
(1).指令是由一串 0 1组成二进制内容,保存在内存中
(2).流程:
<1>.读指令:从内存中把指令读取到寄存器中
<2>.解析指令:查询指令表,理解指令含义,区分操作码与操作数
<3>.执行指令:默认按照顺序进行,但遇到 跳转语句 函数调用或函数返回语句等则会影响顺序
这里演示一个例子:
上述演示了3+14的过程,很复杂
但一个睿频(本电脑)有3.61GHz,1GHz = 10 亿Hz,假设一个步骤消耗1个"HZ",能运行36亿个指令呢~
这里演示的是一个执行完接着下一个,但真实情况是流水式运行
二.操作系统
操作系统由两个基本功能:
1) 管理各种硬件设备,防⽌硬件被失控的应⽤程序滥⽤;2) 向应⽤程序提供稳定的运行环境,防止你打印个helloword 把系统整崩溃了,哈哈~
三.进程
1>:介绍:
每个应⽤程序运⾏于现代操作系统之上时,操作系统会提供⼀种抽象的表示这个应用程序运行--进程;简单点就是,操作系统会把exe文件加载到内存中并创建一个集合来表示这个应用,而集合内部是创造出来的进程负责来执行内部操作
一个exe文件对应多个进程,不够有些进程在逻辑束中限制多开(比如游戏不能多开,浏览器课多开等)
2>:进程的特点
可以看到浏览器的这些进程都有资源消耗,而且不管点击什么软件,都是由一个个进程运行起来的
特点:
1.进程的创建要消耗一定的硬件资源
2.进程是操作系统分配资源的基本单位
3>:PCB
(1).描述:
通过一个结构变量,描述了一个进程的各种属性,把这个结构变量称为"PCB"
这个结构变量可以类比java/C++的类与C结构体,里面描述了各种属性,把这这个类或结构体称为"PCB"
(2).组织:
使用一定的数据结构,把N个PCB连接起来(一般是链表)
相当于把他们的类连接在一起
4>:PID
(1).介绍:
一个数字编号,用于标识操作系统的每个进程
(2).特点
唯一性:
每个进程都有一个唯一的PID,结束后可能会复用,能确保同一时刻不会有两个PID相同的进程
这个就好比手机号,一个手机号有且仅有一个人使用,虽然后面可能更换手机号导致别人使用这个手机号,但同一时刻不会有第二个人使用
标识与管理: 通过PID来操作与管理进程
(重):记录在PCB中:
PID是进程控制块(PCB)里的核心信息之一,是进程的"身份证"
5>.文件描述表
一个进程对应一个PCB,一个PCB中会有一个文件描述表,用于记录当前进程打开文件的一个标识(可以理解为地址),后续可以通过这个标识找到文件进行读写或关闭操作
6>:进程调度的两种方式:
并行: 多个核心,分别执行不同的进程的指令
并发:一个CPU,通过"分时复用"的方式实现同时执行
分时复用,微观上来看 这一时刻进行事情A,下一时刻进行事情B;宏观来看,同时进行事情A B
比如微信发消息,这一刻给A发消息,下一时刻给B发消息;
微观上,这两件事是不同时间进行的;宏观上,我同时完成了这两件任务
7>:进程状态:
就绪:时刻准备好了,随时可以被CPU调度
阻塞:线程无法参与CPU调度
比如,今天女盆友说和她吃饭,时间她定;
就绪就是我东西都收拾好了,整装待发就等她说时间了
阻塞就是突然妈妈说今天和我看外婆去,于是我没办法和她吃饭了,不得已放她鸽子
8>:进程优先级
进程之间按的执行没有顺序,但是有优先级的,受进程的状态.调度策略等影响(了解即可)
就比如:我打王者,女朋友发消息了,此时王者的优先级高,消息放后台一会看,总不可能微信的优先级高,消息一下子把我王者弄闪退了,
9>:进程上下文:
进程调度到CPU上执行,执行一半后离开CPU过了一段时间在会在CPU上,还会从之前离开的位置继续线下执行
解释:CPU的寄存器用于保存当前程序的中间状态,当一个进程结束时,会将寄存器的中间状态取出放入内存(PCB)中(类似存档操作),等待进程重新启动时,内存(PCB)会把当前的数值重新写入CPU的寄存器中(类似读档操作)
10>:进程的记账信息:
进程是操作系统分配资源的基本单位,线程的资源分配是不一样的,若某个进程的资源分配太少了,那么操作系统就会统计进程运行多长时间,从而通过记账信息灵活调整(多分点资源)
11>.总结
其实这一切内部还是很复杂的,不过这是C++那边考虑的,咱们只需要简单了解即可
1).操作系统
a.对下,管理各种硬件设置
b.对上,提供各种软件稳定的运行环境
2).进程(运行起来的程序)
运行存入硬盘中的exe文件会得到进程
3).进程管理
a.描述:使用PCB结构体,把今后蹭的各种属性表示
b.组织:通过链表把PCB连接起来
4).pcb的核心属性
a.PID b.内存指针 c.文件描述表 d.状态 e.优先级 f.上下文 g.记账信息
这些都要有了解
四.线程
介绍与特点
a.一个cpu核有多个进程,每个进程有多个线程(数量>=1)
b.这些线程之间共享一个进程的所有资源
c.操作系统分配好进程的资源,接着里面的线程开始执行任务,所以线程是操作系统调度执行的基本单位
d.进程调度本质上是线程调度,有个特例:一个进程里只有一个线程在执行任务也叫进程调度
e.线程又称轻量级进程,线程的创建比进程创建的资源消耗少
f.多线程编译中,多个线程可能会造成冲突,导致bug,会有线程安全问题
比如:某个线程运行出现异常,若成功处理异常,则继续运行;否则,都完蛋,直接崩溃
五.多线程
1.介绍:多线程是线程的一种相关操作
小知识:多线程的操作依赖操作系统提供的对应API,但原生API是C实现的且不同操作系统的API不一样,为此Java标准库对原生API进行了统一封装,到哪里都能用API------->跨平台性
优势:在一定场合下增加运行速度
2.感受线程
(1).代码演示
// 创建线程要继承 Thread(线程) 类
class MyThread extends Thread{
// 重写 run方法
@Override
public void run(){
while(true){
System.out.println("hello");
try {
//因为线程执行很快,加个 sleep 阻塞一下
//sleep 是 Thread 的静态方法
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class threa_1 {
public static void main(String[] args) {
// 1. 创建 Thread 对象
// 这里采用向上转型方式
Thread t = new MyThread();
// 开启线程
t.start();
while(true){
System.out.println("word");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
(2).运行截图
(3).分析
1>:代码分析
----问题1:
调用start本质是调用重写的run方法 ,那可以写直接run吗?
-----问题2:
结果是hello与word交替出现,难道线程是主线程运行一下 新线程运行一下吗?
答:因为线程是操作系统调度的基本单位,而操作系统的调度是随机性的;运行sleep时两个线程都被阻塞,结束后进入就绪状态,此时调用哪个线程不清楚,说以出现了这个结果
----问题3:
jconsole观察:Java中提供了对应的可执行文件来观察线程
3.线程创建
(1).继承 Thread 类
上面所示代码用的就是这个
(2).实现 Runnable 接口
(3).匿名内部类创建 Thread 对象
(4).匿名内部类创建 Runnable 对象
(5)将匿名内部类写成lambda表达式
4.构造方法与属性
1>:构造方法:
代码:
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
}
}
}
public class MyThread {
public static void main(String[] args) {
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字-->张三");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字-->李四");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
这里可以在 jconsole 观察查到:
2>:属性
代码演示就用匿名内部类演示
(1).ID:是线程的唯一标识,确保进程不会重复
这里 Thread.currentThread() 方法会获得当前线程的对象,代码中是 Thread 对象
// 获得当前 main 对象
Thread mainThread = Thread.currentThread();
System.out.println("ID>:"+mainThread.getId());
//结果:
// ID>:1
(2).名称
Thread mainThread = Thread.currentThread();
System.out.println("名称>:"+mainThread.getName());
// 名称>:main
(3).状态
这里只了解方法,具体的状态对应什么,下面有介绍
Thread mainThread = Thread.currentThread();
// 这里状态下面会详细介绍
System.out.println("状态>:"+mainThread.getState());
// 状态>:RUNNABLE
(4).优先级
Thread mainThread = Thread.currentThread();
// 这里状态下面会详细介绍
System.out.println("优先级>:"+mainThread.getPriority());
// 优先级>:5
优先级对应1~10,1为最低,10为最高,优先级高的可以尽快被操作系统调用,默认情况下是5,
优先级决定了在CPU资源竞争中的基本权重
(5).是否为后台进程
Thread mainThread = Thread.currentThread();
// 这里状态下面会详细介绍
System.out.println("是否为后台进程>:"+mainThread.isDaemon());
// 是否为后台进程>:false
详解: 后台线程会等前台线程结束才结束,我们创建的线程都是前台线程
比如:
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
System.out.println("hello");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.start();
}
此时两个都是前台线程,程序运行必须要等前台线程结束,显然结束不了
回到之前没有修改的代码,运行并打开jconsole来看
(6).是否存活
当一个线程处于未开始与执行完时出处于"死亡状态",运行时则是存活状态
(7).是否被中断
这里只了解方法使用,具体情况在下面有介绍
public static void main(String[] args) {
Thread mainThraed =Thread.currentThread();
System.out.println("是否被中断>:"+mainThraed.isInterrupted());
}
// 是否被中断>:false
5.启动线程
start :开启一个线程,并调用 Thread 里的 start0 方法,调用一些API创建线程
run :就是一个"任务",当线程创建后就开始跑 run 里面的任务
6.中断线程(interrupt)
java不提供强行中断
想要中断只能从"入口处解决",也就是Start之后run方法处中断
public class DEMO7 {
// 设置标志位
public static boolean running = true;
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(running){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t 线程结束");
}
});
t.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("请输入任意整数(0代表 t 线程结束)>:");
Scanner scanner = new Scanner(System.in);
if(scanner.nextInt() == 0){
running = false;
System.out.println("t2 线程结束");
}
}
});
t2.start();
}
}
当我输入0,线程就会结束;若对 sleep() 的参数进行修改
Thread.sleep(1000_00);
此时发现,t 线程并没有结束,因为sleep() 让这个线程阻塞100秒,等100秒后才进行循环判断并结束,
这和我们预期的不一样,为此:java引入了两个方法
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// 当前这个线程对象 判断 此时进行阻断
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000_00);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t 线程结束");
}
});
t.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("请输入任意整数(0代表 t 线程结束)>:");
Scanner scanner = new Scanner(System.in);
if(scanner.nextInt() == 0){
t.interrupt();
System.out.println("t2 线程结束");
}
}
});
t2.start();
}
唤醒sleep后抛出异常并用trycatch处理异常,最终 t 线程以异常的形式结束了
但是sleep捕捉异常后在catch又抛出异常,这显然不好,我们可以将此处改为其他的语句
比如:打印调用栈
try {
Thread.sleep(1000_00);
} catch (InterruptedException e) {
e.printStackTrace();
}
此时运行结果
发现程序不但没退出,还打印了一下hello,虽然说打印了调用栈
明明 t.interrupt() 把标志位的值修改为true,接着到循环判断,while(false)程序退出才对呀,怎么回事?
发现sleep太坑了,会修改标记位的值
所以就要防止再判断了,即对catch捕捉的异常进行处理
try {
Thread.sleep(1000_00);
} catch (InterruptedException e) {
// 有三种方法
// 方法一.直接抛异常
// throw new RuntimeException(e);
// 方法二.直接 break(此处演示)
break;
// 方法三.进行一些其他的操作再break
// 如:进行一些资源关闭在 break
}
再尝试运行
这就是阻断的方法与运行处理,不过我更喜欢叫他中断
7.线程等待(join)
1.介绍
对象A.join
可让调用该方法的线程B陷入阻塞状态,等待执行完A后,线程B进入就绪状态开始执行
2.方法
一般咱们用第二个就行,虽然第三个更精准,提供了 ns纳米级别 的参数类型,但线程的调度是ms级别,ns没太大用
3.引入与使用
对于多线程来说,我们无法判断执行顺序,但可以确定结束顺序
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程 thread 执行");
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程 thread1 执行");
}
});
thread1.start();
thread.start();
}
比如个代码,虽然说线程的调度是随机的,但发现好几次运行结果都是有顺序的?
若把start的顺序换一下,结果就成了 thread 在前 thread1 在后了
保证原来的代码顺序不变,想让结果变为上述的thread在前 thread1在后,可以加入join,可以将thread1这样写
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread.join();
System.out.println("thread1 等待 thread 结束再执行");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程 thread1 执行");
}
});
但这个等待就是死等,若是线程thread是这样呢?
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
}
}
});
thread1线程陷入阻塞,一直在等待thread线程结束后自己的线程才进入就绪状态,但thread始终结束不了 (ps:这里就演示结果截图了) ,可以使用第二个方法
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// thred1陷入阻塞,等待thread执行完
// 若提前执行完,thread1 正常进入就绪状态
// 若 3s 后 thread 没执行完,thread1进入就绪状态
thread.join(3000);
System.out.println("thread1 等待 thread 结束再执行");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程 thread1 执行");
}
});
thread1.start();
thread.start();
}
8.sleep
这个就不具体演示了,前面代码都用到
重点:sleep(0)
正常情况下每个线程运行都会消耗一定的时间(时间片),而sleep(0)会让当前线程放弃时间片(即从与运行状态变为就绪状态),让其他优先级大于等于该线程的运行,若没有,则从就绪状态变为运行状态,继续运行
9.进程的相关基础属性:
线程的基础属性:构造方法 id 名称 优先级 daemon属性(前台后台线程)等,也有 sleep wait join 这些专门服务于线程的方法
10.线程的6种状态
总体分为: 三(三种运行状态)+三(三种阻塞状态)
(1).new
官方解释:安排了⼯作, 还未开始⾏动,即创造了线程但还没start前的状态
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
}
}
});
System.out.println("当前状态>:"+thread.getState());
}
// 当前状态>:NEW
(2).runnable
官方解释:可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.
即线程执行中或未执行但状态为就绪状态
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
}
}
});
thread.start();
System.out.println("当前状态>:"+thread.getState());
}
// 当前状态>:RUNNABLE
(3).terminated
官方解释:工作完成了,即线程运行完毕已经被操作系统销毁了,但线程对象还在
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
});
thread.start();
thread.join();
System.out.println("当前状态>:"+thread.getState());
}
// 当前状态>:TERMINATED
(4). waiting
死等或者进入阻塞状态
死等:
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
System.out.println("当前main线程状态>:"+mainThread.getState());
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.start();
thread.join();
}
进入阻塞状态:
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(mainThread.getState());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
thread.join();
}
// WAITING
(5).timed_waiting
带有超时时间的等待
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
System.out.println("当前main线程状态>:"+mainThread.getState());
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.start();
thread.join(5000);
}
(6).block
锁等待,即:多个线程因为竞争同一把锁,而造成的等待
这里 第六种 Block下面死锁有介绍
11.线程安全
(1).线程安全
概念:如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
(2).线程不安全
引入:
比如:两个线程修改一个变量5000次
public class DEMO7 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i < 5000;i++){
count++;
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i < 5000;i++){
count++;
}
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println("count = "+count);
}
}
可以发现,预期的结果是10000,但连续运行多次发现每一次结果都不一样
为什么?
这里随机列举几个可能,发现若是第一和第二种交替出现那就是正确答案 ,可若是其他的就不敢保证,因为还会出现这些情况
发现多线程真的危险,此时就要进行加锁操作了(下面进行详细介绍)
小结:
这样与结果不匹配的就是线程不安全,那么什么会引起线程不安全呢:
(1).线程是随机调度的
(2).多个线程修改一个变量
(3).操作不是原子的
(4).内存可见性
(5).指令重排序
下面一一介绍
12.锁synchronized
由线程上述加加5000次引出锁的概念,这里进行详细介绍:
1>:概念:
可以让执行的代码不会被其他线程抢占或打断,保证原子性
2>:锁的演示
(1).引入锁的解法:
解法一: 给count上锁
public class DEMO7 {
public static int count = 0;
public static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i < 5000;i++){
synchronized (locker){
count++;
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i < 5000;i++){
synchronized (locker){
count++;
}
}
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println("count = "+count);
}
}
// count = 10000
分析:
解法二:给循环上锁
public class DEMO7 {
public static int count = 0;
public static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
for(int i = 0;i < 5000;i++){
count++;
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
for(int i = 0;i < 5000;i++){
count++;
}
}
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println("count = "+count);
}
}
// count = 10000
分析:
(2).错误示范
两个锁的参数不一样
有锁和没锁
(2).小总结
a.对同一对象加锁才会有锁竞争,会导致其他的线程进入阻塞状态,最终达到线程安全(对这个代码而言)
b.若不是同一对象加锁则会造成线程不安全情况
(重)b.我们只考虑对同一个对象加锁会出现锁竞争,不考虑对什么对象加锁,String Scanner等都行
(2)与其他语言区别
通过上面介绍知道:
一旦线程上锁(这里记为A),对于同一个对象来说,其他线程就会进入阻塞状态,若A在unlock(解锁)前用了return或者throw导致unlock没有执行,那么其他线程将会一直阻塞,
这种操作很危险
而java的锁与其他语言(C/C++,C#,py)区别:
(1).其他语言:需要调用类似 lock 与 unlock 的方法手动进行上锁与解锁,且必须运行到unlock才解锁
(2)java:在synchronized例的函数体内{ }进行上锁与解锁,且不管return 或 throw 只要出了函数体自动解锁,释放线程
(3).总结:
对于锁来说,加锁会让线程安全,但也会降低效率,,因为必须等A线程解锁后B才开始运行,所以不要盲目去加 ,什么时候需要了,再去加
(3).静态/实例方法加锁:
实例方法加锁: 相当于给this加锁
静态方法加锁:相当于给 类对象(动态中反射有讲到) 加锁
小总结:对于synchronized 来说,不关心对象是谁(无所谓哪个对象,eg:Object String 类对象等),只需关心是否针对同一个对象上锁 !!!!!!
3>:锁在集合类应用:
标准库的集合类中,目前学过的 ArrayList LinkedList 优先级队列 HahMap HashSet等等都是线程不安全的,而Vector Hahtable 等,是线程安全的,但这两个已经快被淘汰了
4>:连续加锁:
(1)连续上锁
public class DEMO7 {
public static int count = 0;
public static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 5000;i++)
synchronized (locker){
synchronized (locker){
synchronized (locker){
count++;
}
}
}
}
});
thread.start();
thread.join();
System.out.println("count = "+count);
}
}
// count = 5000
按照我们所学的,第一次对locker进行加锁,接着在又对 locker 加锁 ,会造成锁阻塞,但发现结果去不是这样
解释:
若第1次对一个对象上锁,第2次尝试上锁的时候,synchronized会检查两次上锁是否同一个对象,若是的话,那就直接把locker的加锁数量加一,否则正常上群锁
(2)多重上锁的"上锁"与"解锁"
5>:死锁
死锁:两个及以上的线程都想拿到对方的锁对象,但因为上锁了所以都在干等对方解锁
(1).一个线程一把锁,连续加锁(也称可重入锁)
这个是典型的易错题,连续加锁不会导致死锁
死锁是两个及以上线程出现的情况
而连续加锁会跳过多余的加锁,针对同一个对象来说
(2).两个线程两把锁
public static void main(String[] args) throws InterruptedException {
Thread_5 A = new Thread_5();
Thread_5 B = new Thread_5();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A){
System.out.println("线程A成功上锁");
synchronized (B){
System.out.println("线程A中线程B功上锁");
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B){
System.out.println("线程B成功上锁");
synchronized (A){
System.out.println("线程B中线程A成功上锁");
}
}
}
});
thread.start();
thread1.start();
}
运行结果的分析:
此时到jconsole观察:
(3).n个线程n把锁)(哲学家就餐问题)
13.死锁的解决:
死锁的四个必要条件:
(1).锁是互斥的
同一个锁在同一时间时间只能给一个人,其他线程必须等锁释放了才能继续开始运行
(2).锁不可被抢占
在一个线程上锁后,在解锁前其他线程不可能抢走这把锁
区别:
互斥性是"同一时间只给一个人",
不可被抢占是"给了一个人后,别人不能硬抢,必须等他主动还回来"
(3).请求和保持
A 在说获取到锁1后,在不释放的情况下还想获得锁2,容易造成死锁(eg:上面讲的两个线程的死锁问题)
(4).循环等待或环路等待
指多个线程(大于等于2)之间等待互相形成锁等待(eg:还是两个线程问题,A等B释放,B等A释放)
最常用的方法就是 编号标记法:给线程标记序号,按序号进行
如哲学家就餐问题:
(5).解决
这四个就是构成死锁的必要条件,想要解决死锁,就必须破坏这几个条件(1个或多个)
破坏请求与保持:就是防止出现嵌套锁
public static void main(String[] args) throws InterruptedException {
Thread_5 A = new Thread_5();
Thread_5 B = new Thread_5();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A){
System.out.println("线程A成功上锁");
}
synchronized (B){
System.out.println("线程A中线程B功上锁");
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B){
System.out.println("线程B成功上锁");
}
synchronized (A){
System.out.println("线程B中线程A成功上锁");
}
}
});
thread.start();
thread1.start();
}
破坏循环等待 :采用编号标记法,给线程标记序号,按序号进行
public static void main(String[] args) throws InterruptedException {
Thread_5 A = new Thread_5();
Thread_5 B = new Thread_5();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A){
System.out.println("线程A成功上锁");
synchronized (B){
System.out.println("线程A中线程B功上锁");
}
}
}
});
//规定,先 A 在 B
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A){
System.out.println("线程B成功上锁");
synchronized (B){
System.out.println("线程B中线程A成功上锁");
}
}
}
});
thread.start();
thread1.start();
}
14.多线程的风险
回顾 11 中讲到,多线程会有五个风险:
(1).线程是随机调度的(这个在线程那边说)
(2).多个线程修改一个变量(这个在count++说,11 )
(3).操作不是原子
(4).(重点)内存可见性(volatile)
基本流程:简单介绍--->先提出简单解决方法--->代码实现--->分析--->介绍java内存模型
介绍:对于程序员来说,大佬少菜鸡多,为了照顾我们,大佬在编译器加了个"优化机制"来优化我们的代码,当代码提交后在原有的逻辑不变的情况下,编译器会自动修改代码,让其运行变得更高效
有利有弊:虽然优化了代码,但也引入了新的问题,内存可见性
简单解决方法:尝试在一个线程内部通过标志位不停运行线程,另一个则在内部修改标志位
代码:
public class JDemo {
public static int flag = 0;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(flag == 0){
// 这里不进行任何操作
}
System.out.println("线程 thread 结束");
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数,非0代表线程结束>:");
flag = scanner.nextInt();
System.out.println("线程 thread1 结束");
}
});
thread.start();
thread1.start();
}
}
结果截图:
分析:
所以 价格 volatile关键字就可以了:
public static volatile int flag = 0;
( 易混淆 )与synchronized区别:volatile 和 synchronized 有着本质的区别.synchronized 能够保证原⼦性volatile 保证的是内存可⻅性
java的内存模型(java memory model):
在java进程中,每一个线程都会有一个工作内存(work momory),而这些线程的工作内存又会共享一份主内存(main memory)
那么当一个线程针对某个数据进行修改操作时:
修改:先把数据从主内存拷贝到工作内存,对工作内存修改后会重新写到主内存
读取:先把数据从主内存拷贝到工作内存,再对工作内存进行读取
而内存可见性体现:
1.while中的 flag 值 一直从工作内存中读取
2.因为多次读取的值都是一样且判断不了另一个线程是何时修改从而把该线程工作内存的读优化了
3.在对flag的值操作时,又因为工作内存是主内存的副本,导致在这个线程修改主内存影响不另一个工作内存
小知识:
memory 指的是 能存储数据的单元
main memory才指内存(又称主内存)
工作内存就是 cpu寄存器 + 缓存.
有些cpu没有 寄存器,有些没有缓存,有的两个都没,为了方便跨平台,统一用工作内存代指
小问题:其实在while内部加个Thread.sleep()也可以哦~~但如果函数体内代码太复杂可能会导致优化失败
(5).指令重排序
介绍:
编译器会自动优化代码,导致线程启动时对指令的的运行顺序做出改变,从而引发指令重排序问题
1>:回顾单例模式:
饿汉模式:
class Single{
private static Single single = new Single();
private Single(){}
public static Single getSingle() {
return single;
}
}
懒汉模式:
class Single{
private static Single single = null;
private Single(){}
public static Single getSingle() {
if(single == null){
single = new Single();
}
return single;
}
}
哪个线程安全?
由此看来,懒汉模式存在线程风险,此时要保证这个操作是原子的,就需要加上锁,防止被别人抢
2>:引入锁
错误引入:
这里与刚才一样,还是会导致线程不安全
正确引入:
private static Object locker = new Object();
public static Single getSingle() {
synchronized (locker){
if(single == null){
single = new Single();
}
}
return single;
}
这样第一次创建的对象剩下线程直接返回对象,就可以解决上述问题,但又会引入新问题,锁不是越多越好,第二次 至第 n 次,每次调用都要解锁上锁,太占资源了
第一次优化:
private static Object locker = new Object();
public static Single getSingle() {
// 方便第二次至第 n 次获取
// 即:判断是否加锁
if(single == null){
synchronized (locker){
// 判断第一次是否为空,并赋值
// 即:用于第一次创建对象
if(single == null){
single = new Single();
}
}
}
return single;
}
这样当 第二次 至第 n 次读取就不会有很大开销,讲了这么久,回顾主题,此时会发生指令重排序问题
3>:指令重排序的体现及优化:
此时就需要加 volatile 让与 single 相关的操作编译器别优化
第二次优化:
private volatile static Single single = null;
4>:小结:
目前对于指令重排序对线程安全的影响,只是对网上常见说法的总结,并不可靠
目前指令重排序的案例只有: 单例模式
有可能对于现在的 jdk来说,不加 volatile 也没事;但大家最好加上,不怕一万就怕万一
5>:与datebase 结合
public class JDBC {
private static volatile DataSource dataSource ;
public static DataSource getInstance() throws SQLException {
if(dataSource == null){
synchronized (JDBC.class){
if(dataSource == null){
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setURL("jdbc:java://127.0.0.1:3306/java_002?characterEcoding=utf8&usrSSL=false");
((MysqlDataSource)dataSource).setUser("root");
((MysqlDataSource)dataSource).setPassword("123456");
}
}
}
return dataSource;
}
}
这种代码是否有错误?
当然,当 dataScoure 赋值后,若被 线程2抢占判断 dataScoure != null接着返回dataScoure,那么下一步与数据库服务器建立连接会找不到服务器,因为赋值下面的一堆环境配置没有执行
正确书写:
public static DataSource getInstance() throws SQLException {
if(dataSource == null){
synchronized (JDBC.class){
if(dataSource == null){
DataSource dataSource1 = new MysqlDataSource();
((MysqlDataSource)dataSource1).setURL("jdbc:java://127.0.0.1:3306/java_002?characterEcoding=utf8&usrSSL=false");
((MysqlDataSource)dataSource1).setUser("root");
((MysqlDataSource)dataSource1).setPassword("123456");
dataSource = dataSource1;
}
}
}
return dataSource;
}
15.wait 与 untify
(1).前言:
线程饿死问题:
指某些线程长期得不到CPU的调用,一直处于等待状态
比如:有多个线程 A B C,在他们内部都对同一块锁对象locker进行上锁操作
一开始假设A对locker上锁,结果里面的代码是死循环,导致锁对象locker得不到释放,线程 B C一直处于等待状态,一直得不到CPU调用,容易"饿死"
(2).介绍:
wait:
1. 释放当前锁并进入阻塞状态,等待被唤醒
2. 被唤醒后获得"重新竞争锁"的机会,尝试获取锁:
a. 若获取成功就会进行后续相关代码
b. 若获取失败,就会进入阻塞状态,等待别的线程释放锁
注: wait开始进入阻塞状态,相当于 进入死等状态,等待被唤醒
后面的被唤醒后由于锁竞争进入的阻塞状态,进入死等状态,等待别的锁解锁
两者等待的不是一个东西 ! ! ! !
untify:唤醒锁
untifyAll:唤醒所有锁
(3).使用
1):wait依赖锁,而untify不依赖
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
// 不进行任何处理
}
}
});
thread.wait();
// untify不依赖锁,但java要求必须要写在锁中
//thread.untify();
}
若运行会报错:非法的监视器异常
小知识:
JVM内部实现synchronized时内部使用了如 Monitors 作为一些变量名/方法名,被称为"监视器锁",起到监视作用
2):引入锁
a. synchronized 内部的wait操作必须和锁对象一样
b.多线程使用
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
try {
System.out.println("thread 线程使用 wait 之前");
locker.wait();
System.out.println("thread 线程使用 wait 之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
System.out.println("thread1 线程使用 notify 之前");
System.out.println("输入任意数字,唤醒 wait");
Scanner scanner = new Scanner(System.in);
scanner.nextInt();
locker.notify();
System.out.println("thread1 线程使用 notify 之后");
}
}
});
thread.start();
thread1.start();
}
分析:
c.notify特点
多个线程对同一对象上锁, 若都进入wait之后 ,被notify唤醒的wait是随机的
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
try {
System.out.println("t1 线程使用 wait 之前");
locker.wait();
System.out.println("t1 线程使用 wait 之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
try {
System.out.println("t2 线程使用 wait 之前");
locker.wait();
System.out.println("t2 线程使用 wait 之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
try {
System.out.println("t3 线程使用 wait 之前");
locker.wait();
System.out.println("t3 线程使用 wait 之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t1.start();
t2.start();
t3.start();
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
System.out.println("t4 线程使用 notify 之前");
System.out.println("输入任意数字,随机唤醒 wait");
Scanner scanner = new Scanner(System.in);
scanner.nextInt();
locker.notify();
System.out.println("t4 线程使用 notify 之后");
}
}
});
t4.start();
}
可以看到 后续的线程还在等待唤醒中
注意:有可能运行时会出现连续多次都是 t1 线程被唤醒,但当你执行次数多了之后,统计发现唤醒确实是随机的
notifyAll:全部唤醒,至于唤醒后谁先拿到 锁 并运行就各凭本事了
// 修改 线程t4
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker){
System.out.println("t4 线程使用 notify 之前");
System.out.println("输入任意数字,全部唤醒 wait");
Scanner scanner = new Scanner(System.in);
scanner.nextInt();
locker.notifyAll();
System.out.println("t4 线程使用 notify 之后");
}
}
});
16.阻塞队列 (BlockingQueue)
1>.特点:
1.先进先出
2.通常情况下是线程安全的
3.具有阻塞功能
a.队列为空,尝试拿值会阻塞
b.队列满时,尝试放值会阻塞
2.生产者消费模型
(1).降低资源竞争
(2).解耦合
(3).削峰填谷
峰就是请求的高峰
对于双十一或者新年回家等,大量的购买导致资源消耗峰值上升
(4).缺点
上面的都是优点,那么缺点呢?
缺点:对某些请求的处理时间受到影响
比如:
a. 上面的图,虽然说峰值很高,但请求都是一起处理的,会一下子返回结果
b. 下面的,某些请求可能在正在排队(队列已满情况),终于轮到请求执行了,结果服务器访问超时
3.队列代码演示
构造方法:
// 基于数组实现
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(100);
// 基于优先级队列实现
BlockingQueue<String> blockingQueue1 = new PriorityBlockingQueue<>();
// 基于链表实现
BlockingQueue<String> blockingQueue2 = new LinkedBlockingDeque<>();
// 双端阻塞队列只能基于链表实现
BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>();
阻塞队列也是队列,队列的方法都能用
特别的就是 put和take与offer和poll功能一样,只是多了阻塞属性和异常处理
public class Demo3 {
public static void main(String[] args) {
BlockingQueue<Long> blockingQueue = new ArrayBlockingQueue<>(10);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
long n = 0;
while(true){
try {
blockingQueue.put(n);
Thread.sleep(1000);
System.out.println("输入>:"+ n);
n++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
long x = blockingQueue.take();
System.out.println("输出>:"+ x);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.start();
thread1.start();
}
}
5>:自己实现
public class MyBlockingQueue {
public int[] element;
public int head;
public int tail;
public int size;
public MyBlockingQueue(int capcaticy){
element = new int[capcaticy];
}
public void put(int value) throws InterruptedException {
synchronized (this){
while(size == element.length){
this.wait();
}
element[tail] = value;
size++;
tail = (tail + 1) % element.length;
this.notify();
}
}
public int take() throws InterruptedException {
synchronized (this){
while(size == 0){
this.wait();
}
int x = element[head];
head = (head + 1 ) % element.length;
this.notify();
return x;
}
}
public static void main(String[] args) {
MyBlockingQueue myBlockingQueue = new MyBlockingQueue(10);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int n = 0;
while(true){
try {
myBlockingQueue.put(n);
Thread.sleep(1000);
System.out.println("输入>:"+ n);
n++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
int x = myBlockingQueue.take();
System.out.println("输出>:"+ x);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.start();;
thread1.start();
}
}
17.池
1>:概念
提前让操作系统调用API创建好线程,并把这些线程放在一个集合类中,需要的时候直接从中取
为什么要用"池"?池能干嘛?
2>: 类的介绍
打开文档,在java.util.concurrent 下找 ThreadPollExecotur 类(官方提供的现成的线程池)
(1):提交方法
submit(Runnable task)方法:
这个方法就是把 参数test 放入线程池中去执行.类型为Runnable
同时还会返回Future对象,通过这个对象可以获得任务执行结果或捕捉任务重抛出的异常
execute(Runnable task)
与上面一样,不过没有返回值且无法捕捉异常
区别:
若只是单纯提交不用考虑异常,用execute;
若需要返回值或处理异常等操作,用submit
(2).构造方法
关于ThreadPollExecutor 的构造方法有四个,不过 第四个 是 前三个构造方法 参数的总和,所以主要介绍第四个
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
这里挨个介绍
稍微总结一下
(3).使用
这样的方法使用时非常不方便,参数填的非常多了
为此java对 ThreadPoolExcutors 进行了封装并提供了简便的工厂类Excutors ,可以查看是如何实现的
Excutors作为工厂类里面对ThreadPoolExcutor进行封装,提供了一系列创造对象的方法,可以通过传递少量参数的方法更便利创造出对应类型的线程池对象
总的来说,基本就这几种常用
内部逻辑这里不写,有兴趣的可以查看源码观察如何实现
简单代码演示:
public class Demo4 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
final int id = i;
executorService.submit(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("线程名字>:"+thread.getName()+"执行到第"+id+"次:");
}
});
}
}
}
这里使用的是普通线程池,执行完会等60s退出 ,当然若是使用固定线程池则不会等待
(4).自己实现
class MyThreadPoolExcutors {
// 这里不考虑线程上限
private BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<>();
public MyThreadPoolExcutors(int n){
for (int i = 0; i < n; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
// 线程池是为了提前创建线程,直接拿来用
// 期望是在任务结束前线程是不会结束的
// 所以加了 循环,当线程创建开始不断执行任务
while(true){
Runnable MyTask = blockingQueue.take();
MyTask.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 不希望 主线程结束了 这些线程还在取值
// 变为后台线程
thread.setDaemon(true);
thread.start();
}
}
public void submit(Runnable task) throws InterruptedException {
blockingQueue.put(task);
}
}
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExcutors myBlockingQueue = new MyThreadPoolExcutors(4);
for (int i = 0;i < 1000;i++){
final int id = i;
myBlockingQueue.submit(()->{
String str = Thread.currentThread().getName();
System.out.println("name = "+str+" 执行第"+id+"次");
});
}
// 让主线程调用完sleep一会,确保任务执行完
Thread.sleep(900);
}
}
18.定时器
1>:介绍:和闹钟类似,达到⼀个设定的时间之后, 就执⾏某个指定好的代码.
⽐如⽹络通信中, 如果对⽅ 500ms 内没有返回数据, 则断开连接尝试重连.比如看视频卡了5000ms没反应,则会断开并重新连接⽐如⼀个 Map, 希望⾥⾯的某个 key 在 3s 之后过期(⾃动删除).类似于这样的场景就需要⽤到定时器.
2>:代码演示
在java中将定时器进行了封装,提供了一个类 Timer,其核⼼⽅法为 schedule
schedule 包含两个参数.第⼀个参数指定即将要执⾏的任务代码,第⼆个参数指定多⻓时间之后执⾏ (单位为毫秒).
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello word");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello word");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello word");
}
},1000);
// 确保当前任务可执行完
Thread.sleep(4000);
// 因为 这些任务会被 timer 内部的线程执行
// 但这个线程执行完并没停止
// cancel 立即停止当前线程,并忽略没执行的任务
timer.cancel();
}
}
3>:自己实现
class MyTimeTask implements Comparable<MyTimeTask>{
// 任务
private Runnable task;
// 时间,(采用绝对时间戳)
private long time;
public MyTimeTask(Runnable task,long delay){
this.task = task;
this.time = delay + System.currentTimeMillis();
}
public void run(){
this.task.run();
}
public Runnable getTask() {
return task;
}
public long getTime() {
return time;
}
// 优先级队列按照 MyTimaTask 排序,所以要写比较规则
@Override
public int compareTo(@NotNull MyTimeTask o) {
return (int)(this.time - o.time);
}
}
// 描述一个计时器
class MyTime{
// 采用 优先级队列,因为希望按照时间来排序,队顶元素就是时间最小的
// 而 顺序表 链表等显然不合适,HashTree倒可以排序,但取出元素远没有堆方便
private PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();
private Object locker = new Object();
public MyTime(){
new Thread(new Runnable() {
@Override
public void run() {
while(true){
synchronized (locker){
try{
// 1.拿到时间最短的元素
// 不清楚是否为空,所以 peek
MyTimeTask myTimeTask = queue.peek();
// 2. 进行判断
while(myTimeTask == null){
// 用 wait 代替 continue 避免忙等
locker.wait();
myTimeTask = queue.peek();
}
// 3. 拿到当前时间
long n = System.currentTimeMillis();
// 4.与执行时间进行判断
if(myTimeTask.getTime() <= n){
queue.poll();
myTimeTask.run();
}else {
// 当前时间 < 执行时间,那就等多长时间
locker.wait(myTimeTask.getTime() - n);
}
}catch (InterruptedException e){
throw new RuntimeException("wait 等待超时");
}
}
}
}
}).start();
}
public void schedule(Runnable task,long delay){
synchronized (locker){
MyTimeTask timeTask = new MyTimeTask(task,delay);
queue.offer(timeTask);
locker.notify();
}
}
}
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
MyTime myTime = new MyTime();
myTime.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello---3");
}
},3000);
myTime.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello---2");
}
},2000);
myTime.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello---1");
}
},1000);
// 确保任务执行完,并退出
Thread.sleep(5000);
System.exit(-1);
}
}
这里实现的是优先级队列+阻塞,那么就想到阻塞队列用优先级队列实现,这里为什么不用?
所以阻塞队列很危险,一不留神就出现死锁,一般情况下用优先级队列
六.总结
1>:锁
对于锁来说,加锁会让线程安全,但也会降低效率,,因为必须等A线程解锁后B才开始运行,所以不要盲目去加 ,什么时候需要了,再去加
2>:保证线程安全
1. 使⽤没有共享资源的模型2. 适⽤共享资源只读,不写的模型a. 不需要写共享资源的模型( 每个线程都只给本线程的变量赋值)b. 使⽤不可变对象( eg:final修饰的变量)3. 直⾯线程安全(重点)a. 保证原⼦性b. 保证顺序性,防止线程抢占c. 保证可⻅性,注意编译器优化问题
3>:线程的优点
1. 线程作为轻量级进程,比进程创建花费的资源少,且本身 占⽤的资源要⽐进程少很多2. 与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多3. 将多线程任务可分配到不同处理器核心进行并发操作,即: 充分利⽤多处理器的可并⾏数量4. 在等待慢速I/O操作结束的同时,其他线程可执⾏其他的计算任务5. 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
4>: 进程与线程的区别
1. 进程是系统进⾏资源分配和调度的⼀个独⽴单位,线程是程序执⾏的最⼩单位。2. 进程有⾃⼰的内存地址空间,线程只独享指令流执⾏的必要资源,如寄存器和栈。3. 由于同⼀进程的各线程间共享内存和⽂件资源,可以不通过内核进⾏直接通信。4. 线程的创建、切换及终⽌效率更⾼。
总结:
对于锁来说,加锁会让线程安全,但也会降低效率,,因为必须等A线程解锁后B才开始运行,所以不要盲目去加 ,什么时候需要了,再去加