volatile关键字
volatile禁止了编译器优化,还可以禁止指令重排序,避免了直接读取CPU寄存器中的缓存数据,而是每次都重新读内存。
因为编译器每次正常执行都是把主内存的数据加载到工作内存中,再进行计算处理。volatile即使保证每次读取内存都是真正从主内存中读取的。(工作内存并不是真正的内存,主内存才是真正的内存)
例如可以用于禁止编译器优化,从而处理之前JAVAEE多线程2中编译器优化而导致的线程调用优化而导致的进程结果运行错误。
public class L107 {
static class Counter{
volatile public int count = 0;
}
public static void main(String[] args){
Counter counter = new Counter();
Thread t = new Thread(()->{
while (counter.count == 0){
}
});
t.start();
Thread t2 = new Thread(()->{
System.out.println("请输入一个数字");
Scanner scanner = new Scanner(System.in);
counter.count=scanner.nextInt();
});
t2.start();
}
}
这个时候进程可以正常退出。
tip:要解决上述问题,根源在于短时间多次重复读取不变的数据而导致的编译器优化。因此要解决的话也可以从短时间这个方面入手,去增加每次读取间隔。
public class L107 {
static class Counter{
public int count = 0;
}
public static void main(String[] args){
Counter counter = new Counter();
Thread t = new Thread(()->{
while (counter.count == 0){
try {
Thread.sleep(1000);
//在线程进行休眠,间隔一秒读取一次内存,避开编译器自动优化
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("执行结束");
});
t.start();
Thread t2 = new Thread(()->{
System.out.println("请输入一个数字");
Scanner scanner = new Scanner(System.in);
counter.count=scanner.nextInt();
});
t2.start();
}
}
volatile起到的效果是保证内存可见性,并不保存原子性。
针对一个线程读,一个线程修改,这个场景是合适的。如果是两个线程修改则无能为力了。
如下图
package threading;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 14:56
*/
public class L1071 {
static class Counter{
volatile public int count = 0;
public void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
即使修饰了count,面对t1和t2两个线程的加和仍然算不到预期的十万这个结果。
wait和notify来调配线程执行顺序
wait操作做了三件事情:
1.释放当前锁。(得先加上锁才能谈释放,因此wait和加锁操作密不可分)
2.进行等待通知。
3.满足一定条件的时候(别人调用notify),被唤醒,然后尝试重新获取锁。
notify也要包含在synchronized里面,因为线程1没有释放锁的话,线程2也就无法调用到notify(因为锁阻塞等待),线程1调用wait,在wait里面就释放锁了,这个时候虽然线程1阻塞synchronized里面,但是线程1的锁已经释放了。
package threading;
import java.util.Objects;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 15:15
*/
public class L1072 {
public static void main(String[] args) {
Object object = new Object();
System.out.println("wait之前");
synchronized (object){
try {
object.wait();
//wait是object的子类
//
//线程执行到wait就会发生阻塞,直到另一个线程调用notify把这个wait唤醒才会继续往下走。
//在wait里面执行的这个时候的锁已经解锁了,其他线程可以正常获取到这个锁
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("wait之后");
}
}
如果有t1 t2两个线程
t1加锁后,在执行部分逻辑到wait时。(t2也想加锁但是要等t1执行wait解锁)
wait解开t1的锁,t2再进行加锁,执行部分逻辑直到notify。
然后跳转到t1的wai阻塞t部分,唤醒t1并重新加锁
可能直到t2继续执行,并成功解锁后t1才会加锁成功。
注意:
加锁的对象和调用wait的对象需要是同一个对象。
wait的对象和notify的对象也得是同一个对象
如下示例:
package threading;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 15:50
*/
public class L1073 {
public static void main(String[] args) {
// 专门准备一个对象,保证和等待通知的是同一个对象
Object object = new Object();
//第一个线程进行wait
Thread t1 = new Thread(()->{
while (true){
synchronized (object){
System.out.println("wait之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//这里写的代码一定是在notify之后执行的
System.out.println("wait之后");
}
}
});
t1.start();
//第二个i=线程进行notify
Thread t2 = new Thread(()->{
while (true){
synchronized (object){
System.out.println("notify之前");
//这里写的代码一定是在wait唤醒之前执行的
object.notify();
System.out.println("notify之后");
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t2.start();
}
}
如果t2先执行了notify,t1后执行了wait,就直接错过了。调用Notify不会有副作用,就会一直等下去。
还有一个notifyAll 可以唤醒所有的wait,唤醒的wait需要重新竞争锁。过程仍然是串行的
多线程案例
一
单例模式
单个实例instance对象,某个类有且只有一个实例(为需求决定的)
当属性变成类属性的时候,此时就已经是”单个实例“
import java.security.Signature;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 17:01
*/
//把这个类作为单例类,要求里面只有一个实例
class Singleton{
private static Singleton instance = new Singleton();
//此时instance就是这个类唯一实例
public static Singleton getInstance(){
return instance;
}
//把构造方法设为private,此时在类外面,就无法继续new实例了。
private Singleton() {
}
}
public class L1074 {
public static void main(String[] args) {
//如果代码这么写就违背了初衷,应该禁止这个类在类外部被new。
//Singleton instance2 = new Singleton();
//因此需要把private设置为私有的
Singleton instance = Singleton.getInstance();
}
}
懒汉模式
package threading;
import java.security.Signature;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 17:15
*/
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
//在这里真正创建实例,首次调用getinstance的时候触发
//后续调用的时候instance不为空直接返回。
}
return instance;
}
}
public class L1075 {
public static void main(String[] args) {
SingletonLazy instance = SingletonLazy.getInstance();
}
}
单例模式只涉及读,而懒汉模式涉及到修改,相较于单例模式更加不安全。
要怎么让上述代码保证线程安全呢,就需要通过加锁把多个操作打包成一个原子操作。
package threading;
import java.security.Signature;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 17:15
*/
class SingletonLazy{
private volatile static SingletonLazy instance = null;
//加上violatile来禁止指令重排序
public static SingletonLazy getInstance(){
if(instance == null ){
//这个if判定是否要加锁。
synchronized (SingletonLazy.class){
//在最外层给最开始的加载数据就加锁,但是当线程安全的时候,无法避免还是要进行加锁这个操作,稍显繁琐。因此还可以加一层判断条件
if(instance == null){
//这个if判断是否要创建实例。
instance = new SingletonLazy();
}
//在这里真正创建实例,首次调用getinstance的时候触发
//后续调用的时候instance不为空直接返回。
}
}
return instance;
}
}
public class L1075 {
public static void main(String[] args) {
SingletonLazy instance = SingletonLazy.getInstance();
}
}
但是还是存在一个问题,有两线程同时调用了getinstance(),第一个线程拿到锁,进入第二层if开始new对象,new操作本身可以粗略分为三个步骤:
1.申请内存
2.调用构造方法,来初始化实例
3.把内存首地址赋值给instance引用
这个场景,编译器就可能会进行指令重排序这个隐藏的线程安全隐患。new的2,3步在单线程角度下是可以调换顺序的,因为效果完全一样。如果这个时候触发指令重排序,按照132顺序执行,就会得到一个不完全的对象,只有内存但是内存数据无效。因此t2线程在调用getinstance()的时候就会认为这个instance非空从而直接返回instance,后续可能就会针对instance来进行解引用操作(使用里面的属性,方法)
因此需要在加上violatile来禁止指令重排序,同时如果有更多的线程,有的线程在修改,也会导致内存可见性问题,更应该加上volatile。
二
阻塞队列(先进先出的特殊队列)
和普通队列相比,阻塞队列有:
1.线程安全特点
2.带有阻塞功能
a.如果队列慢,继续入队列,入队列操作就会阻塞,直到队列不满,入队列才能完成。
b.如果队列空,继续出队列,出队列操作也会阻塞,直到队列不空,出队列才会完成。
使用阻塞队列,可以有利于代码解耦合(降低两个代码模块的关联程度)
同时可以削峰填谷。
package threading;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 17:58
*/
public class L1076 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(100);
//带有阻塞功能的put
blockingQueue.put(1);
blockingQueue.put(2);
blockingQueue.put(3);
//出队列
Integer ret = blockingQueue.take();
System.out.println(ret);
ret = blockingQueue.take();
System.out.println(ret);
ret = blockingQueue.take();
System.out.println(ret);
//如果多取一个出来,程序就会阻塞。
ret = blockingQueue.take();
System.out.println(ret);
}
}
自己实现阻塞队列:
1.先实现普通队列
2.加上线程安全
3.加上阻塞的实现
package threading;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-07
* Time: 18:08
*/
class MyBlockingQueue{
private int[] items = new int[1000];
private volatile int head =0;
private volatile int tail =0;
private volatile int size =0;
//入队列
public void put(int elem) throws InterruptedException {
//判断队列是否满了
while (size>=items.length){
this.wait();
}
//进入插入操作,把elem放到items里面,放到tail指向的位置。
items[tail] = elem;
tail++;
if(tail>=items.length){
tail=0;
}
size++;
this.notify();
//入队列成功就唤醒判断队列是否为空的wait
}
//出队列
public Integer take() throws InterruptedException {
//判断队列是否为空,为空则不能出队列。
while (size==0){
//判断条件,队列为空就阻塞
this.wait();
}
//队列非空,进行取元素操作。
int ret = items[head];
head++;
if(head>=items.length){
head = 0;
}
size--;
this.notify();
return ret;
}
}
//在队列中,同一时刻不会出现即是空又是满的情况,要么是put阻塞,要么是take阻塞
public class L077 {
public static void main(String[] args) {
}
}
还差一些未补完