介绍
多个线程对共享资源的进行读写操作的时候,由于cpu指令执行的顺序不同,导致每次的结果可能不一样。为了解决这一问题,可以用加锁的方式解决。
临界区和竞态条件
临界区: 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
竞态条件: 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized加锁
synchronized
让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,保证了临界区代码的原子性。
可以做如下类比:
synchronized(对象)
中的对象好比是房间,每个房间只有一把钥匙,多个线程相当于人,人需要有钥匙(持有锁)才能进入房间。- 当线程t1执行临界区时,相当于第一个人拿到了钥匙进入房间做事情。
- 当线程t2也运行到了临界区时,相当于第二个人来到房间门口,由于没有钥匙,只能阻塞住。
- 如果在这中间线程t1的cpu时间片不幸用完,它会被踢出门外(不要以为持有了锁,它就会一直执行下去),t1仍然拿着钥匙,下次t1再次被cpu分配到时间片时,它继续开门进入工作。
- 当t1执行完sychronized的代码,它会开门,交出钥匙,同时唤醒其他阻塞的线程,让他们竞争钥匙,进门工作。
synchronized实践
synchronized加在方法上,相当于对当前对象加锁,synchronized加在静态方法上,相当于对类对象加锁。进行synchronized的加锁分析时,主要看不同的线程是否持有同一把锁,或者拿上面的比方来说,是不是进入同一个房间。如果是类对象,那么一个类就是一个房间,对象的话,一个对象是同一个房间。
synchronized加在方法上
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......"); } }
结果:
synchronized加在方法上相当于对当前这个对象加锁,他们想要进的是同一个房间(同一把锁)。
synchronized加在其中一个方法上
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的这个类对象的锁,所以会阻塞。