线程安全
- 一个程序运行多个线程本身是没有问题的
- 问题有可能出现在多个线程访问共享资源
- 多个线程都是读共享资源也是没有问题的
- 当多个线程读写共享资源时,如果发生指令交错,就会出现问题
临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。
线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。
如下面不安全的代码
// 对象的成员变量
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()¬ify()来实现
wait()将线程进入阻塞状态,notify()将线程唤醒
当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)
如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁
- Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
- Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
- 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
- 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁
注意:
-
Blocked状态和Waitting状态都是阻塞状态
-
Blocked线程会在owner线程释放锁时唤醒
-
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的区别?
二者都会让线程进入阻塞状态,有以下区别
- wait是Object的方法 sleep是Thread的方法
- wait会立即释放锁 sleep不会释放锁
- 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;
}