目录
一、volatile关键字
1.volatile能保证内存的可见性
- 下面的代码不会输出r,虽然我们在主线程中把 quit 改成了 true ,但是子线程实际上是不知道的,就会一直在whlie循环中
import java.util.concurrent.TimeUnit;
/**
* @author happy
*/
public class Main {
static boolean quit = false;
static class MyThread extends Thread {
@Override
public void run() {
long r = 0;
while (quit == false) {
r++;
}
System.out.println(r);
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(5);
quit = true;
}
}
- 在定义quit时加上volatile修饰,等待5s之后就会输出一个数字r
volatile static boolean quit = false;
最好把在代码中经常需要改变的值使用volatile修饰
2.volatitle也可以保证原子性
3.volatitle可以保证代码重排序
比如对于实例化一个对象
正确的顺序是1 -> 2 -> 3
但是在实际中可能会重排序成1 -> 3 -> 2,此时这个对象是一个没有正确初始化的对象
使用volatitle可以确保顺序是正确的,对象一定是正确初始化的
二、wait();和notify();
线程和线程之间需要互相唤醒、通知
1.wait();和notify();的一些知识点
- 要先对对象进行synchronized加锁 ,才能使用wait()和notify()
- notify是随机唤醒的
- wait()和notify()都是属于Object类,Java中的对象都有这两个方法
-
wait()方法会先释放锁,然后等待,最后再加锁
public class Wait_Demo {
static class MyThread extends Thread {
private Object o;
MyThread(Object o) {
this.o = o;
}
@Override
public void run() {
try {
// 休眠5s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 要使用notify()方法,必须要加锁
synchronized (o) {
// 主线程持有锁,按照我们之前的想法子线程是不能加锁的
// 但是实际上,wait()方法会先释放锁,然后等待,最后再加锁
System.out.println("唤醒主线程"); // 输出
o.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
MyThread t = new MyThread(o);
t.start();
// wait()方法也需要加锁
synchronized (o) {
o.wait(); // wait的时候先释放o这个锁,然后等待,最后再次加锁
System.out.println("永远不会到达"); // 输出
}
}
}
- 线程持有多个锁的时候,是哪个线程执行了wait方法,就是释放了哪个线程的锁
public class Wait_Demo2 {
static Object lock1 = new Object();
static Object lock2 = new Object();
static Object lock3 = new Object();
static class MyThread extends Thread {
@Override
public void run() {
synchronized (lock3) {
// 此时是可以输出成功的
// 因为lock3的锁已经被释放了
System.out.println("lock3 成功");
}
synchronized (lock1){
// 此时是输出失败的
// 并没有释放lock1的锁
System.out.println("lock1");
}
}
}
public static void main(String[] args) throws InterruptedException {
synchronized (lock1) {
synchronized (lock2) {
synchronized (lock3) {
MyThread t = new MyThread();
t.start();
// 主线程持有 3 把锁
// 因为是lock3 执行了wait方法,所以只会释放lock3的锁
lock3.wait(); // 释放 lock3 锁
}
}
}
}
}
三、条件变量Condition
功能和wait()、notift()是一样的
也是需要加锁的
四、单例模式
通过代码保护一个类,使得类在整个进程中只有一个对象
单例模式有两种典型实现:饿汉和懒汉
1.饿汉模式
先创建实例,不管需不需要使用,这个instance对应的实例就是该类的唯一实例
public class StarvingMode {
// 是线程安全的
// 类加载的时候执行
// JVM 保证了类加载的过程是线程安全的
private static StarvingMode instance = new StarvingMode();
public static StarvingMode getInstance() {
return instance;
}
private StarvingMode() {}
}
2.懒汉模式
先不着急去创建实例,等使用的时候再创建
public class LazyMode {
private static LazyMode instance = null;
public static LazyMode getInstance() {
// 第一次调用这个方法时,说明我们应该实例化对象了
// 要保证的是整个getInstance的原子性
// if (instance == null) {
// synchronized (LazyMode.class) {
// instance = new LazyMode();
// }
// }
// return instance;
// 上面这种加锁的方式不能保证整个getInstance的原子性
// 如果第一个线程A抢到锁,此时instance = null,线程A就对LazyMode类来进行实例化,
// 线程A实例化完之后突然发生了线程调度
// 线程B抢到了锁,此时判断的时候instance是不为空的,
// 已经有一个线程对LazyMode类来进行实例化
// 如果return instance的话就不能保证原子性
if (instance == null) {
synchronized (LazyMode.class) {
if (instance == null) {
// 再次判断instance是否为null
// 就算别的线程抢到了锁,此时该线程也不能进行初始化
instance = new LazyMode(); // 只在第一次的时候执行
}
}
}
return instance;
}
private LazyMode() {}
}
四、阻塞队列
1.阻塞队列是先进先出规则的队列
阻塞队列也是线程安全、产生阻塞效果的
- 如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止
- 如果队列已满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止
2.BlockingQueue的一些常见方法
2.1put()方法
2.2take()方法
2.3offer()方法
3.生产者-消费者模型
生产者:一个/多个线程,只负责向队列中放入元素
消费者:一个/多个线程,只负责从队列中取出元素
1.实现一个阻塞队列---一个生产者和一个消费者
public class MyArrayBlockingQueue {
private long[] array;
private int frontIndex; // 永远在队列的第一个元素位置
private int rearIndex; // 永远在队列的最后一个的下一个位置
private int size;
public MyArrayBlockingQueue(int capacity) {
array = new long[capacity];
frontIndex = 0;
rearIndex = 0;
size = 0;
}
public synchronized void put(long e) throws InterruptedException {
// 判断队列是否已经满了,考虑到有假唤醒的情况
while (array.length == size) {
this.wait();
}
// 预期:队列一定不是满的
array[rearIndex] = e;
rearIndex++;
if (rearIndex == array.length) {
rearIndex = 0;
}
size++;
notify();
}
public synchronized long take() throws InterruptedException {
while (size == 0) {
wait();
}
long e = array[frontIndex];
frontIndex++;
if (frontIndex == array.length) {
frontIndex = 0;
}
size--;
notify();
return e;
}
}
2.测试1
- 程序开始执行的时候没有出现输出,因为主线程需要queue.take(),子线程在等待我们输入一个数字来放进队列中,两个线程都保持了僵持的状态;
- 当获取到我们的输入时,子线程就把我们输入的数字存入队列,这样主线程就可以从队列中取到我们输入的这个元素,此时会输出 我们刚才输入的数字
import java.util.Scanner;
public class Main1 {
static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);
static class MyThread extends Thread {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
long e = scanner.nextLong();
try {
queue.put(e);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
long take = queue.take();
System.out.println(take);
}
}
3.测试2
- 程序开始执行的时候也没有出现输出,因为我们初始化队列的时候规定只能放3个元素,但是主线程要放4个元素,queue.put(4)的时候会阻塞,主线程需要等待子线程从队列中取出一个元素才可以把4放入队列,子线程在等待我们输入一个数才会从队列中取出一个元素,子线程和主线程也保持僵持的状态;
- 当获取到我们的输入时,子线程就从队列中取出一个元素,这样主线程就可以把4放入队列,此时会输出 “4 被放入队列中”
import java.util.Scanner;
public class Main2 {
static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);
static class MyThread extends Thread {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
long e = scanner.nextLong();
try {
queue.take();
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
queue.put(1);
queue.put(2);
queue.put(3);
queue.put(4); // 开始在queue中我们只让可以放3个,放第四个的时候会阻塞
System.out.println("4 被放入队列中");//当获取到输入的时候,子线程就会从队列中取出一个,然后“4”就可以放进队列中
}
}
4.注意
如果是多个生产者和多个消费者的话,上面实现的代码会出错
- 我们想要的是生产者唤醒消费者;消费者唤醒生产者
- 等待的线程中有生产者也有消费者,但是notify是随机唤醒的极端情况下,一个消费者把剩余的消费者全部唤醒了,size就变成了0,然后消费者继续去消费就会导致全部的消费者都进入等待,此时,所有的生产者和消费者都进入了等待,没有线程来唤醒生产者和消费者了
那么我们可以把所有的线程都唤醒 ,但是性能不太好
当消费者唤醒消费者的时候,由于while(size == 0),消费者就会继续等待;只有唤醒生产者才会继续执行