synchronized关键字解决线程安全问题
# 1. 同步问题
多线程编程的三大问题:
- 分工:把不同的工作分配给不同的线程执行,提高效率
- 同步:多个线程操作同一个资源(共享资源)
- 互斥:多线程并发时,某一时刻只能有一个线程访问资源
共享资源带来的并发问题 (opens new window)(👈详细的分析点击文章)
【并发问题的引出----12306卖票】
卖票案例出现了线程的安全问题,出现了不存在的票和重复的票,这就属于线程的安全性问题,这在现实生活中(12306卖票)时不允许存在的。
假如你卡里有500w,你和你女朋友同时到银行去取钱。你到柜台取300万的同时,你女朋友拿着卡到自动取款机取400万。因为在同一时刻,你们共同的账户都有500万,你们取的金额都小于500万。但是,有一个人取的同时,另一个人一定是不能同时操作的,不然取出的就是700万了。
当一个人操作账户时,银行为了防止数据错误,另一个人是没法取钱的,此时这个资源被锁上了。得等到你女朋友先取完钱,将账户金额扣除400万之后,你才能去取钱。
那么,怎样来解决线程的安全性问题呢?
我们可以给线程加个锁🔗,独占资源,来保证一个线程在访问共享数据的时候,无论是否失去了对CPU的执行权,让其他的线程只能等待当前占有CPU的线程执行完其所有的操作,其他线程才能获取资源(等待当前线程卖完票,其他线程在进行卖票)
锁🔗怎么来实现呢?可以通过synchronized关键字为程序逻辑上锁
synchronized关键字:
synchronized控制对 “对象” 的访词,每个对象对应一把锁。把synchronized理解为一把锁🔒,锁的是线程对象,所以也称为对象锁。
CPU会给被调度的线程发一把钥匙🔑,当前线程只有在获取到了这把锁的钥匙🔐之后,才能进入到同步方法或者同步代码块中共享数据。
锁🔒具有独占性,当线程A获取到对象锁之后,其他线程即使得到了CPU的调度,也领到了钥匙📌,但是取没法打开。只有当线程A执行完其线程任务之后,其他线程才能拿到钥匙📌访问资源。
每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
这样做的好处是保证了数据的安全性(买票不会出现重复票和不存在的票),劣势就是影响执行效率。
synchronized解决线程安全问题:
- 使用同步代码块
- 使用同步方法
# 2. 锁的实现
# 同步代码块
【12306卖票修改】
# 同步原理的分析
假设CPU调度线程的顺序是线程A、线程B、线程C
- 线程A先抢到了CPU的执行权,进入while循环,遇到了synchronized代码块
- 线程A会获取到synchronized代码块的锁对象,进入到同步代码块中执行
- 线程A在睡眠时,线程B抢到了CPU的执行权
- 进入到了while循环,遇到了同步代码块。此时发现对象锁被线程A占有,线程B没法获取无法进入同步代码块。此时线程B处于阻塞中,一直等到线程A归还锁对象
- 同理线程C也处于阻塞中
- 一直到线程A执行完同步代码中的代码,会把锁对象归还给同步代码块。线程B才能获取到锁对象,进入到同步中执行
同步保证了只能有一个线程可以在同步中执行共享数据,保证了安全
但是程序频繁的判断锁、获取锁、释放锁,程序的效率会降低
总结
同步中的线程没有执行完毕不会释放锁,同步外的线程没有锁不能进入同步代码块,处于阻塞状态
# 同步方法
- 直接在方法声明上使用
synchronized
,此时表示同步方法在任意时刻只能有一个线程进入 - 同步方法锁的是
this
当前对象
/**
* @Author: Mr.Q
* @Date: 2020-05-26 16:36
* @Description:同步方法
*/
class Web1230 implements Runnable {
private int tickets = 20;
@Override
public void run() {
while (tickets > 0) {
this.sale();
}
}
//此处通过同步方法加上锁
public synchronized void sale() {
//this表示当前对象
synchronized (this) {
//在此同步代方法中,只有一个线程在跑
if (tickets >= 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
System.err.println("执行线程任务时出现了异常...");
}
System.out.println(Thread.currentThread().getName() + "还剩" + tickets-- + "张票");
}
}
}
}
public class SyncMethod {
public static void main(String[] args) {
Web1230 run = new Web1230();
new Thread(run,"线程A").start();
new Thread(run,"线程B").start();
new Thread(run,"线程C").start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 静态同步方法
静态的同步方法锁对象是谁?
- 不能是this,this是创建对象之后产生的,静态方法优先于对象。
- 静态方法的锁对象是本类的class属性--->>class文件对象(反射)
我们可以理解为将同步代码块加到了方法中,此时this对象变成了类的class属性(通过反射来获取)
# 二者之间的区别
synchronized关键字可应用在方法级别 [ 粗粒度锁 ] 或者是代码块级别 [ 细粒度锁 ]
- 同步方法直接在方法上加synchronized实现加锁,锁的是类的对象;
- 同步代码块则在方法内部加锁,锁的目标更明确
很明显,同步方法锁的范围比较大,而同步代码块范围要小点。
同步的范围越大,性能就越差。一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好
# 3. 对象锁全局锁
synchronized对象锁也叫同步锁,锁的哪个对象,保护的哪个资源
synchronized说明
锁当前this对象
class Sync {
//成员方法
public synchronized void method() {
}
}
=======等价于=========
class Sync {
//当前this对象
public void method() {
synchronized (this) {
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
锁当前类对象,全局锁,锁类对应的class对象
class Sync {
//静态方法
public synchronized static void method() {
}
}
=======等价于=========
class Sync {
//静态方法
public static void method() {
//类对象
synchronized (Sync.class) {
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 案例说明
不加static,锁的是当前的this对象
- 本案例并没有锁住当前的
this
对象,synchronized
加和不加没区别 - 同理同步方法也是如此,没锁住
- 要想实现同步,采用
static
方法全局锁或者同步块锁Class
对象
class Sync implements Runnable{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
//锁的是Sync的对象
public synchronized void test() {
System.out.println("test->开始,当前线程为: " + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test-->>结束,当前线程为: " + Thread.currentThread().getName());
}
}
public class ObjectSync {
public static void main(String[] args) {
Sync mythread = new Sync();
new Thread(mythread,"A").start();
new Thread(mythread,"B").start();
new Thread(mythread,"C").start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
A,B,C三个一组出现表明:没有加static的synchronized没有锁住当前对象
通过上述代码以及运行结果我们可以发现,没有看到synchronized起到作用,结果为三个线程同时运行test
方法。
因为同步方法是粗粒度锁,实际上,synchronized(this){ 代码块上锁 }
以及非static的同步方法,只能防止多个线程同时执行同一个对象的同步代码段。即本质上是三个线程属于不同的对象,不同的对象同时执行同一个方法,synchronized锁住的是括号里的当前对象,而不是代码。所以并不会产生竞争的并发效果,简单的理解为是并行的,结果为三个一组出现。
对于非static的synchronized同步方法,锁的就是对象本身也就是this。
加上static,静态同步方法上锁,是全局锁。锁的是当前类的Class对象,类的Class对象只有一个
对比运行结果:
不加static | A,B,C三个一组的出现 |
---|---|
加上static | A,B,C三个不固定出现 |
那么此时我们在同步方法上加static关键字变成的全局锁,或者是用同步代码块来明确指定锁的目标来解决此问题...
当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。
要搞清楚
synchronized
到底锁住的是什么,最经典的就是八锁问题了。