synchronized关键字-监视器锁(monitor lock)

这就是我们上一篇中代码提到的加锁的主要方式,本质上是调用系统api进行加锁,系统api本质是靠cpu特定指令加锁.

synchronize的特性

互斥性

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,,其它线程如果也执行到同一个对象synchronized就会阻塞等待(锁冲突/锁竞争)

进入synchronized修饰的代码块,相当于加锁.

退出synchronized修饰的代码块,相当于解锁.

让我们回顾一下上一篇中这一段代码:

synchronized (locker) {//locker是锁对象,后面会讲
    count++;
}

进入代码块内部(第一个大括号),相当于针对当前对象加锁.

执行完毕(出第二个大括号)相当于针对当前对象"解锁" .

让我们在多线程的场景下分析一下这个过程.

通过锁竞争可以让第二个线程指令无法插入到第一个线程指令中间,但此时第一个线程仍可被调度cpu. 

上述过程就可以看作不同的人(线程)排队上厕所,一个人进去就得上锁,这时其它人进不去,直到那个人开了锁才可以.

理解"阻塞等待"

针对每一把锁,操作系统内部都维护了一个等待队列.当这个锁被某个线程占用的时候,其它线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁.

注意:

上一个线程解锁之后,下一个线程并不是就能够立即获取到锁.而是要靠操作系统来"唤醒".这也就是操作系统线程调度的一部分工作.

假设有A B C三个线程,线程A先获取到锁,然后B尝试获得锁,然后C尝试获得锁,此时B和C都在阻塞队列中排队等待.但是A释放锁之后,虽然B比C先来的,但是B不一定能获取到锁,而是和C重新竞争,并不遵守先来后到的规则.

利用锁确实对多线程执行效率有影响,但这样仍会比串行执行快,因为锁以外的的内容仍然是并发执行的

可重入

定义:synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

因此Java不会出现锁死问题,但锁死的内容仍需要了解

理解"把自己锁死"

一个线程没有释放锁,然后又尝试加锁.

//第一次加锁,加锁成功

lock();

//第二次加锁,锁已经被占用,阻塞等待.

lock();

按照之前锁的设定,第二次加锁的时候,就会阻塞等待.直到第一次的锁被释放,才能获取到第二个锁.但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,就无法进行解锁操作.这时候就会死锁.

死锁的三种典型场景

1.一个线程一把锁:如果锁不是可重入锁.并且一个线程对这把锁2次就会出现死锁(把钥匙锁在屋里了).

public class TestLock {
    public static Object locker = new Object();
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    synchronized (locker) {
                        count++;
                    }
                }
            }
        });

        Thread t2 = new Thread(() -> {
           for(int i = 0; i < 50000; i++) {
               synchronized (locker) {
                   count++;
               }
           }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这里的t1按理来说是死锁的类型,不过synchronized是可重入锁,所以可以正常执行.

2.两个线程两把锁:线程1获取到锁A,线程2获取到锁B,接下来线程1尝试获取到锁B,线程2尝试获取到锁A就会导致死锁.(房子的钥匙锁车里了,车钥匙锁房子里了). 

public class TestLock2 {
    public static Object A = new Object(), B = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
           synchronized (A) {
               //sleep一下,是给t2时间,让t2也能拿到B
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               //尝试获取B,并没有释放A
               synchronized (B) {
                   System.out.println("t1拿到了两把锁");
               }
           }
        });

        Thread t2 = new Thread(() -> {
           synchronized (B) {
               //sleep一下,是给t1时间,让t1能拿到A
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               //尝试获取A,并没有释放B
               synchronized (A) {
                   System.out.println("t2 拿到了两把锁");
               }
           }
        });

        t1.start();
        t2.start();
    }
}

因此,形如这样的代码不会执行到第二次获取锁内的内容,通过jconsole可以观察到原因.

 

可见,两个线程都卡在了获取对方已获取得到 的锁的地方,而且状态为BLOCKED.

不过在这种情况下,仍可以通过约定加锁顺序来解决问题.

3.n个线程可以获取到m把锁. (建议看一下哲学家吃饭问题,这里就不过多讲解了).

死锁的四个必要条件(以下的条件缺一不可,缺一个不构成死锁)

1.互斥使用:获取锁的过程是互斥的.一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待.(锁的基本特性,不好破坏)

2.不可抢占:一个线程拿到锁之后,只能主动解锁,不能让别叠对象把锁强行抢走(锁的基本特性,也不好破坏)

3.请求保持:一个线程拿到锁A之后,在持有A的条件下,尝试获取B(代码结构,看实际需求)

4.循环等待(环路等待):一个想获取到另一个的锁,另一个又在等其它的.(是最容易破坏的:指定一定规则,可避免循环等待->比如指定加锁顺序)

解决死锁的方案

(1)引入一个额外的锁

(2)去掉一个线程

(3)引入计数器,限制同时工作的线程数

(4)前面三个方案普适性不高,还是建议这个:引入加锁规则

以下面的代码为例,让我们分析一下synchronized的可重入性.

 在可重入锁的内部,包含着"线程持有者"和"计数器"两个信息.

如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么就可以继续获取到锁,让计数器自增.(真正加锁,同时给计数器+1(初始为0,加锁之后变成1了,说明当前这个对象被该线程加锁一次),同时记录线程是谁,解锁时把count--,直到count减到零,才算是真正的解锁了)

synchronized使用实例

synchronized本质上要修改指定对象的"对象头".从使用角度来看,synchronized也一定要搭配一个具体的对象使用.

修饰代码块

明确指定锁哪个对象(比较常用的方法).

锁任意对象:

public class TestSynchronizedDemo {
    private Object locker = new Object();
    
    public void method() {
        Synchronized (locker) {
        
        }
    }
}

锁当前对象:

public class TestSynchronizedDemo {
    public void method() {
        synchronized(this) {
            //需要理解好这里是不是同一个对象
        }
    }
}

直接修饰普通方法

锁的TestSynchronizedDemo对象

public class TestSynchronizedDemo {
    public synchronized void method() {
        //相当于给this加锁(锁对象this)
    }
}

 修饰静态方法(不常见)

锁的TestSynchronizedDemo类对象(就如果synchronized是加到static方法上,相当于给类加锁).

public class TestSynchronized {
    public synchronized static void method() {
    
    }
}

 我们要重点理解,synchronized锁的是什么.两个线程竞争同一把锁,才会产生阻塞等待

两个线程分别尝试获取两把不同的锁,不会产生锁竞争.

Java标准库中的线程安全类

Java标准库中很多线程都是不安全的.这些类可能涉及多线程修改共享数据,也没有任何加锁措施

比如:ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder

但还是有一些是线程安全的.使用了一些锁机制来控制.

Vector(不推荐使用),HashTable(不推荐使用),ConcurrentHashMap,StringBuffer(不推荐使用)

还有的是没有加锁,但因为不涉及修改的特殊类,也是线程安全的.

String

 

  • 43
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
synchronized关键字Java中用于实现线程同步的关键字。它可以应用于方法或代码块,确保在同一时间只有一个线程能够访问被synchronized修饰的代码块或方法。 实现原理主要包括以下几个方面: 1. 监视器Monitor Lock):每个Java对象都有一个监视器,可以抽象地理解为对象内部的一种标记。当一个线程访问synchronized代码块或方法时,它会尝试获取对象的监视器,如果获取成功,则进入临界区执行代码;如果获取失败,则线程进入阻塞状态,直到获得为止。 2. 内存屏障(Memory Barrier):synchronized关键字不仅保证了互斥性(即同一时间只有一个线程能够执行synchronized代码块或方法),还保证了可见性和有序性。在释放之前,会将对共享变量的修改刷新到主内存中;在获取之前,会从主内存中重新读取共享变量的值,确保每个线程看到的共享变量值一致。 3. 重入性(Reentrancy):synchronized关键字是可重入的,即同一个线程可以多次获取同一个对象的监视器而不会发生死。每次获取时,的计数器会递增,释放时计数器递减,只有当计数器为0时才真正释放。 4. 互斥性(Mutual Exclusion):synchronized关键字保证了临界区的互斥性,同一时间只有一个线程能够执行被synchronized修饰的代码块或方法。其他线程需要等待当前占用的线程释放后才能继续执行。 需要注意的是,synchronized关键字只能用于同一个进程或线程内部的同步,不能用于不同进程或线程之间的通信和同步。在Java 5及之后,还引入了更灵活的Lock和Condition接口来替代synchronized关键字,提供了更多高级的线程同步操作和更细粒度的控制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值