介绍
多个线程对共享资源的进行读写操作的时候,由于cpu指令执行的顺序不同,导致每次的结果可能不一样。为了解决这一问题,可以用加锁的方式解决。
临界区和竞态条件
临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
-
synchronized加锁
synchronized
让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,保证了临界区代码的原子性。
可以做如下类比: sychronized(对象)
中的对象好比是房间,每个房间只有一把钥匙,多个线程相当于人,人需要有钥匙(持有锁)才能进入房间。- 当线程t1执行临界区时,相当于第一个人拿到了钥匙进入房间做事情。
- 当线程t2也运行到了临界区时,相当于第二个人来到房间门口,由于没有钥匙,只能阻塞住。
- 如果在这中间线程t1的cpu时间片不幸用完,它会被踢出门外(不要以为持有了锁,它就会一直执行下去),t1仍然拿着钥匙,下次t1再次被cpu分配到时间片时,它继续开门进入工作。
- 当t1执行完sychronized的代码,它会开门,交出钥匙,同时唤醒其他阻塞的线程,让他们竞争钥匙,进门工作。
synchronized实践
synchronized加在方法上,相当于对当前对象加锁,synchronized加在静态方法上,相当于对类对象加锁。进行synchronized的加锁分析时,主要看不同的线程是否持有同一把锁,或者拿上面的比方来说,是不是进入同一个房间。如果是类对象,那么一个类就是一个房间,对象的话,一个对象是同一个房间。synchronized加在方法上
```shell public class SychronizedTest { public static void main(String[] args) {
} }Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
@Slf4j class Number{ @SneakyThrows public synchronized void a() { log.debug(“a start……”); Thread.sleep(1000); log.debug(“a end……”); } @SneakyThrows public synchronized void b() { log.debug(“b start……”); Thread.sleep(1000); log.debug(“b end……”); } }
**结果:**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/384158/1643027706344-c464d932-982e-4e32-8fbf-36da50acdee6.png#clientId=uc4a4bccc-f9e9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=151&id=uc11b4afc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=302&originWidth=1314&originalType=binary&ratio=1&rotation=0&showTitle=false&size=169330&status=done&style=none&taskId=u3a0ffc38-d81c-4a91-b4e6-d2051cad989&title=&width=657)<br />synchronized加在方法上相当于对当前这个对象加锁,他们想要进的是同一个房间(同一把锁)。
<a name="siGTz"></a>
## synchronized加在其中一个方法上
```shell
public class SychronizedTest2 {
public static void main(String[] args) {
Number2 n1 = new Number2();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
}
@Slf4j
class Number2 {
@SneakyThrows
public synchronized void a() {
log.debug("a start......");
Thread.sleep(1000);
log.debug("a end......");
}
@SneakyThrows
public void b() {
log.debug("b start......");
Thread.sleep(1000);
log.debug("b end......");
}
}
结果:
方法a加了锁,方法b未加锁,所以两个线程没有持有同一个锁,所以不会阻塞。
synchronized加在不同锁上
public class SychronizedTest3 {
public static void main(String[] args) {
Number3 n1 = new Number3();
Number3 n2 = new Number3();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
}
@Slf4j
class Number3 {
@SneakyThrows
public synchronized void a() {
log.debug("a start......");
Thread.sleep(1000);
log.debug("a end......");
}
@SneakyThrows
public synchronized void b() {
log.debug("b start......");
Thread.sleep(1000);
log.debug("b end......");
}
}
结果:
本质上,两个线程是在不同的两个锁,也就是说进入的两个不同的房间,所以不会阻塞。
synchronized加在静态方法上
public class SychronizedTest4 {
public static void main(String[] args) {
Number4 n1 = new Number4();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
}
@Slf4j
class Number4 {
@SneakyThrows
public synchronized static void a() {
log.debug("a start......");
Thread.sleep(1000);
log.debug("a end......");
}
@SneakyThrows
public synchronized void b() {
log.debug("b start......");
Thread.sleep(1000);
log.debug("b end......");
}
}
结果:
synchronized加在静态方法上,相当于对这个类对象加锁,和加在方法上不是同一个锁,他们进入的不是同一个房间,所以不会阻塞。
synchronized全加在静态方法上
public class SychronizedTest5 {
public static void main(String[] args) {
Number5 n1 = new Number5();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
}
@Slf4j
class Number5 {
@SneakyThrows
public synchronized static void a() {
log.debug("a start......");
Thread.sleep(1000);
log.debug("a end......");
}
@SneakyThrows
public synchronized static void b() {
log.debug("b start......");
Thread.sleep(1000);
log.debug("b end......");
}
}
结果:
两个线程加的是同一个锁,这个Numer4的这个类对象的锁,所以会阻塞。
变量的线程安全分析
通过加锁synchronized
让线程安全,那什么情况下需要加锁,换句话说,什么情况下变量会线程不安全呢?
成员变量和静态变量是否线程安全?
- 如果他们没有共享,则线程安全
如果他们被共享了
局部变量是线程安全的
- 局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全。
1.