锁 & synchronized 关键字

x33g5p2x  于2021-11-28 转载在 其他  
字(3.4k)|赞(0)|评价(0)|浏览(394)

本篇接着上篇 线程安全,可以先看一下上篇~~

锁 是什么?

举例理解:

锁的特点:

锁的特点是互斥的,同一时刻只有一个线程能获取到锁,其他的线程若也尝试获取锁,就会发生阻塞等待,一旦发生等待,需一直等到刚才的线程释放锁,此时剩下的线程再重新竞争锁

锁的基本操作:

1.加锁(获取锁) — lock
2.解锁(释放锁) — unlock

锁的使用

synchronized 关键字 — 监视器锁

Java 中使用锁,需要借助关键字 synchronized

例如,上边自增的例子,我们可以在方法前加这个关键字:
加锁解锁都由一个关键字来包办,这样的好处是:避免出现忘记解锁的情况

synchronized public void increase(){
    count++;
}

此时就是:进入 increase 方法前,会先尝试加锁;increase 方法执行完毕后,就会自动解锁
尝试加锁的时候不一定就能立刻成功,如果发现当前的锁已经被占用了,该代码就会阻塞等待,一直等到之前的线程释放锁,才可能会获取到这个锁

此时,看运行结果:

上述结果,我们就发现,自增发生的线程不安全问题就得到了解决
那么,锁是如何解决线程安全问题的???

线程 在获取到锁之后,如果出问题(因为其他原因导致长时间的阻塞),此时,其他线程也只能"干瞪眼"~
这也是多线程编程时会遇到的一个常见困难,极端情况下,可能会出现死锁的情况。一旦死锁了,锁就永远也解不开了,程序也就凉凉了~~

死锁中非常经典的案例:哲学家进餐问题

锁,用起来没那么容易,存在很多注意事项:

  • 使用的时候,一定要注意按照正确的方式来使用,否则就容易出现各种问题
  • 一旦使用锁,这个代码基本上就和 “高性能” 无缘了
    锁的等待时间是不可控的,可能会等很久很久

之前所学习的 StringBuffer 是线程安全的,StringBuilder 是线程不安全的,换句话说,也就是 StringBuffer 内部加锁了,同样,还有 Vector(🔒) / ArrayList ,HashTable(🔒) / HashMap
若在单线程下使用,仍然建议使用不加锁的,因为更高效!

理解 synchronized 的具体使用:

可以灵活的指定某个对象来加锁,而不仅仅是把锁加到某个方法上
若把 synchronized 关键字写到方法前,相当于给当前对象 (this) 来加锁
所谓的加锁,其实是给某个指定的对象来加锁

画图理解:

synchronized 的几种常见用法:

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 类型不同,对应到两个类对象(两把锁,就不会发生竞争)
但是如果两个对象是相同类,就是对应到一个类对象(即:同一把锁,就会发生竞争)

相关文章