同步锁

167 阅读4分钟

线程安全

  • 一个程序运行多个线程本身是没有问题的
  • 问题有可能出现在多个线程访问共享资源
    • 多个线程都是读共享资源也是没有问题的
    • 当多个线程读写共享资源时,如果发生指令交错,就会出现问题

临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。

线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。

如下面不安全的代码

// 对象的成员变量
private static int count = 0;

public static void main(String[] args) throws InterruptedException {
  // t1线程对变量+5000次
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            count++;
        }
    });
  // t2线程对变量-5000次
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            count--;
        }
    });

    t1.start();
    t2.start();

    // 让t1 t2都执行完
    t1.join();
    t2.join();
    System.out.println(count);
}

// 运行结果 
-1399

上面的代码 两个线程,一个+5000次,一个-5000次,如果线程安全,count的值应该还是0。

线程安全的类一定所有的操作都线程安全吗?

开发中经常会说到一些线程安全的类,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的

成员变量和静态变量是否线程安全?

  • 如果没有多线程共享,则线程安全
  • 如果存在多线程共享
    • 多线程只有读操作,则线程安全
    • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 局部变量引用的对象未必是线程安全的
    • 如果该对象没有逃离该方法的作用范围,则线程安全
    • 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全

synchronized

该关键字是用于保证线程安全的,是阻塞式的解决方案。

让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。

当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

synchronized实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断

线程安全的代码

private static int count = 0;

private static Object lock = new Object();

 // t1线程和t2对象都是对同一对象加锁。保证了线程安全。此段代码无论执行多少次,结果都是0
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (lock) {
                count++;
            }
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (lock) {
                count--;
            }
        }
    });

    t1.start();
    t2.start();

    // 让t1 t2都执行完
    t1.join();
    t2.join();
    System.out.println(count);
}

重点:加锁是加在对象上,一定要保证是同一对象,加锁才能生效

线程通信

wait+notify

线程间通信可以通过共享变量+wait()&notify()来实现

wait()将线程进入阻塞状态,notify()将线程唤醒

当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)

如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁

  1. Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
  2. Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
  3. 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
  4. 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁

注意:

  1. Blocked状态和Waitting状态都是阻塞状态

  2. Blocked线程会在owner线程释放锁时唤醒

  3. wait和notify使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常

  • wait() 释放锁 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒

  • notify()随机唤醒一个waitSet里的线程

  • notifyAll()唤醒waitSet中所有的线程

    static final Object lock = new Object(); new Thread(() -> { synchronized (lock) { log.info("开始执行"); try { // 同步代码内部才能调用 lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.info("继续执行核心逻辑"); } }, "t1").start();

    new Thread(() -> { synchronized (lock) { log.info("开始执行"); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.info("继续执行核心逻辑"); } }, "t2").start();

    try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("开始唤醒");

    synchronized (lock) { // 同步代码内部才能调用 lock.notifyAll(); } // 执行结果 14:29:47.138 [t1] INFO TestWaitNotify - 开始执行 14:29:47.141 [t2] INFO TestWaitNotify - 开始执行 14:29:49.136 [main] INFO TestWaitNotify - 开始唤醒 14:29:49.136 [t2] INFO TestWaitNotify - 继续执行核心逻辑 14:29:49.136 [t1] INFO TestWaitNotify - 继续执行核心逻辑

wait 和 sleep的区别?

二者都会让线程进入阻塞状态,有以下区别

  1. wait是Object的方法 sleep是Thread的方法
  2. wait会立即释放锁 sleep不会释放锁
  3. wait后线程的状态是Watting sleep后线程的状态为 Time_Waiting

生产者消费者模型

指的是有生产者来生产数据,消费者来消费数据,生产者生产满了就不生产了,通知消费者取,等消费了再进行生产。
1

消费者消费不到了就不消费了,通知生产者生产,生产到了再继续消费。

  public static void main(String[] args) throws InterruptedException {
        MessageQueue queue = new MessageQueue(2);

		// 三个生产者向队列里存值
        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(() -> {
                queue.put(new Message(id, "值" + id));
            }, "生产者" + i).start();
        }

        Thread.sleep(1000);

		// 一个消费者不停的从队列里取值
        new Thread(() -> {
            while (true) {
                queue.take();
            }
        }, "消费者").start();

    }
}

// 消息队列被生产者和消费者持有
class MessageQueue {
    private LinkedList<Message> list = new LinkedList<>();

    // 容量
    private int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }

    /**
     * 生产
     */
    public void put(Message message) {
        synchronized (list) {
            while (list.size() == capacity) {
                log.info("队列已满,生产者等待");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.addLast(message);
            log.info("生产消息:{}", message);
            // 生产后通知消费者
            list.notifyAll();
        }
    }

    public Message take() {
        synchronized (list) {
            while (list.isEmpty()) {
                log.info("队列已空,消费者等待");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Message message = list.removeFirst();
            log.info("消费消息:{}", message);
            // 消费后通知生产者
            list.notifyAll();
            return message;
        }
    }

}
 // 消息
class Message {

    private int id;

    private Object value;
}