1.wait和notify(都需要搭配synchronized使用)
join(),哪个线程调用这个方法,哪个线程就堵塞
wait和notify都是Object的方法,随便定义一个对象都可以使用
wait():
执行之前要做三件事:
1.释放当前的锁
2.让线程进入阻塞状态
3.当线程被唤醒的时候,重新获取到锁。
//wait()
public class Demo22 {
public static void main(String[] args) throws InterruptedException {
// Object object = new Object();
// System.out.println("wait之前:");
// object.wait();//释放锁的前提是得有锁,不然会产生不合法监视器状态异常
// System.out.println("wait之后: ");
Object object = new Object();
System.out.println("wait之前:");
synchronized(object) {
object.wait();
//把 wait 要放到 synchronized 里面来调用. 保证确实是拿到锁了的.
//wait() 先释放锁,再让线程进入阻塞状态,最后等待唤醒重新获取到锁
}
System.out.println("wait之后: ");
}
}
wait和notify():可以用来避免"线程饿死"
package thread;
public class Demo23 {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("wait之前:");
try {
object.wait(1000);//可以添加等待的时间
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("wait之后: ");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object) {//可重复锁
System.out.println("进行通知:");
object.notify();//唤醒,如果有多个线程需要唤醒,可以用notifyAll(),不过notify更为可控,唤醒之后,各个线程重新获取锁的过程是串行执行的
}
});
t1.start();
t2.start();
}
}
2.单例模式:
常见的设计方案:
1)单例模式
2)工厂模式
单例:单个实例(对象)
有些场景之下只能有一个对象:比如娶老婆,生孩子就不一定是单例的。
很多用于管理数据的对象应该是“单例的” : MySQL JDBC DataSource (描述了mysql服务器的位置)
然后咱们就会想,那为啥会专门有一个设计模式?
我写代码的时候只给这个类new一次对象不就可以了,但不是每一个人都这样想,于是我们就需要让编译器帮我们监督(出现多个对象时就会报错)。比如像final,interface,@override,throws
单例模式有两种:饿汉模式和懒汉模式
饿汉模式:在类加载的时候就会创建实例对象
懒汉模式:在第一次调用getInstance的时候才会创建实例对象
eg:文本编辑器(记事本)
打开一个10G的大文件,存在两种形式
1.先把所有的内容都加载到内存中,然后再显示内容(加载速度慢)------->饿汉
2.只加载一小部分数据到内存,立即显示内容,随着用户翻页,再动态加载其它内容。------>懒汉
//饿汉模式:类一加载就会创建实例
class Singleton {
private static Singleton instance = new Singleton();//当类被加载的时候就会创建实例
private Singleton() {
//将构造方法设为私有,类外面的代码都无法new出多个对象。
}
public static Singleton getInstance() {
//通过这个方法可以获取到刚才创建的实例
//后续想获取到这个实例,都通过getInstance()方法获取
return instance;//
}
}
public class Demo24 {
//单例模式
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);//两个其实是对同一个对象进行操作
// Singleton s3 = new Singleton();//上面已有一个实例,出现报错
}
}
//懒汉模式:当需要实例(第一次使用getinstance)的时候再去创建
class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
单例模式是否是线程安全的?
饿汉模式是线程安全的,只进行了读取instance这一个变量;
懒汉模式是不安全的,既读取而且还修改变量,可能会出现线程不安全问题。
那么该如何加锁呢?
那么该如何实现线程安全的同时又能不对执行效率产生影响呢?
package thread;
//懒汉模式:当需要实例(第一次使用getinstance)的时候再去创建
class SingletonLazy {
private static volatile SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if(instance == null) {//这个if是用来判断是否需要加锁
synchronized(SingletonLazy.class) {
if(instance == null) {//这个if是用来判断是都需要new对象
instance = new SingletonLazy();
}
}
}
return instance;
}
}
public class Demo25 {
public static void main(String[] args) {
}
}
指令的重排序(编译器的优化):保证逻辑不变,调正原有代码的执行顺序,提高程序的执行效率
Question:t1执行到new的过程中,已经加锁,t2还能执行吗?t2还能穿插进来吗?(以上述懒汉模式代码为例)
t2执行的第一个if,并未涉及到加锁的操作,是可以执行的。
锁的阻塞等待一定是两个线程都加锁的时候触发。
由于t2并未满足第一个if的条件,没进入if的内部,没加锁,直接返回instance。
New对象这个操作可能会触发指令重排序
new可拆分成三步:
1.申请内存空间
2.在内存空间上构造对象(构造方法)
3.把内存地址赋值给instance引用
执行顺序可为123或132,单线程下无影响,多线程可能有。
假设按照132执行:
当t1线程执行完1和3时,此时instance已经是非空,instance指向的是一个还未初始化的非法对象。此时还未执行2.
t2线程突然开始执行,t2判定 instance == null, 此时不成立,直接将return instance。
进一步t2线程可能会访问instance里面的属性和方法,此时就很容易出现问题(与上面还未初始化的非法对象)。
指令重排序的解决:用volatile关键字
单例模式是一个慢慢完善的过程,不是一下就能写好的!!!!
3.阻塞队列:多线程代码中经常用到的一种数据结构
特性:
1)线程安全
2)带有阻塞特性
a)如果队列为空,继续出队列,就会发生阻塞。阻塞到其他线程往队里添加元素为止。
b)如果队列为满,继续入队列,就会发生阻塞。阻塞到其它线程从队列里取走元素为止。
意义:可以用来实现“生产者消费者模型”
举例:
过年很多人一起包饺子(多个线程一起工作,比单线程效率高),由于擀面杖只有一个,所以专门有一个人负责擀面皮。
于是擀面皮的那个人就是生产者,然后把擀的面皮放在筛子(阻塞队列)里,拿面皮包饺子的人叫消费者。
生产者把生产出来的内容,放到阻塞队列中,消费者从队列中获取内容。
当擀面皮的人生产慢了,拿面皮的人就得等(队列为空,阻塞);
当擀面皮的人生产快了,擀面的人就得等,筛子装不下了。(队列为满,阻塞)
生产者消费者模型的意义:
1.解耦合:两个模块联系越紧密,耦合越高。(对于分布式系统,更加有意义)
2.削峰填谷
峰:短时间内请求量比较多
谷:短时间内请求量比较少
三峡大坝的原理:
当上游水量增加,大坝关闸蓄水,承担压力,往下游有节奏放水;
当上游水量少了,大坝开闸放水,给下游提供水。
阻塞队列的使用:
1.java标准库已经提供现成阻塞队列
2.标准库里,针对BlockingQueue提供了两种实现方式:
1)数组
2)链表
3.BlockingQueue虽然继承自Queue,Queue提供的方法都可以用,但这些方法都不具备“阻塞”特性
package thread;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
//阻塞队列
public class Demo26 {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
queue.put("111");//阻塞式入队列
queue.put("nb");
queue.put("lihai");
queue.put("111");
String elem = queue.take();//阻塞式出队列
System.out.println(elem);
elem = queue.take();//阻塞式出队列
System.out.println(elem);
elem = queue.take();//阻塞式出队列
System.out.println(elem);
}
}
自己实现一个阻塞队列:普通队列+线程安全(加锁)+阻塞(wait和notify)
基于数组实现——>环形队列
package thread;
//阻塞队列的实现
class MyBlockingQueue {
//最大长度也可指定构造方法,由构造方法的参数决定
String[] data = new String[100];
private volatile int head = 0;//队列的起始位置
private volatile int tail = 0;//队列结束位置的下一个位置
private volatile int size = 0;//队列中有效元素的个数
// Object locker = new Object();//可用this,也可用locker
public void put(String elem) throws InterruptedException {
synchronized (this) {//加锁
while (size == data.length) {
//队列满了,继续插入元素,应该阻塞
this.wait();
}
//队列未满,添加元素
data[tail] = elem;
tail++;
size++;
if (tail == data.length) {
//此时,如果tail已经到了数组末尾,让tail回到开头(环形队列)
tail = 0;
}
this.notify();//唤醒take中的wait
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (size == 0) {
//如果队列为空,继续取元素,阻塞
this.wait();
}
//队列不为空
String tmp = data[head];//返回队头元素
head++;//删除队头元素
size--;
if (head == data.length) {
head = 0;//到了数组末尾,从0开始
}
this.notify();//唤醒put中的wait
return tmp;
}
}
}
public class Demo27 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
//生产者,消费者,分别使用一个线程表示
//消费者
Thread t1 = new Thread(() -> {
while(true) {
try {
String ret = queue.take();
System.out.println("消费:"+ret);
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//生产者
Thread t2 = new Thread(() -> {
int num = 1;
while(true) {
try {
queue.put(num + "");
System.out.println("生产:"+ num);
num++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
当wait被唤醒之后,队列一定是不满的吗?
不一定。除了notify,还有其它唤醒方式——> interrupt方法 ->会出现InterruptedException
于是wait唤醒之后,我们得循环进行多次判断,确认队列不满,才可往后执行;如果满,那就继续wait