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

简介: 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

相关文章
|
7月前
|
安全 Java
Synchronized和Lock的区别
Synchronized和Lock的区别
72 0
|
Java
synchronized和lock的区别
synchronized和lock的区别
89 0
|
7月前
|
Java C++
11.synchronized底层是怎么通过monitor进行加锁的?
11.synchronized底层是怎么通过monitor进行加锁的?
48 0
11.synchronized底层是怎么通过monitor进行加锁的?
|
7月前
|
设计模式 安全 编译器
线程学习(3)-volatile关键字,wait/notify的使用
线程学习(3)-volatile关键字,wait/notify的使用
60 0
|
缓存 Java 编译器
volatile,解决内存可见性引起的问题,wait和notify
volatile,解决内存可见性引起的问题,wait和notify
|
Java
Lock 和 Synchronized的区别?
本章主要讲解了Lock 和 Synchronized的区别和知识点
55 0
|
Java
synchronized和Lock的区别
synchronized和Lock的区别
76 0
|
安全 Java 编译器
【JavaEE】synchronized监视锁 volatile关键字wait和notify
【JavaEE】synchronized监视锁 volatile关键字wait和notify
【JavaEE】synchronized监视锁 volatile关键字wait和notify
|
安全 Java 数据库
多线程之Lock显示锁
多线程之Lock显示锁
锁、C#中Monitor和Lock以及区别
1.Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,当然在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。
2611 0

热门文章

最新文章