synchronized 关键字
Posted 熬夜磕代码丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了synchronized 关键字相关的知识,希望对你有一定的参考价值。
文章目录
一、synchronized 的特性
互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入该方法相当于针对该对象"加锁" ( lock )
执行完该方法相当于对该对象"解锁" ( unlock )
当有一个线程加锁之后,其他线程只能阻塞等待直到释放锁
注意:
- 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
- 假设有 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种使用方式:
- 修饰实例方法:作用于当前实例加锁
public synchronized void add()
- 修饰静态方法:作用于当前类对象加锁
public synchronized static void add()
- 修饰代码块:指定加锁对象,对给定对象加锁
锁当前对象
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我们没有加任何的锁,但它是不可修改的,仍然是线程安全的。
为什么不给每个集合都加上锁呢?
因为在加锁的同时,会带来额外的时间开销,有些情况下,不使用多线程,不会面临线程安全问题,所以我们的一些集合并没有加锁,因此,我们在多线程情况下使用这些集合时,可以手动加锁。
四、 死锁
可重入死锁
public synchronized void add()
synchronized (this)
this.num++;
一个线程,一把锁,连续加入两次,如果锁不是可重入锁,就会造成死锁问题。
但是Java的synchronized和ReentrantLock都是可重入锁,这里我们无法演示,大家可以参考我上面加可重入问题所讲的。
相互争夺锁
两个线程两把锁,t1,t2现针对锁A,锁B加锁,再去获取对方的锁,双方都不愿意让步,就会造成死锁问题。
这里我们举一个生活中类似的例子,某一天一码通给崩了,我们的手机健康吗打不开了,于是我们的程序员就需要赶到公司去修复这个问题,但是在公司楼下被保安拦住了,要求出示健康码,程序员说: 我上楼修复了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拿到了锁,线程2就得进行阻塞状态。
- 不可抢占:线程1拿到锁之后,必须是线程1主动释放,线程2不可能强行获取到
- 请求和保持:线程1拿到锁A后,再去获取锁B的时候,A这把锁仍然保持,不会释放
- 循环等待:我们第二种死锁的典型情况,线程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锁。
以上是关于synchronized 关键字的主要内容,如果未能解决你的问题,请参考以下文章