初识多线程(2.0)
目录
等待一个线程的结束 join
join 可以 决定 多个线程执行的 先后顺序

在以上的代码中 main 线程中 在 等待 t 线程结束再执行
在main中调用join
所以main 要等待 t 执行完成才能继续执行 否则只能堵塞等待中。。。
为了避免要处于一直等待中 所以 join又提供带参数的版本
t.join(mills : 3000, nanos : 500);
当执行时间超过了 3秒 join 于是就不再等待了 就继续向下执行
nanos(纳秒) 在一般的场景中是不考虑这个参数的
因为这样的单位的精确 很难进行
休眠当前线程 sleep
也是在先前所使用到的方法
但有一点值得注意的是:
因为线程的调度是不可控的,
所以,这个⽅法只能保证 实际休眠时间是⼤于等于参数设置的休眠时间的。
此外 它还有着 sleep(0) 的 特殊用法
表明这里的线程 放弃当前的资源 等待操作系统的重写调配
(将CPU的资源让出执行其他)
Thread
1.线程创建
2.关键属性
3.终止线程
4.线程等待
5.获取线程引用
6.线程休眠
线程状态
NEW:安排了⼯作,还未开始⾏动、
new 了新的 Thread对象 但还未 start
RUNNABLE:可⼯作的.⼜可以分成正在 ⼯作中和 即将 开始⼯作.
BLOCKED:这⼏个都表⽰排队等着其他事情
也是一种特殊的等待,由 锁🔒造成的阻塞
WAITING:这⼏个都表⽰排队等着其他事情
死等 没有超时时间的等待
TIMED_WAITING:这⼏个都表⽰排队等着其他事情
线程阻塞中 (阻塞的时间是有上线的)
而且 join(时间)也会进入到 TIMED_WAITING 状态内
TERMINATED:⼯作完成了
内核中的线程已经结束,但是thread的对象还存在
线程安全
线程是并发执行的,调度是随机的
所以不写出安全的代码 会引发多线程的安全问题

按照正常逻辑
先执行t1 结束后
执行t2
或者
先执行t2 结束后
执行t1(这里都不会引发阻塞)
这里打印的值都是 t1 t2 所执行完的
理应说值应该是10000 但是结果相差甚远
所以这里产生了 线程安全问题

产生原因
1) 随机调度 抢占式执行(根本)
2)多个线程同时修改一个变量
3)修改操作不是原子的
个操作不能被中途打断,要么全部执行完,要么完全不执行
Java 里很多代码看起来是一行,实际是多个步骤,多线程切换时会打断执行,导致结果错误。
例如先前的++操作
4)内存可见性
可⻅性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到
5)指令重排序
编译器 / CPU 为了优化效率,会指令重排(打乱执行顺序),单线程没事,但多线程下会导致逻辑错误。
解决方法
synchronized关键字
1) 互斥
synchronized会起到 互斥 效果
某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏ 到同⼀个对象synchronized就会 阻塞等待.
synchronized(锁的对象){
一些要保证线程安全的代码
}
synchronized 修饰普通方法 相当于给this加锁
public class SynchronizedDemo {
public synchronized void methond() {
}
}
synchronized 修饰静态方法 相当于类给对象加锁
public class SynchronizedDemo {
public synchronized static void method() {
}
}
监视器锁 monitor lock
JVM中采用的术语
使用锁的会报出一些异常
可以通过 监视器锁 可以看到这些报错信息
2)可重入


死锁是非常严重的bug 当执行到这里时会直接卡住
但是对于JAVA 的 synchronized 来说
synchronized 同步块对同⼀条线程来说是 可重⼊ 的,不会出现⾃⼰把⾃⼰锁死的问题
因此没有上⾯的问题.
此外还有一种特殊情况

可重入锁原理:
锁对象内部保存,到那哪个线程 ,在后续的线程再次加锁中 线程对比是否和以前的加锁线程相同

可重入锁实现
1.在锁的内部记录当前锁是哪个线程持有的,后续加锁进行判断
2.通过计数器确定要真正的要解锁的对象

修改代码如上所示
死锁
1) 一把线程 连续加两次锁🔒
上述已经提及
2)两把线程 两把锁🔒 每个线程在第一把锁后尝试获取对方的锁

以上是一个很常见的产生死锁的场景
在不释放第一把锁的前提之下 执行第二把锁
sleep 执行让这里的实现确保成功
因为可能当T1都执行完了 T2都还没开始呢
3)N个线程 M把锁
经典的哲学家就餐问题(这里不过多赘述)
在极端情况之下 都使用左手中的筷子 就会触发死锁 就开始一起等待
构成死锁条件
互斥条件
资源一次只能由一个进程占用,其他进程必须等待该资源释放 阻塞等待 。
非抢占条件
已分配给进程的资源不能被其他进程强行夺取,必须由进程主动释放。
synchronized 天然满足以上两个条件
请求并保持条件
一个锁在不释放的前提之下,又开启了第二个锁
循环等待条件
A等B B等C C等A
约定好加锁顺序就可以破除循环
只要避免3 和 4的其中一种既可以避免死锁
Java 标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,⼜没有任何加锁措 施.
•ArrayList
• LinkedList
• HashMap
• TreeMap
• HashSet
• TreeSet
• StringBuilder
但是还有⼀些是线程安全的.使⽤了⼀些锁机制来控制.
• Vector(不推荐使⽤)
• HashTable(不推荐使⽤)
对于加锁的代码 有了锁相当于可能与代码会发生竞争产生冲突 ---》 产生了阻塞 导致执行的效率大打折扣
• ConcurrentHashMap
• StringBuffer
(都在关键方法加入了synchronized)
volatile 关键字
内存可⻅性
是造成线程安全的主要原因之一
这里一个简单例子说明
一个线程读取
一个线程使用
修改线程的值并没有读取到 并没有被读写的线程读写到


所以使用了 volatile 关键字 修饰变量
此时编译器不会将其优化成寄存器中
private volatile static int flag = 0;
volatile 解决了内存可见性 问题
依旧待续未完中....
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐





所有评论(0)