2.1 JAVA锁机制--线程同步

74 阅读6分钟

一、线程同步机制

1. 并发

  • 多个线程操作同一个资源

image.png

2. 线程同步

处理多线程问题时,多个线程访问同一个对象,此时就需要线程同步,线程同步就是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。

3. 队列和锁

经典的例子:多个人想要上一个厕所,为了大家都能有序的上厕所,大家开始排队,形成了等待池。但是为了进入测速的人更安全,于时进入后需要锁好厕所门,这样排队的人就进不来了。

在访问资源时加入锁**synchronized**,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个高优先级线程等待一个低优先级线程释放锁,会导致优先级倒置,引起性能问题

二、同步方法

1. Synchronized关键字

每个对象都有一把锁,给方法加了该关键字,都必须获得调用该方法的对象的锁才能执行,否则线程阻塞,方法一旦执行就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获取这个锁。

(1) 同步方法

public synchronized void method(int args) {}

线程不安全示例:

 /**
  * @ClassName SynchronizedTest
  * @Description 线程不安全示例
  * @Author wangwk-a
  * @Date 2022/1/2 17:12
  * @Version 1.0
  */
 public class SynchronizedTest {
     public static void main(String[] args) {
         BuyTicket buyTicket = new BuyTicket();
         Thread th1 = new Thread(buyTicket, "小明");
         Thread th2 = new Thread(buyTicket, "小红");
         Thread th3 = new Thread(buyTicket, "小杨");
         Thread th4 = new Thread(buyTicket, "小王");
 ​
         th1.start();
         th2.start();
         th3.start();
         th4.start();
     }
 }
 ​
 class BuyTicket implements Runnable {
     private int ticketNums = 10;
     boolean flag = true;
     
     @Override
     public void run() {
         while (flag) {
             try {
                 bug();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
 ​
     private void bug() throws InterruptedException {
         if (ticketNums <= 0) {
             flag = false;
             return;
         }
         Thread.sleep(100);
         System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums-- + "张票");
     }
 }
 小明拿到10张票
 小杨拿到9张票
 小红拿到9张票
 小王拿到9张票
 小明拿到8张票
 小杨拿到6张票
 小红拿到6张票
 小王拿到7张票
 小杨拿到4张票
 小王拿到3张票
 小明拿到5张票
 小红拿到5张票
 小王拿到2张票
 小红拿到1张票
 小明拿到1张票
 小杨拿到2张票

修改为同步方法

 private synchronized void bug() throws InterruptedException {}
 小红拿到10张票
 小红拿到9张票
 小红拿到8张票
 小红拿到7张票
 小红拿到6张票
 小红拿到5张票
 小王拿到4张票
 小王拿到3张票
 小王拿到2张票
 小王拿到1张票

缺陷:若将大的方法声明为synchronized将会影响效率

(2) 同步块

  • Obj称为同步监视器,可以是任何对象,但是推荐使用共享资源
  • 同步方法中无需指定同步监视器,因为同步方法的同步检视器是this,就是i对象本身,或者class
  • 同步监视器执行过程:
    • 第一个线程访问,锁定同步监视器,执行其中代码
    • 第二个线程访问,发现同步监视器被锁定,无法访问
    • 第一个线程访问完毕,解锁同步监视器
    • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
synchronized(Obj) {
    // 对同步资源获取
}

不安全示例:

 /**
  * @ClassName SyncBlockTest
  * @Description 银行取钱示例--同步块
  * @Author wangwk-a
  * @Date 2022/1/2 17:30
  * @Version 1.0
  */
 public class SyncBlockTest {
     public static void main(String[] args) {
         Account account = new Account("结婚基金", 100);
         Drawing you = new Drawing(account, 50, "你");
         Drawing girlFriend = new Drawing(account, 100, "girlFriend");
 ​
         you.start();
         girlFriend.start();
     }
 }
 ​
 class Account {
     /**
      * 卡名
      */
     String name;
     /**
      * 余额
      */
     int money;
 ​
     public Account(String name, int money) {
         this.name = name;
         this.money = money;
     }
 }
 ​
 class Drawing extends Thread {
     /**
      * 账户
      */
     Account account;
     /**
      * 取了多少钱
      */
     int getMoney;
     /**
      * 现在手里有多少钱
      */
     int nowMoney;
 ​
     public Drawing(Account account, int getMoney, String name) {
         super(name);
         this.account = account;
         this.getMoney = getMoney;
     }
 ​
     /**
      * 取钱
      */
     @Override
     public void run() {
         if (account.money - getMoney < 0) {
             System.out.println(Thread.currentThread().getName() + "钱不够了,取不了");
         }
         // 模拟网络延时
         try {
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         // 可以取钱
         account.money -= getMoney;
         // 你手里的钱
         nowMoney += getMoney;
 ​
         System.out.println(account.name + "余额为:" + account.money);
         System.out.println(Thread.currentThread().getName() + "手里的钱为:" + nowMoney);
     }
 }
 结婚基金余额为:0
 girlFriend手里的钱为:100
 结婚基金余额为:-50
 你手里的钱为:50

加锁后:

 @Override
     public void run() {
         synchronized (account) {
             if (account.money - getMoney < 0) {
                 System.out.println(Thread.currentThread().getName() + "钱不够了,取不了");
                 return;
             }
             // 模拟网络延时
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             // 可以取钱
             account.money -= getMoney;
             // 你手里的钱
             nowMoney += getMoney;
 ​
             System.out.println(account.name + "余额为:" + account.money);
             System.out.println(Thread.currentThread().getName() + "手里的钱为:" + nowMoney);
         }
     }
 结婚基金余额为:50
 你手里的钱为:50
 girlFriend钱不够了,取不了

2. JUC(Java Util Concurrent)

Java并发包,有很多封装好的类是线程安全的,示例使用**CopyOnWriteArrayList**

 import java.util.concurrent.CopyOnWriteArrayList;
 ​
 /**
  * @ClassName JUCTest
  * @Description 测试JUC安全类型的集合
  * @Author wangwk-a
  * @Date 2022/1/2 17:50
  * @Version 1.0
  */
 public class JUCTest {
 ​
     public static final int THREAD_LOOP = 10000;
 ​
     public static void main(String[] args) {
         CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
         for (int i = 0; i < THREAD_LOOP; i++) {
             new Thread(()->{
                 list.add(Thread.currentThread().getName());
             }).start();
         }
 ​
         try {
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 ​
         System.out.println(list.size());
     }
 }
 10000

点开**CopyOnWriteArrayList**可以看到:

 public class CopyOnWriteArrayList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
     private static final long serialVersionUID = 8673264195747942595L;
 ​
     /** The lock protecting all mutators */
     final transient ReentrantLock lock = new ReentrantLock();
 ​
     /** The array, accessed only via getArray/setArray. */
     private transient volatile Object[] array;
     
     // ...
 }
  • 这个array使用关键字transientvolatile来修饰,表示是可序列化和保证是唯一的
  • 这里面出现了一个锁ReentrantLock是可重入锁

3. 死锁

多线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致多个线程都在等待对方释放资源,都停止执行的情形。

 /**
  * @ClassName DeadLock
  * @Description 死锁测试
  * @Author wangwk-a
  * @Date 2022/1/2 18:05
  * @Version 1.0
  */
 public class DeadLock {
     public static void main(String[] args) {
         MakeUp g1 = new MakeUp(0, "灰姑娘");
         MakeUp g2 = new MakeUp(1, "白雪公主");
 ​
         g1.start();
         g2.start();
     }
 }
 ​
 /**
  * 口红
  */
 class Lipstick {
 ​
 }
 ​
 /**
  * 镜子
  */
 class Mirror {
 ​
 }
 ​
 /**
  * 化妆类
  */
 class MakeUp extends Thread {
     /**
      * 需要的资源只有一份,用static保障
      */
     static Lipstick lipstick = new Lipstick();
     static Mirror mirror = new Mirror();
 ​
     /**
      * 选择
      */
     int choice;
     /**
      * 使用化妆品的人
      */
     String girlName;
 ​
     public MakeUp(int choice, String girlName) {
         this.choice = choice;
         this.girlName = girlName;
     }
 ​
     @Override
     public void run() {
         try {
             makeup();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 ​
     /**
      * 化妆的方法,互相持有对方的锁
      */
     private void makeup() throws InterruptedException {
         if (choice == 0) {
             synchronized (lipstick) {
                 // 获取口红的锁
                 System.out.println(this.girlName + "获得口红");
                 Thread.sleep(1000);
                 synchronized (mirror) {
                     // 1s后想获得镜子
                     System.out.println(this.girlName + "获得镜子");
                 }
             }
         } else {
             synchronized (mirror) {
                 // 获取镜子的锁
                 System.out.println(this.girlName + "获得镜子");
                 Thread.sleep(2000);
                 synchronized (lipstick) {
                     // 2s后想获得口红
                     System.out.println(this.girlName + "获得口红");
                 }
             }
         }
     }
 }
 灰姑娘获得口红
 白雪公主获得镜子
 // 程序并没有结束...

解决:不要让她们两个“拿着碗里的,看着锅里的”,让她们用完先释放,然后再获取别的锁

 private void makeup() throws InterruptedException {
     if (choice == 0) {
         synchronized (lipstick) {
             // 获取口红的锁
             System.out.println(this.girlName + "获得口红");
             Thread.sleep(1000);
         }
         synchronized (mirror) {
             // 1s后想获得镜子
             System.out.println(this.girlName + "获得镜子");
         }
     } else {
         synchronized (mirror) {
             // 获取镜子的锁
             System.out.println(this.girlName + "获得镜子");
             Thread.sleep(2000);
         }
         synchronized (lipstick) {
             // 2s后想获得口红
             System.out.println(this.girlName + "获得口红");
         }
     }
 }
 灰姑娘获得口红
 白雪公主获得镜子
 白雪公主获得口红
 灰姑娘获得镜子

死锁产生的四个条件:

  • 互斥:一个资源每次只能被一个进程使用
  • 请求与保持:线程因请求资源而阻塞时,对已获取的资源保持不放
  • 不剥夺:线程已获得的资源,在未使用完之前,不能被剥夺
  • 循环等待:若干线程形成环的循环等待资源关系

4. Lock

  • JDK5.0开始,Java提供更强大的线程同步机制,通过显式定义同步锁对象来实现同步
  • **java.util.concurrent.locks.Lock**接口是控制多个线程对共享资源访问的工具。提供对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前要先获得锁
  • **ReentrantLock**实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中常用,可以显示加锁、释放锁
 import java.util.concurrent.locks.ReentrantLock;
 ​
 /**
  * @ClassName ReentrantLockTest
  * @Description 可重入锁测试
  * @Author wangwk-a
  * @Date 2022/1/2 18:27
  * @Version 1.0
  */
 public class ReentrantLockTest {
     public static void main(String[] args) {
         MyReentrantLock myReentrantLock = new MyReentrantLock();
         new Thread(myReentrantLock).start();
         new Thread(myReentrantLock).start();
         new Thread(myReentrantLock).start();
     }
 }
 ​
 class MyReentrantLock implements Runnable{
     int ticketNums = 10;
 ​
     /**
      * 定义可重入锁
      */
     private final ReentrantLock lock = new ReentrantLock();
 ​
     @Override
     public void run() {
         while (true) {
             // 加锁
             lock.lock();
             try {
                 if (ticketNums > 0) {
                     try {
                         Thread.sleep(1000);
                         System.out.println(ticketNums--);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 } else {
                     break;
                 }
             } finally {
                 // 解锁
                 lock.unlock();
             }
         }
     }
 }
 10
 9
 8
 7
 6
 5
 4
 3
 2
 1
 ​
 Process finished with exit code 0

5. synchronizedLock对比

  • Lock是显式锁,synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先级顺序:Lock > 同步代码块 > 同步方法