Java 多线程同步-同步代码块&&同步方法

拼搏现实的明天。 2022-11-05 00:49 384阅读 0赞

我们回忆一下之前的火车票案例:

  1. package com.veeja.thread;
  2. /** * @Author veeja * 2021/3/2 11:35 */
  3. public class BuyTicketThreadExtendTest {
  4. public static void main(String[] args) {
  5. BuyTicketThreadExtend t1 = new BuyTicketThreadExtend("one");
  6. BuyTicketThreadExtend t2 = new BuyTicketThreadExtend("two");
  7. t1.start();
  8. t2.start();
  9. }
  10. }
  11. class BuyTicketThreadExtend extends Thread {
  12. /** * 总票数,为了使多个实例抢的都是这10张票,所以用static修饰 */
  13. static int ticketNumber = 10;
  14. /** * 设置线程名字的方法 * * @param name */
  15. public BuyTicketThreadExtend(String name) {
  16. setName(name);
  17. }
  18. /** * 覆写run方法 */
  19. @Override
  20. public void run() {
  21. for (int i = 0; i < 100; i++) {
  22. if (ticketNumber > 0) {
  23. System.out.println("我在" + getName() + "买到了,座号为:\t" + ticketNumber--);
  24. }
  25. }
  26. }
  27. }

如果多运行几次,总会出现一些问题,比如买了两张同样的车票,或者是买了第0张车票,甚至-1张。而且还会出现乱序的情况,车票不是一张一张卖的而是乱序售出。

例如:
在这里插入图片描述
其实这就反应了在多并发的情况下,带来的线程安全问题。

1. 同步代码块

线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须得保证用于处理共享资源的代码在任何时刻只能有一个线程访问。

Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用Synchronized关键字来修饰,被称作同步代码块,其语法格式为:

  1. synchronized(lock){
  2. 操作共享资源代码块
  3. }

上面的代码中, lock是一个锁对象﹐它是同步代码块的关键。
当线程执行同步代码块时,首先会检查锁对象的标志位。默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。
当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码。
循环往复,直到共享资源被处理完为止。

例如上面的,就需要改为:

  1. Object lock = new Object();
  2. synchronized (lock) {
  3. if (ticketNumber > 0) {
  4. System.out.println("我在" + getName() + "买到了,座号为:\t" + ticketNumber--);
  5. }
  6. }

这里要注意的是,我们的锁必须是同一把锁,如果创建了多个线程,各自都有各自的锁,那么就起不到作用了。

一般我们用本类的字节码作为锁,这样就保证了所有实例使用的是一把锁。

  1. class BuyTicketThreadExtend extends Thread {
  2. ...
  3. synchronized (BuyTicketThreadExtend.class) {
  4. ...
  5. }
  6. }

注意:
同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。所以,锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位。线程之间便不能产生同步的效果。

比如在实现Runnable接口的时候,我们可以用this来作为锁,this就是当前对象,因为线程类共用了一个对象,所以可以使用this作为锁:

  1. public class BuyTicketThreadImplementsTest {
  2. public static void main(String[] args) {
  3. BuyTicketThreadImplements tti = new BuyTicketThreadImplements();
  4. Thread t1 = new Thread(tti, "窗口1");
  5. Thread t2 = new Thread(tti, "窗口2");
  6. t1.start();
  7. t2.start();
  8. } }
  9. class BuyTicketThreadImplements implements Runnable {
  10. int ticketNumber = 10;
  11. @Override
  12. public void run() {
  13. for (int i = 0; i < 100; i++) {
  14. synchronized (this) {
  15. if (ticketNumber > 0) {
  16. System.out.println("我在" + Thread.currentThread().getName() + "买到了票,座号为" + ticketNumber--);
  17. }
  18. }
  19. }
  20. }
  21. }

但是我们在使用继承Thread的方式的时候,因为我们创建了不同的对象,所以再使用this就不是同一把锁了,就还会导致多线程安全问题。所以我们一般使用线程类的.class作为锁,

  1. public class BuyTicketThreadExtendTest {
  2. public static void main(String[] args) {
  3. BuyTicketThreadExtend t1 = new BuyTicketThreadExtend("one");
  4. BuyTicketThreadExtend t2 = new BuyTicketThreadExtend("two");
  5. t1.start();
  6. t2.start();
  7. } }
  8. class BuyTicketThreadExtend extends Thread {
  9. static int ticketNumber = 10;
  10. public BuyTicketThreadExtend(String name) {
  11. setName(name);
  12. }
  13. @Override
  14. public void run() {
  15. for (int i = 0; i < 100; i++) {
  16. synchronized (BuyTicketThreadExtend.class) {
  17. if (ticketNumber > 0) {
  18. System.out.println("我在" + getName() + "买到了,座号为:\t" + ticketNumber--);
  19. }
  20. }
  21. }
  22. } }

接下来这段话是从笔记里面看到的,说的我一脸懵,似懂非懂,先放在这里吧:

总结1:认识同步监视器(锁子)synchronized(同步监视器){ }
1)必须是引用数据类型,不能是基本数据类型
2)也可以创建一个专门的同步监视器,没有任何业务含义
3)一般使用共享资源做同步监视器即可
4)在同步代码块中不能改变同步监视器对象的引用
5)尽量不要String和包装类Integer做同步监视器
6)建议使用final修饰同步监视器
总结2:同步代码块的执行过程
1)第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)
总结3:其他
1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
2)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块

2. 同步方法

我们知道,刚才可能出现问题的代码就是抢火车票的那一段,

  1. if (ticketNumber > 0) {
  2. System.out.println("我在" + getName() + "买到了,座号为:\t" + ticketNumber--);
  3. }

我们现在把它抽取出来,形成一个方法,并且加上Synchronized关键字:

  1. public synchronized void buyTicket() {
  2. if (ticketNumber > 0) {
  3. System.out.println("我在" + Thread.currentThread().getName() + "买到了票,座号为" + ticketNumber--);
  4. }
  5. }

其实这就形成了同步方法,同步方法的语法格式如下:

  1. synchronized 返回值类型 方法名(参数1, ...){ }

synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。

上面的程序我们可以改造一下:

  1. public class BuyTicketThreadImplementsTest {
  2. public static void main(String[] args) {
  3. BuyTicketThreadImplements tti = new BuyTicketThreadImplements();
  4. Thread t1 = new Thread(tti, "窗口1");
  5. Thread t2 = new Thread(tti, "窗口2");
  6. t1.start();
  7. t2.start();
  8. }
  9. }
  10. class BuyTicketThreadImplements implements Runnable {
  11. int ticketNumber = 10;
  12. @Override
  13. public void run() {
  14. while (true) {
  15. buyTicket();
  16. if (ticketNumber <= 0) {
  17. break;
  18. }
  19. }
  20. }
  21. public synchronized void buyTicket() {
  22. if (ticketNumber > 0) {
  23. try {
  24. Thread.sleep(10);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. System.out.println("我在" + Thread.currentThread().getName() + "买到了票,座号为" + ticketNumber--);
  29. }
  30. }
  31. }

我们在上面的例子中,实现了Runnable接口的线程类通过实现同步方法是可以解决线程安全问题的,但是如果是继承Thread类的方式呢?

  1. public class BuyTicketThreadExtendTest {
  2. public static void main(String[] args) {
  3. BuyTicketThreadExtend t1 = new BuyTicketThreadExtend("one");
  4. BuyTicketThreadExtend t2 = new BuyTicketThreadExtend("two");
  5. t1.start();
  6. t2.start();
  7. }
  8. }
  9. class BuyTicketThreadExtend extends Thread {
  10. static int ticketNumber = 10;
  11. public BuyTicketThreadExtend(String name) {
  12. setName(name);
  13. }
  14. @Override
  15. public void run() {
  16. while (true) {
  17. try {
  18. sleep(10);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. buyTicket();
  23. if (ticketNumber == 0) {
  24. break;
  25. }
  26. }
  27. }
  28. public synchronized void buyTicket() {
  29. if (ticketNumber > 0) {
  30. System.out.println("我在" + getName() + "买到了,座号为:\t" + ticketNumber--);
  31. }
  32. }
  33. }

我们运行几次,其实还是有问题的:

  1. 我在one买到了,座号为: 9
  2. 我在two买到了,座号为: 10
  3. 我在two买到了,座号为: 8
  4. 我在one买到了,座号为: 7
  5. 我在two买到了,座号为: 6
  6. 我在one买到了,座号为: 5
  7. 我在two买到了,座号为: 4
  8. 我在one买到了,座号为: 3
  9. 我在two买到了,座号为: 2
  10. 我在one买到了,座号为: 1

卖出的次序是不对的。这是为什么呢?其实还是锁的问题,在继承Thread类的这种写法里面,我们的锁并不是同一把。这个时候,要在方法前加一个static修饰符,保证是同一把锁。

  1. public static synchronized void buyTicket() {
  2. if (ticketNumber > 0) {
  3. System.out.println("我买到了,座号为:\t" + ticketNumber--);
  4. }
  5. }

但这样有些限制,我们在静态方法里面只能访问静态的方法和变量,所以其他非静态的变量和方法就无法访问了。

总结:
首先,同步方法也有自己的锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。
这样做的好处是,同步方法被所有线程共享,方法所在的对象相对于所有线程来说也是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到了线程同步的效果。

但是,有时候我们的方法需要是静态方法,静态方法不需要创建对象就可以直接使用“类名.方法名”的方式调用。这个时候,我们都没有对象,那么同步方法的锁就不会是this,那是什么呢?
Java中静态方法的锁是该类所在类的class对象,该对象可以直接使用类名.class的方式获取。

关于同步方法

  1. 不要将run()定义为同步方法
  2. 非静态同步方法的同步监视器是this
    静态同步方法的同步监视器是 类名.class 字节码信息对象
  3. 同步代码块的效率要高于同步方法
    原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
  4. 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块

同步代码块和同步方法解决了多线程的问题,使得在多个线程访问共享数据时变得安全,但是同时因为每次执行到同步代码块时每次都会判断锁的状态,非常的消耗资源,效率比较低。

发表评论

表情:
评论列表 (有 0 条评论,384人围观)

还没有评论,来说两句吧...

相关阅读

    相关 线(12)同步方法同步

    在并发编程中,同步方法和同步块是两种常用的同步机制,它们用以确保多线程环境下对共享资源的访问是安全的,防止数据的不一致性和竞态条件的产生。本文将详细探讨这两种同步机制的工...

    相关 Java线线同步-同步

    Java线程:线程的同步-同步块   对于同步,除了同步方法外,还可以使用同步代码块,有时候同步代码块会带来比同步方法更好的效果。   追其同步的根本的目的,是控制竞