本篇接着上篇 线程安全,可以先看一下上篇~~
举例理解:
锁的特点是互斥的,同一时刻只有一个线程能获取到锁,其他的线程若也尝试获取锁,就会发生阻塞等待,一旦发生等待,需一直等到刚才的线程释放锁,此时剩下的线程再重新竞争锁
1.加锁(获取锁) — lock
2.解锁(释放锁) — unlock
Java 中使用锁,需要借助关键字 synchronized
例如,上边自增的例子,我们可以在方法前加这个关键字:
加锁解锁都由一个关键字来包办,这样的好处是:避免出现忘记解锁的情况
synchronized public void increase(){
count++;
}
此时就是:进入 increase 方法前,会先尝试加锁;increase 方法执行完毕后,就会自动解锁
尝试加锁的时候不一定就能立刻成功,如果发现当前的锁已经被占用了,该代码就会阻塞等待,一直等到之前的线程释放锁,才可能会获取到这个锁
此时,看运行结果:
上述结果,我们就发现,自增发生的线程不安全问题就得到了解决
那么,锁是如何解决线程安全问题的???
线程 在获取到锁之后,如果出问题(因为其他原因导致长时间的阻塞),此时,其他线程也只能"干瞪眼"~
这也是多线程编程时会遇到的一个常见困难,极端情况下,可能会出现死锁的情况。一旦死锁了,锁就永远也解不开了,程序也就凉凉了~~
死锁中非常经典的案例:哲学家进餐问题
锁,用起来没那么容易,存在很多注意事项:
之前所学习的 StringBuffer 是线程安全的,StringBuilder 是线程不安全的,换句话说,也就是 StringBuffer 内部加锁了,同样,还有 Vector(🔒) / ArrayList ,HashTable(🔒) / HashMap
若在单线程下使用,仍然建议使用不加锁的,因为更高效!
可以灵活的指定某个对象来加锁,而不仅仅是把锁加到某个方法上
若把 synchronized 关键字写到方法前,相当于给当前对象 (this) 来加锁
所谓的加锁,其实是给某个指定的对象来加锁
画图理解:
1.加到普通方法前:表示锁 this
2.加到静态方法前:表示锁 当前类的类对象
3.加到某个代码块前:显式指定给某个对象加锁
// 3 举例:
public class ThreadDemo15 {
static class Test{
public void method(){
// 这里写成 this,也就是给下面的 t 加锁
synchronized (this){
System.out.println("呵呵");
}
}
}
public static void main(String[] args) {
Test t = new Test();
t.method();
}
}
.
public class ThreadDemo16 {
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(){
@Override
public void run(){
Scanner scan = new Scanner(System.in);
synchronized (locker){
System.out.println("请输入一个整数: ");
// 用户如果不输入,就会一直阻塞在 nextInt 里,这个锁就会被一直占有
int num = scan.nextInt();
System.out.println("num = " + num);
}
}
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run(){
while (true){
synchronized (locker){
System.out.println("线程2 获取到锁啦~");
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
t2.start();
}
}
输出结果:
由输出结果可以发现:一旦线程1 获取到锁,并且没有释放的话,线程2 就会一直在锁着里阻塞等待
可通过 jconsole 查看:
.
.
当输入一个整数之后,让 线程1 释放锁之后,线程2 才能继续执行:
也就是 让线程1 加锁 locker1,线程2 加锁 locker2
此时再运行结果,就会发现,两个线程之间不再有任何竞争关系,各自跑各自的~
t1 获取到锁之后,t2 仍然再继续执行,即:两个线程之间没有竞争,没有互斥了 (用的两个对象来分别 synchronized)
.
仍以取钱为例:
两个人分别在不同的ATM机里取钱,就没有竞争,各自取各自的
锁和对象是一一对应的,每个对象的对象头内部都有一个锁标记,在加锁时,一定要明确当前的代码是在给哪个对象进行加锁,以及需要思考清楚,这样的操作是否能起到互斥的效果
若再将代码修改为(只贴修改了之后的代码):
synchronized (locker1.getClass())
synchronized (locker2.getClass())
那么,此时,线程1 和 线程2 会在发生互斥嘛?
会发生互斥,因为getClass 得到的是类对象(类对象只有一个,同一个类只有唯一的类对象),两个线程都在分别针对 locker1 和 locker2 的类对象进行竞争,此处 locker1 和 locker2 类型都是 Object,对应的类对象其实是相同的对象
线程1 尝试加锁的时候,就会把这个对象头的锁标记改成 true,此时,若线程2 也去尝试加锁,就会发现标记已经是 true,就无法修改了,就会陷入阻塞,因此,发生了竞争互斥
代码示例:
public static void main(String[] args) {
Object locker = new Object();
Thread t = new Thread(){
@Override
public void run(){
synchronized (locker){
System.out.println("请输入一个数: ");
Scanner scan = new Scanner(System.in);
int num1 = scan.nextInt();
System.out.println(num1);
synchronized (locker){
System.out.println("请输入一个数: ");
int num2 = scan.nextInt();
System.out.println(num2);
}
}
}
};
t.start();
}
此时由于 locker 对象已经被加锁了,因此第二个对 locker 对象加锁的操作应该会加锁失败(需要第一个锁释放),这个代码看起来是一个死锁
事实上,这个代码不会死锁,synchronized 在内部针对这样的情况进行了特殊处理,synchronized 底层调用的是 操作系统提供的 mutex (互斥量)
Java 中的加锁 → pthread_mutex_lock函数
如果在一个线程中,连续调用两次这个方法,就会产生死锁
synchronized 会在加锁时判定,若当前对象已经在 当前线程 中加过锁了,这个操作并不会真的执行这个 lock 函数,而是仅仅维护一个"引用计数"
第一次进行 synchronized 的时候,才真的调用了这个 lock 函数
Java 中的解锁 → pthread_mutex_unlock函数
当"引用计数" 减到0时,才真的调用 unlock 函数
上述这种特性的锁 — 可重入锁
总结:
每个类有一个自己的唯一的类对象,若 locker1 和 locker2 类型不同,对应到两个类对象(两把锁,就不会发生竞争)
但是如果两个对象是相同类,就是对应到一个类对象(即:同一把锁,就会发生竞争)
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_47988201/article/details/121415314
内容来源于网络,如有侵权,请联系作者删除!