文章目录

  • 一、synchronized 的特性
    • 互斥
    • 可重入
  • 二、 synchronized 使用示例
  • 三、 java标准库的线程安全类
  • 四、 死锁
    • 可重入死锁
    • 相互争夺锁
    • 哲学家就餐问题
    • 死锁的四个必要条件

一、synchronized 的特性

互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

进入该方法相当于针对该对象”加锁” ( lock )
执行完该方法相当于对该对象”解锁” ( unlock )

当有一个线程加锁之后,其他线程只能阻塞等待直到释放锁
注意:

  1. 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
  2. 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

可重入

一个线程对同一个对象是否可以连续加锁两次,如果可以就是可重入的

public synchronized void add() {        synchronized (this) {            this.num++;        }    }



站在this锁对象,它认为自己已经被另外的线程占用的,这里的第二次加锁是否要阻塞等待” />
线程进入一个层锁,第二次加锁的时候会组设等待,直到第一次锁释放,才能获取第二个锁,但是我们会在锁对象里记录一下,当前的锁是那个对象是那个线程持有的,如果加锁线程和持有线程是同一个,就放过,否则阻塞。

上面的代码是完全没问题的. 因为 synchronized 是可重入锁.

二、 synchronized 使用示例

synchronized的3种使用方式:

  1. 修饰实例方法:作用于当前实例加锁
public synchronized void add() {           }
  1. 修饰静态方法:作用于当前类对象加锁
public synchronized static void add() {           }
  1. 修饰代码块:指定加锁对象,对给定对象加锁
    锁当前对象
public void add() {        synchronized (this) {                    }    }

锁类对象

public class SynchronizedDemo {    public void method() {        synchronized (SynchronizedDemo.class) {       }   }}

我们需要注意的是,synchronized锁的是什么?
只有两个线程竞争获取同一把锁,才会阻塞等待。

两个线程分别获取不同的锁不会产生竞争。

三、 java标准库的线程安全类

之前我们一直是单线程操作线程,所以也不用太注重线程安全问题,但当我们多线程操作集合的时候,我们就需要注意线程安全问题了。
线程不安全的集合:
1.ArrayList
2.LinkedList
3.HashMap
4.TreeMap
5.HashSet
6.TreeSet
7.StringBuilder

线程安全的集合:
1.ConcurrentHashMap
2.StringBuffer

我们可以看到StringBuffer的方法大多数都加了synchronized.

但是concurrentHashMap的方法没有加synchronized,但是同样是线程安全的,大家下去可以研究一下。
String我们没有加任何的锁,但它是不可修改的,仍然是线程安全的。
为什么不给每个集合都加上锁呢” />

这里我们举一个生活中类似的例子,某一天一码通给崩了,我们的手机健康吗打不开了,于是我们的程序员就需要赶到公司去修复这个问题,但是在公司楼下被保安拦住了,要求出示健康码,程序员说: 我上楼修复了bug才能出示健康码。保安: 你出示了健康码才能上楼修复bug. 这个情景和我们此处的死锁非常类似。

public static void main(String[] args) {        Object A = new Object();        Object B = new Object();        Thread t1 = new Thread(() -> {            synchronized (A) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println("t1获取到了锁A和锁B");                }            }        });        Thread t2 = new Thread(() -> {            synchronized (B) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (A) {                    System.out.println("t2获取到了锁A和锁B");                }            }        });        t1.start();        t2.start();    }


我们可以发现程序什么都没有输出,证明t1,t2都没有获取到两把锁,相互堵塞状态。
我们打开jconsole看一下线程详细状态。

我们可以看到我们创建的t1,t2线程

我们可以看到t1处于BLOCKED阻塞状态,看到堆栈信息20行,证明我们t1线程获取不到锁B。

我们可以看到t2也处于BLOCKED阻塞状态,看到堆栈信息32行,证明我们t1线程获取不到锁A。
针对这样的死锁问题,我们需要借助jconsole这样的工具查看状态和堆栈信息去分析原因并进行修改。

哲学家就餐问题


我们有六个哲学家和六根筷子,每个哲学家要想吃饭,就必须拿起左手和右手的两根筷子。
每个哲学家只有两种情况:
1.发呆状态(线程阻塞状态)
2.吃饭状态(线程获取到锁并执行)
由于操作系统的随机调度,这六个哲学家,随时都可能吃面条,和发呆。但有这么一种情况。

我们六个哲学将同时处于吃饭状态,拿起了右手的筷子,当准备拿左手的筷子时,发现没有筷子可拿,都在等左边的哲学家放下筷子,可是没有哲学家放,于是都陷入了阻塞状态,这就是一种典型的死锁问题。

死锁的四个必要条件

  1. 互斥使用:线程1拿到了锁,线程2就得进行阻塞状态。
  2. 不可抢占:线程1拿到锁之后,必须是线程1主动释放,线程2不可能强行获取到
  3. 请求和保持:线程1拿到锁A后,再去获取锁B的时候,A这把锁仍然保持,不会释放
  4. 循环等待:我们第二种死锁的典型情况,线程1和线程2尝试获取锁A,B,线程1在获取锁B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放A

由于synchronized的特性,前三点我们是无法改变的,想要打破死锁,我们只能从循环等待这里改变。
给锁编号,按照一个固定的顺序来加锁,我们针对银行家问题按从小到大加锁。

我们可以发现当该哲学家去拿左手小的1筷子的时候,发现已经被拿了,于是它进入阻塞状态。

另一个哲学家拿起5.6,然后放下,然后旁边的哲学家在拿起4.5放下,这样死锁问题就解决了。

我们再看一下t1,t2获取A,B的那个死锁问题,我们规定按照锁A,B的顺序进行获取。

public static void main(String[] args) {        Object A = new Object();        Object B = new Object();        Thread t1 = new Thread(() -> {            synchronized (A) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println("t1获取到了锁A和锁B");                }            }        });        Thread t2 = new Thread(() -> {            synchronized (A) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println("t2获取到了锁A和锁B");                }            }        });        t1.start();        t2.start();    }


我们可以看到不会再出现死锁问题,t1,t2都获取到了A,B锁。