一个线程针对同一个对象,连续加锁两次,是否会有问题 ~~ 如果没问题,就叫可重入的.如果有问题,就叫不可重入的.
代码示例:
synchronized public void add(){
synchronized (this){
count++;
}
}
解析:
锁对象是this,只要有线程调用add,进入add方法的时候,就会先加锁(能够加锁成功).紧接着又遇到了代码块,再次尝试加锁.站在this 的视角(锁对象),它认为自己已经被另外的线程给占用了,这里的第二次加锁是否要阻塞等待呢?
如果第二次加锁成功,这个锁就是可重入的,Java中的synchronized 是“可重入锁”.
如果第二次加锁会阻塞等待,就是不可重入的,这样的锁称为“不可重入锁”.
这时新的情况就出现了:
场景如下: 一个线程没有释放锁, 然后又尝试再次加锁,这个锁是“不可重入锁”,第二次加锁的时候, 就会阻塞等待,直到第一次的锁被释放, 才能获取到第二个锁,但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作,这时候就会死锁.
注: 博主后面的内容会重点讲解死锁.
多个线程操作同一个集合类,就需要考虑到线程安全的事情.
1.Java 标准库中很多集合类都是线程不安全的,没有任何加锁措施.
比如:ArrayList
,LinkedList
,HashMap
,TreeMap
,HashSet
,TreeSet
,StringBuider
.
2.但是还是有一些内置了synchronized
加锁的集合类,线程是相对来说,更安全一点的,
比如: Vector
(不推荐使用),HashTable
(不推荐使用),StringBuffer
,ConcurrentHashMap
.
3.最后就是String
它没有加锁,但是由于是不可变对象,不涉及修改,线程是绝对安全的
问题: 既然加锁了,线程会变得安全一些,为什么集合类都不加上锁了?
原因: (1).加锁这个是有副作用的,会产生额外的时间开销.
(2).对程序猿来说,有更多的选择空间,这些没内置加锁机制的集合类,当没有线程安全问题的时候,就可以放心用,当有线程安全问题的时候,可以手动加锁.但是像StringBuffer这些内置synchronized加锁的结合类,就没有这种选择,这也是为什么是Vector,HashTable不推荐使用的原因之一.
~~ 死锁是一个非常影响我们幸福感问题.
一旦程序出现死锁,就会导致线程就跪了,无法继续执行后续工作了,程序势必会有严重bug,在我们写代码的时候,不经意间,就会写出死锁代码,并且这玩意还不容易测试出来.
~~ 如果锁是“不可重入锁”,就会死锁.
注: Java里 synchronized 和 ReentrantLock 都是“可重入锁”.C++,Python,操作系统原生的加锁API都是不可重入的.
~~ 线程 t1 和线程 t2 各自先针对锁A和锁B加锁,再尝试获取对方的锁.
例子: (1)可以理解为你把家里门的钥匙锁在车里了,而车钥匙锁在家里了,最后,你不仅车开不了呢,家也回不去了
(2)有一个东北人和一个陕西人正坐在饺子馆的同一个餐桌上准备吃饺子,东北人吃饺子,喜欢蘸酱油,但他面前只有一瓶醋;陕西人吃饺子,喜欢蘸醋,但不巧的是他面前只有酱油;东北人说: “兄弟,你把酱油给我,我用完了之后给你醋”,但是陕西人也说:”老兄,你把醋给我,我用完了之后给你酱油”.假设哈,注意这是假设(现实生活一般不可能会这样)!!!如果这两人互不相让,此时就僵持住了.
例子2的代码:
public class ThreadDemo14 {
public static void main(String[] args) {
Object jiangyou = new Object();// jiangyou => 酱油
Object cu = new Object();// cu => 醋
Thread dongbeiren = new Thread(() -> {// dongbeiren => 东北人
synchronized (cu) {
try {
Thread.sleep(1000);// 确保两个线程都先把第一个锁拿到 => 线程是抢占式执行的
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (jiangyou) {
System.out.println("东北人把醋和酱油都拿到了");
}
}
});
Thread shanxiren = new Thread(() -> {// shanxiren => 陕西人
synchronized (jiangyou) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (cu) {
System.out.println("陕西人把酱油和醋都拿到了");
}
}
});
dongbeiren.start();
shanxiren.start();
}
}
运行结果:
使用 jconsole 查看线程的情况
注: 针对死锁问题,是需要借助像 jconsole 这样的工具进行定位,看线程的状态和调用栈,从而分析出代码是在哪里死锁了.
想要解决死锁问题,就得分析一下死锁的形成.
1.互斥使用: 一个线程拿到一把锁之后,另一个线程就不能使用了(锁的基本特性).
2.不可抢占: 一个线程拿到锁之后,必须是这个线程主动释放,其它线程就获取不到了(墙角是挖不了滴).
3.请求和保持: 线程1拿到锁A之后,再去尝试获取锁B,这时线程1的A这把锁还是保持的,不会因为获取锁B就把锁A给释放了.
个人理解“吃着碗里的,惦记锅里的”,一个男生有了女朋友后,还去和其他女生搞暧昧,追求其他女生(渣男).
4.循环等待: 线程1尝试获取到锁A和锁B,线程2尝试获取到锁B和锁A,线程1在获取B的时候等待线程2的释放;同时,线程2在获取A的时候等待线程1释放锁A(或者可以理解家钥匙锁车里了,车钥匙锁家里了).
注: 条件1,2,3都是锁的基本特性,对于,synchronized这把锁来说,是无法改对.循环等待是这四个条件中唯一一个和代码结构相关的,也是作为程序猿的我们可以控制的.
解决死锁的方法其实很简单,就是破解循环等待这个必要条件:
针对锁进行编号,在需要同时获取多把锁的时候,约定加锁顺序,务必先对小的编号加锁,后对大的编号加锁(这是解决死锁,最简单可靠的办法).
注: 解决死锁,还有个银行家算法,本质上是对资源的更合理的分配,比较复杂,不适合在实际开发中使用,但是是学校操作系统课的期末考试必考题,建议上这门课的时候认真听一下,博主不在这讲解,因为我也不太理解,讲不来,(●’◡’●).
代码:
public class ThreadDemo14 {
public static void main(String[] args) {
// 假设 jiangyou 是 1 号, cu 是 2 号, 约定先拿小的, 后拿大的
Object jiangyou = new Object();// jiangyou => 酱油
Object cu = new Object();// cu => 醋
Thread dongbeiren = new Thread(() -> {// dongbeiren => 东北人
synchronized (cu) {
try {
Thread.sleep(1000);// 确保两个线程都先把第一个锁拿到 => 线程是抢占式执行的
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (jiangyou) {
System.out.println("东北人把醋和酱油都拿到了");
}
}
});
Thread shanxiren = new Thread(() -> {// shanxiren => 陕西人
synchronized (cu) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (jiangyou) {
System.out.println("陕西人把酱油和醋都拿到了");
}
}
});
dongbeiren.start();
shanxiren.start();
}
}
运行结果:
线程的基本概念.轻量,共享资源,节省了资申请的开销
Thread类的使用
线程状态
线程安全
一个多线程实际的执行顺序有多种变数 ~~ 线程安全就是在所有的变数下,都能够运行正确!!!
抢占式执行,随机调度
多个线程同时修改同一个变量
修改操作不是原子性的 ~~ 加锁(synchronized)
内存可见性问题
指令重拍序
关于加锁
修饰方法
修饰代码块 ~~ 手动指定加到那个对象上
锁对象
死锁
死锁的概念
死锁的三个典型情况
一个现场一把锁.连续加锁两次
两个线程两把锁,分别获取对方的锁
N个线程,M把锁
可重入和不可重入
线程针对同一个对象,连续加锁二次,是否会死锁
会死锁,就叫可重入,不会死锁,就叫不可重入的.
注: synchronized 是可重入的
死锁的四个必要条件
最核心的就是“循环等待”
解决: 在针对多把锁加锁的时候,约定好锁的顺序
如何破除死锁