本次笔记来自狂神说和尚硅谷
JUC(二)
8.常用的辅助类
8.1 CountDownLatch
CountDownLatch 相对于是一个减法计数器,允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " Go out");
countDownLatch.countDown();//数量-1
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归零,然后再向下执行
System.out.println("close Door");
}
}
结果:
0 Go out
2 Go out
4 Go out
5 Go out
1 Go out
3 Go out
close Door
原理:
countDownLatch.countDown();//数量-1
countDownLatch.await();//等待计数器归零,然后再向下执行
每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行!
8.2 CyclicBarrier
允许一组线程全部等待彼此达到共同屏障点的同步辅助。
CyclicBarrier 相对于是加法计数器,允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
CyclicBarrier : 指定个数线程执行完毕再执行操作
代码:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
//集齐七颗龙珠召唤神龙
//召换龙族的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("召唤神龙成功!"));
for (int i = 1; i <= 7; i++) {
final int temp = i;//临时变量
//lambda 能操作到 i 吗 不可以
//lambda本质上就是个类,不可能拿到上面的变量需要转折一下》中间变量,通过final变量
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集" + temp + "龙珠");
try {
cyclicBarrier.await();//等待
// 等待7个线程执行完了,上面的计数器到7,每次循环都会加1,到7了,才会完整的走下去(开启新的线程执行)
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
结果:
Thread-0收集1龙珠
Thread-3收集4龙珠
Thread-2收集3龙珠
Thread-1收集2龙珠
Thread-4收集5龙珠
Thread-5收集6龙珠
Thread-6收集7龙珠
召唤神龙成功!
总结:
7个线程执行完了,计数器变成7,它才会完整的走下去。如果改成8,那么程序将会一直执行,停止不了,它要等到了8才会执行那句话。
注意点:
lambda 不能操作到 i 。
lambda本质上就是个类,不可能拿到上面的变量
需要转折一下》中间变量,通过final变量
8.3 Semaphore(信号量)
概念:
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。
》 semaphore 就是一个容器 用它来限制并发的线程数
Semaphore: 同一时间只能有指定数量得到线程
举例:
就比如抢车位!6车但只有3个停车位置,
第一次进来3辆,其他只能等待,有车走,那等待的车就会进来
代码:
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
//线程数量:停车位! 限流!一次性只能进来三个
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
//acquire()得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//release()释放
//只有释放了后面的线程才会继续往里走
}
},String.valueOf(i)).start();
}
}
}
结果:
1抢到车位
4抢到车位
2抢到车位
1离开车位
2离开车位
4离开车位
3抢到车位
6抢到车位
5抢到车位
5离开车位
3离开车位
6离开车位
注意:
不是说所有车都走才可以进
按道理是走一辆可以进一辆
这里是因为所有车都是停2秒
概念:
semaphore.acquire();
获得,假设如果已经满了,等待,等待被释放为止 !
-1
的操作
semaphore.release();//release()释放
释放,会将当前的信号量释放,然后唤醒等待的线程!
+ 1
的操作
Semaphore的作用:
多个共享资源互斥的使用!
并发限流,控制最大的线程数!
9.读写锁 ReadWriteLock
实现类: ReentrantReadWriteLock
概念:
ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。
read lock可以由多个线程同时进行,write lock是独家的。
(读可以被多线程同时读,写的时候只能有一个线程去写)
作用: 提交效率和性能
举例:
写的时候只希望同时只有一个线程写,
读的时候所有人都可以读,提交性能,
这时候就可以使用ReadWriteLock
注意:
用synchronized 和lock锁可以,但是不能更精细。
下面的例子第一步不加读写锁的话
就会造成 写入的时候会有另一个插入,
达不到想要的先写入再写入ok
总结:
独占锁(写锁) 一次只能被一个线程占有
共享锁(读锁) 多个线程可以同时占有
ReadWriteLock》
读-读 可以共存!
读-写 不能共存!
写-写 不能共存!
代码:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
// MyCache myCache = new MyCache();
MyCacheLock myCache = new MyCacheLock();
//写入
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
//1
class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
//读写锁:更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//取,读,所有人都可以读
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
//2
//自定义缓存
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
// volatile保证原子性
//存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入ok");
}
//取,读,所有人都可以读
public void get(String key){
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取ok");
}
}
总结:
1.当使用MyCache myCache = new MyCache();时,即调用第二个
结果:
1写入1
1写入ok
3写入3
2写入2
…
1读取1
5写入5
2写入ok
3读取3
3读取ok
…
一看结果就知道不符合
2.当使用MyCacheLock myCache = new MyCacheLock();时,即调用第一个
结果:
1写入1
1写入ok
3写入3
3写入ok
…
2读取2
5读取5
5读取ok
1读取1
1读取ok
…
这时候就对了
10.阻塞队列
双端队列: 两头都会进去 而队列只会从一头进去
10.1 什么是阻塞队列?
队列》先进先出
写入: 如果队列满了,就必须阻塞等待。
取: 如果是队列是空的,必须阻塞等待生产。
这两种情况属于不得不阻塞
10.2 BlockingQueue
Interface BlockingQueue<E>
它的父接口有: Collection <E>
实现类主要有:
ArrayBlockingQueue(数组) ,
LinkedBlockingQueue(链表) ,
SynchronousQueue(同步队列)
什么情况下我们会使用阻塞队列:
多线程并发处理和线程池!
10.3 队列的四组API
学会使用队列主要有添加、移除
四组API
1、抛出异常
2、不会抛出异常(有返回值,没有异常)
3、阻塞等待(一直阻塞 一直等无止尽)
4、超时等待(等待超时 超过了某时间,我就不等了)
代码:
所有的结果都在里面
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) throws InterruptedException {
test3();
}
//抛出异常
public static void test1(){
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);//队列的大小
System.out.println(blockingQueue.add("a"));//返回值是true
System.out.println(blockingQueue.add("b"));//true
System.out.println(blockingQueue.add("c"));//true
// IllegalStateException: Queue full 再添加个元素就报异常,队列已经满了
// System.out.println(blockingQueue.add("d"));
System.out.println(blockingQueue.element());//查看队首元素是谁 //a
System.out.println();
System.out.println(blockingQueue.remove());//a
System.out.println(blockingQueue.remove());//b
System.out.println(blockingQueue.remove());//c
// NoSuchElementException abc取完是空的,再取就报异常啦 没有元素
// System.out.println(blockingQueue.remove());
}
//有返回值,没有异常
public static void test2(){
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));//true
System.out.println(blockingQueue.offer("b"));//true
System.out.println(blockingQueue.offer("c"));//true
// System.out.println(blockingQueue.offer("d"));//false 不抛出异常
System.out.println(blockingQueue.peek());//a
System.out.println();
System.out.println(blockingQueue.poll());//a
System.out.println(blockingQueue.poll());//b
System.out.println(blockingQueue.poll());//c
// System.out.println(blockingQueue.poll());//null 不抛出异常 弹(移除)出来前三个,再取值,就取不到报null
}
//等待 阻塞(一直阻塞 一直等无止尽)
public static void test3() throws InterruptedException {
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");//无结果 返回值是void
// blockingQueue.put("d");//队列没有位置了 一直阻塞(等待,程序不会结束)
System.out.println(blockingQueue.take());//返回值当前元素E //a
System.out.println(blockingQueue.take());//b
System.out.println(blockingQueue.take());//c
System.out.println(blockingQueue.take());// 没有这个元素 一直阻塞 会一直等待等取出
}
//等待 阻塞(等待超时 超过了某时间,我就不等了)
public static void test4() throws InterruptedException {
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
// 等待一段时间放弃 时间时长(超时时间) 时间单位(超时单位)等2秒,没有位置就超时退出》程序2秒后退出
blockingQueue.offer("d",2, TimeUnit.SECONDS);// 等待超过2秒就退出
System.out.println();
System.out.println(blockingQueue.poll());//a
System.out.println(blockingQueue.poll());//b
System.out.println(blockingQueue.poll());//c
blockingQueue.poll(2,TimeUnit.SECONDS);// 等待超过2秒就退出
}
}
10.4 SynchronousQueue 同步队列
概念:
没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
put 存,take 取
总结:
一个线程往里面放,一个线程从里面取,一定是这个流程。
和其他的BlockingQueue不一样,SynchronousQueue不存储元素
put了一个元素,必须从里面先take取出来,否则不能在put进去值!
11.线程池(重点)
11.1 概念:
线程池: 三大方法、7大参数、4种拒绝策略
池化技术
线程池、连接池、内存池、对象池… 创建、销毁。十分浪费资源
程序的运行,本质:占用系统的资源!所以要进行优化资源的使用!
所以需要用到池化技术。
池化技术: 事先准备好一些资源 ,有人要用,就来我这里拿,用完之后还给我。
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理。
即线程复用、可以控制最大并发数、管理线程
注意点:
阿里巴巴java开发规范曾说过:
[强制]线程池不允许使用Executors去创建,而是通过ThreadPool Executor的方式,这样的处理方式让写的同学更加明确线程池的运行规则|规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致0OM。 (约为21亿)OOM 》内存溢出 - CachedThreadPool 和ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致0M。
11.2 线程池:三大方法
1.newSingleThreadExecutor
》单个线程 只有一个线程在运行
2.newFixedThreadPool
》创建一个固定的线程池的大小 最多只能有xx个线程同时执行
3.newCachedThreadPool
》缓存,可伸缩的,遇强则强,遇弱则弱
代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// Executors 工具类 3大方法
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//单个线程 只有一个线程在运行
// ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 创建一个固定的线程池的大小 最多只能有5个线程同时执行
// ExecutorService threadPool = Executors.newCachedThreadPool();
// 缓存,可伸缩的,遇强则强,遇弱则弱 比如下面10循环 那么最高一定有10个线程同时执行
try {
for (int i = 0; i < 10; i++) {
//使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});//一个具体的线程
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
结果1:
结果2:
结果3:
11.3 7大参数 和 四种拒绝策略
11.3.1 源码(哪些7大参数)
本质: 上面的三大方法都是调用ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit,//超时单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,
//线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handler//拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
11.3.2 举例:
银行办理业务,一共有五个口,空闲的时候,就开了两个,来了两个人占了两个口,第三个人就只能去候客区等待,候客区一共有三个口,假如有一天,人特别多,候客区满了,导致五个窗口必须都开,最后也都满了,这时候又来一个人,那么这个人就只能选要么走,要么等,使用的机制就是拒绝策略。
和线程池对比:
比较线程池,最开始只有两个窗口开的,这就是核心(Core)线程池的大小,五个窗口属于Max 最大线程池大小,什么时候会满,那就是候客区也满了,这时候五个窗口都会打开,即五个人同时处理业务,候客区属于阻塞队列。
11.3.3 四种拒绝策略:(java.util.concurrent)
1.new ThreadPoolExecutor.AbortPolicy() (默认处理的拒绝策略)
银行满了,还有人进来,不处理这个人的,
抛出异常RejectedExecutionException
2.new ThreadPoolExecutor.CallerRunsPolicy ()
哪里来的去哪里 银行不受理了 结果可能是 main ok /main线程去处理
3.new ThreadPoolExecutor.DiscardPolicy ()
队列满了,丢掉多出的任务,不会抛出异常
4.new ThreadPoolExecutor.DiscardOldestPolicy ()
队列满了,将最早进入队列的任务删除,之后再尝试加入队列,也不会抛出异常
11.4 手动创建一个线程池
企业里只会用这种
参数:
第一个 核心大小 2个
第二个 最大 同时只能有五个
超时等待不候:三、四
第三个 基于时间单位的数字 超过该数据就不等了
第四个 时间单位
就比如上面的银行业务,人爆满,都处理完后,五个窗口还是都开着
1个小时后还没有业务,就关闭三个窗口来休息了,即线程池要被释放了
第五个 队列 候客区
第六个 创建线程的工厂 一般是选择默认工厂
第七个 拒绝策略
import java.util.concurrent.*;
public class Demo00 {
public static void main(String[] args) {
//自定义线程池! 工作 ThreadPoolExecutor
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
//最大承载:Deque + max
for (int i = 1; i <= 8; i++) {
//使用了线程池之后,使用线程池来参加线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
注意:
超过9会抛异常(被拒绝策略里的) RejectedExecutionException
结果:
池的最大的大小如何去设置!
》maximumPoolSize
了解: IO密集型,CPU密集型:(调优)!
CPU密集型
最高的比如我的电脑可以12条线程同时执行
》可以保持CPU的效率最高
获取CPU的线程的代码
System.out.println(Runtime.getRuntime().availableProcessors());
IO密集型
你的程序有15个大型任务 io十分占用资源 至少有15个线程去处理io
所以一定要大于15 有多余的线程去干别的
判断你程序中十分耗IO的线程,一般是大于2倍
12.四大函数式接口
lambda表达式、链式编程、函数式接口、Stream流式计算
这四个最好都会
函数式接口:只有一个方法的接口
只要是函数型接口可以用Lambda表达式简化
说明:(摘录)
匿名内部类是内部类的简化写法。
它的本质是一个带具体实现的父类或者父接口的匿名的子类对象。
一般只需要使用唯一的一次时需要用到。
匿名内部类的定义格式:
接口名称 对象名 = new 接口名称() {
// 覆盖重写所有抽象方法
};
12.1 Function 函数式接口
源码:
@FunctionalInterface
public interface Function<T, R> {//传入参数 T 返回类型R
R apply(T t);
…
}
Function 函数型接口,有一个输入参数,有一个输出
import java.util.function.Function;
public class Demo01 {
public static void main(String[] args) {
// Function<String,String> function = new Function<String,String>(){
// @Override
// public String apply(String str) {
// return str;
// }
// };
Function<String,String> function = (str) -> str;
System.out.println(function.apply("asd"));//asd
}
}
12.2 Predicate 断定型接口
源码:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
…
}
有一个输入参数,返回值只能是布尔值!
import java.util.function.Predicate;
public class Demo02 {
public static void main(String[] args) {
//判断字符串是否为空
// Predicate<String> predicate = new Predicate<String>() {
// @Override
// public boolean test(String str) {
// return str.isEmpty();
// }
// };
Predicate<String> predicate = str->str.isEmpty();
System.out.println(predicate.test(""));//true
}
}
12.3 Consumer 消费型接口
源码:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
…
}
只有输入,没有返回值
import java.util.function.Consumer;
public class Demo03 {
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println(str);
// }
// };
Consumer<String> consumer = str -> System.out.println(str);
consumer.accept("asdf");//asdf
}
}
12.4 Supplier 供给型接口
源码:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
没有参数 只有返回值
import java.util.function.Supplier;
public class Demo04 {
public static void main(String[] args) {
// Supplier supplier = new Supplier<Integer>(){
// @Override
// public Integer get() {
// return 1024;
// }
// };
Supplier supplier = () -> {return 1024;};
System.out.println(supplier.get());//1024
}
}
13.Stream流式计算
13.1 什么是Stream流式计算
大数据》存储+计算
集合、MySQL 本质就是存储东西的;
计算都应该交给流来操作!
13.2 举例
User 类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 无参 有参 get,set,toString方法
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private int age;
}
题目要求:用一行代码实现!
现在有5个用户!筛选:
1、ID必须是偶数
2、年龄必须大于23岁
3、用户名转为大写字母
4、用户名字母倒着排序
5、只输出一个用户!
Test
import java.util.Arrays;
import java.util.List;
public class Test {
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"C",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
//集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
//计算交给Stream
//Lambda表达式、链式编程、函数式接口、Stream流式计算
list.stream()
.filter(u->{return u.getId()%2==0;})
.filter(u->{return u.getAge()>23;})
.map(u->{return u.getName().toUpperCase();})
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})
.limit(1)
.forEach(System.out::println);
}
}
结果 :》E
14.JMM
14.1 什么是JMM(保证线程的安全)
JMM: Java内存模型,不存在的东西,概念!约定!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
14.2 举例和8大操作
有个主存,线程A,这时候主存有个flag=true,线程A不会直接拿,而是拷贝过去到线程自己的工作内存,操作的是拷贝的那份,拷贝的变成false,需要刷新回主存。
8种操作:(摘录)
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock(锁定):
作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):
作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):
作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):
作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use(使用):
作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign(赋值):
作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store(存储):
作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write(写入):
作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:(摘录)
1.不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
2.不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
3.不允许一个线程将没有assign的数据从工作内存同步回主内存
4.一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
5.一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
6.如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
7.如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
8.对一个变量进行unlock操作之前,必须把此变量同步回主内存
14.4 问题
这时候线程B 的flag=false刷新回主内存
主存变成flase 但线程A还是true没有读取到最新的值
问题,线程B修改了值,但是线程A不能及时可见
代码演示:
import java.util.concurrent.TimeUnit;
public class Demo01 {
private static int num = 0;
public static void main(String[] args) {//main线程
new Thread(()->{//线程1对主内存的变化是不知道的
while (num == 0){}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);//1
}
}
》程序启动输出1,但程序不会停止
解决方法:
主内存发生变化,必须通知线程A
这时候就需要用Volatile
15.Volatile
15.1 概念
请你谈谈你对Volatile的理解:
Volatile是Java虚拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
15.2 保证可见性
上面的工程改成》
不加volatile 程序就会死循环!
加volatile 可以保证可见性
private volatile static int num = 0;
》结果就是1 程序也停止了
15.3 不保证原子性
15.3.1 证明不保证原子性
原子性: 不可分割
线程A在执行任务的时候,不能被打扰的,
也不能被分割。要么同时成功,要么同时失败。
public class Demo02 {
//volatile不保证原子性
private volatile static int num = 0;
public static void add(){
num++;
}
//num++ 不是原子性的操作
//加个synchronized 一定能解决 结果就是2万 每次保证只有一个线程进来
//加 volatile依旧不能保证原子性 还是有两个线程同时进去
public static void main(String[] args) {
//理论上num结果应该为2万
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){//判断还有多少线程存活 >2就代表还没成功 main和gc默认执行
Thread.yield();//让出cpu使用权,重新竞争CPU执行权
}
System.err.println(Thread.currentThread().getName()+" "+num);
}
}
结果: main 20000左右 很少会到2万
15.3.2 如何保证原子性(原子类)
问题:
如果不加lock和synchronized,怎么样保证原子性
说明:
num++不是原子性的操作 看似是一行代码 其实不是
查看底层源码
(底层本质上是四条字节码指令,并不是原子性操作)
先找到target对应的类,然后show in Explorer打开文件夹
cmd 然后javap -c Demo02.class反编译 看到字节码文件
1.获得这个值
2.+1
3.写回这个值
解决方法:使用原子类,解决原子性问题
java.util.concurrent.atomic 原子类
(高效和少耗资源比加lock和synchronized)
常用的Classes (原子包装类)》
- Class AtomicBoolean
- Class AtomicInteger 更安全
- Class AtomicLong
代码:
import java.util.concurrent.atomic.AtomicInteger;
public class Demo02 {
//volatile不保证原子性
//原子类的Integer
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement();//AtomicInteger + 1 方法 但不是个简单的+1操作 用的底层的CAS 效率更高
}
public static void main(String[] args) {
//理论上num结果应该为2万
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.err.println(Thread.currentThread().getName()+" "+num);
}
}
结果: main 20000
这些类的底层都直接和操作系统挂钩!在内存中修改值。
Unsafe类是一个很特殊的存在!
15.4 禁止指令重排
15.4.1 什么是指令重排:
你写的程序,计算机并不是按照你写的那样去执行的。
源代码–>编译器优化的重排–>
指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
举例:
int x=1;//1
int y=2;//2
x=X+5; //3
y=x*x; //4
我们所期望的: 1234 但是可能执行的时候会变成2134 1324
但绝 不可能是4123!
但指令重排出现的概率比较低
15.4.2 volatile
》所以加了volatile可以避免指令重排
volatile可以避免指令重排:
内存屏障。CPU指令。作用:
1、保证特定的操作的执行顺序!
2、可以保证某些变量的内存可见性
( 利用这些特性volatile实现了可见性)
总结:
Volatile 是可以保持可见性。
不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
Volatile 作用在单例模式
16.玩转单例模式
//懒汉式单例
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "ok");
}
private volatile static LazyMan lazyMan;
//双重监测模式的懒汉式单例 DCL懒汉式
public static LazyMan getLazyMan() {
if (lazyMan == null){
// 采用了一个double check的机制,就是每当判断如果有实例的话,
// 只有在创建的时候会抢夺锁
// 其他的线程仅仅一个读取的操作都需要等待锁,会造成效率低下的问题。
// 所以就是说,锁的范围越小越好
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();//不是一个原子性操作
/*指令重排
1.分配内存空间
2.执行构造方法,初始化对象
3.把这个对象指向这个空间
123
走132 A没问题
但线程B可能有问题 //此时lazyMan还没有完成构造 volatile这是避免指令重排
*/
}
}
}
return lazyMan;
}
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getLazyMan();
}).start();//Thread-0ok
}
}
}
17.可重入锁(递归锁)
举例:
就是一个家,打开大门,
那么所有房间都可以进去
》一把锁就够了,里面的可以无障碍进入
synchronized(隐式) 上锁解锁自动完成
Lock(显式) 上锁解锁手动完成
都是可重入锁
举例1:
可重入锁 同步代码块
public class Lock01 {
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(Thread.currentThread().getName() + " 外层");
synchronized (o){
System.out.println(Thread.currentThread().getName() + " 中层");
synchronized (o){
System.out.println(Thread.currentThread().getName() + " 内层");
}
}
}
},"t1").start();
}
}
结果:
t1 外层
t1 中层
t1 内层
举例2:
可重入锁 递归调用
*/
public class Lock02 {
public synchronized void add(){
add();
}
public static void main(String[] args) {
new Lock02().add();
}
}
结果:
报栈溢出异常 》 java.lang.StackOverflowError
因为是可重入锁 体现出了循环递归调用 ,所以才会报错
举例3:
可重入锁 lock
进入第一个锁以后,无障碍进入第二个
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Lock03 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(()->{
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " 外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " 内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
System.out.println("aaa");
lock.unlock();
},"aa").start();
}
}
结果:
t1 外层
t1 内层
注意:
如果最里面的 lock.unlock(); 不写
虽然能出现 t1 外层 t1 内层 但是aaa就出不来 程序一直不会结束
锁没有释放,所以aa线程就得不到那把锁,
就不会执行,他要等待锁释放后,
然后得到,才能继续往下走。
18.死锁
18.1 什么是死锁?
两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去
18.2 产生死锁原因:
第一 系统资源不足
第二 进程运行推进顺序不合适
第三 资源分配不当
18.3 代码
import java.util.concurrent.TimeUnit;
public class DeadLock {
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName() + " 持有锁a,试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}// 看起来更明确
synchronized (b){
System.out.println(Thread.currentThread().getName() + " 获取锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName() + " 持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println(Thread.currentThread().getName() + " 获取锁a");
}
}
},"B").start();
}
}
结果:
A 持有锁a,试图获取锁b
B 持有锁b,试图获取锁a
》程序结束不了
18.4 验证是否是死锁(deadlock)
第一步(jps -l) 查看对应类的进程号xxxx
jps命令 查看当前正在运行的进程和详细内容
类似linux 的ps -ef
第二步 (jstack xxxx)
jstack命令 jvm自带堆栈跟踪工具
在Terminal中写命令。
Found 1 deadlock代表出现死锁