作用
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。
通俗来说,能够保证同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized是Java的关键字,被Java原生支持,是最基本的互斥同步手段。
线程不安全的例子
1 | private void q4() { |
synchronized两个用法
对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己制定锁对象)。
- 同步代码块锁
1
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
33private void q4() {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
Logger.d("gxh", "执行完毕");
}
class MyRunnable implements Runnable {
@Override
public void run() {
synchronized (this) {
Logger.d("gxh", "我是对象锁代码块形式,线程为:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Logger.d("gxh", Thread.currentThread().getName() + "运行结束");
}
}
}
1 | 05-07 10:53:28.894 14503-14545/cn.gxh.view E/gxh: 我是对象锁代码块形式,线程为:Thread-3 |
可以看到使用同步代码块保证了线程安全,也就是同一时间只有一个线程在访问这段代码。那么锁对象该写什么呢?这里我们写的是this,我们也可以创建一个锁对象。效果是一样的。
1 | class MyRunnable implements Runnable { |
如果业务逻辑很复杂,我们需要有多个同步代码块,但是这几个同步代码块又不是依次执行的,那么会使用不同的锁对象。
1 | 05-07 11:00:40.797 14997-15043/cn.gxh.view E/gxh: 我是lock1锁代码块形式,线程为:Thread-2 |
- 方法锁
1 | private void q4() { |
1 | 05-07 13:37:09.624 23278-23351/cn.gxh.view E/gxh: 我是对象锁的方法修饰符形式,线程为:Thread-2 |
类锁
指synchronized修饰静态的方法或指定锁为Class对象。类锁的本质是Class对象的锁。类锁只能在同一时刻被一个对象拥有。
- synchronized加在static方法上
为什么synchronized和static一起使用呢?跟上面的普通方法锁有什么区别吗?肯定是有区别的。我们上面两个线程访问的method()是同一个对象的,那么试想一下,如果两个线程访问的method()不是同一个对象的呢?还会是同样的效果吗?
1 | private void q4() { |
1 | 05-07 16:05:01.279 31773-31819/cn.gxh.view E/gxh: 我是对象锁的方法修饰符形式,线程为:Thread-3 |
很显然,是线程不安全的。假如我还想让同一时间只有一个线程执行这个方法,怎么办?那就得下面这种写法啦。
1 | static class MyRunnable1 implements Runnable{ |
1 | 05-07 16:08:47.902 32076-32117/cn.gxh.view E/gxh: 我是对象锁的方法修饰符形式,线程为:Thread-2 |
- .class
还记得吗?上面说的同步代码块,我们用的锁对象是this、或者自己创建的对象。现在我们要用*.class了。
多线程访问同步方法的7种情况
- 多个线程同时访问一个对象的同步普通方法
锁生效,多个线程会串行。
- 多个线程访问多个对象的同步普通方法
锁不生效,多个线程并不会串行。因为锁的还是各自实例对象。
- 多个线程访问的是synchronized的静态方法
锁生效,多个线程会串行。
- 访问同步方法和非同步方法
非同步方法不受影响。
- 访问同一个对象的不同的同步普通方法
因为锁的还是同一个实例对象,所以这几个方法不会同时被执行,多个线程还是串行的。
- 同时访问静态的synchronized方法和非静态的synchronized方法
一个线程访问静态的synchronized方法,另一个线程访问非静态的synchronized方法。这两个线程并不会串行,而是并行的,因为这两个方法的锁是不一样。
- 方法抛异常后,会释放锁
注意点
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待
- 每个实例都对应有自己的一把锁,不同实例之间互不影响(锁对象是*.class以及synchronized修饰的static方法时,所有对象共用同一把类锁)
- 无论是方法正常执行完毕或者方法抛出异常,都会释放锁
synchronized的性质
可重入
支持重进入,该锁能够支持一个线程对资源的重复加载。通俗来说,就是一个线程获得了锁之后仍能连续多次地获得该锁而不会被该锁阻塞。
注意点
- 同一个方法是可重入的
1 | private void q5(){ |
- 可重入不要求是同一个方法
1 | private void q5(){ |
1 | 05-08 09:58:21.267 24919-24919/cn.gxh.view E/gxh: method4 |
- 可重入不要求是同一个类中的
1 | class Father { |
1 | 05-08 10:13:31.762 26285-26285/cn.gxh.view E/gxh: Child |
不可中断
一旦这个锁被别的线程获得了,如果我还想获得,我只能选择等待或者阻塞,知道别的线程释放这个锁。如果别的线程永远不释放锁,那我只能等下去。
原理
实现原理
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
可重入原理
- JVM负责跟踪对象被加锁的次数;
- 有个monitor计数器,线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时,计数会递增。
- 任务结束离开,则会执行monitorexit,计数递减,直到完全释放。(执行一次monitorexit,计数减1,直到计数为0则完全释放)
可见性原理
缺陷
- 效率低 : 锁的释放情况少,试图获得锁时不能设定超时,不能中断一个试图获得锁的线程
- 不够灵活(读写锁是比较灵活的):加锁和释放的时机比较单一
- 无法知道是否成功获取到锁
注意事项
- 锁对象不能为空
- 作用域不宜过大
- 避免死锁
- 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
- synchronizedshide 使得同时只有一个线程可以执行,性能较差,有哪些办法提升性能?