创建普通的maven项目,确保我们的项目一定是在Java8的环境下,检查settings中的以下几点
添加一个 lombok 依赖
什么是 JUC ?
查看源码可以发现,start方法底层调用了本地方法,本地方法就是C语言提供的
//本地方法,调用底层c++, java无法操作硬件
private native void start0();
并发(多线程操作同一个资源,交替执行)
并行(多个人一起行走, 同时进行)
代码检测当前CPU核数
public class TestCore {
public static void main(String[] args) {
// 获取cpu核数
// cpu密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质: 充分利用CPU的资源
查看Thread
源码,可以发现枚举类State
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死等
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
之前我们所学的使用线程的传统思路是:
比如,
//线程不安全:买票例子
//线程不安全,输出结果有买重票有负数票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"抢票的我").start();
new Thread(station,"买票的你们").start();
new Thread(station,"可恶的黄牛党").start();
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
//外部停止方式
boolean flag = true;
@Override
public void run() {
//买票
while (true){
if (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
private void buy() throws InterruptedException {
//判断是否有票
if (ticketNums <= 0){
flag = false;
return;
}
//模拟延时,放大问题
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName()+"--> 拿到第 "+ ticketNums--+" 张票");
}
}
但是这样写代码有诸多问题,不太符合OOP思想,增加耦合性等问题,
实际工作的使用线程的思路是:
比如,这里我们使用了实现Runnable的方法创建线程,还是用了lambda表达式来创建Runnable实例
public class TestCore {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}).start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}).start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}).start();
}
}
// 这是一个资源类,存放属性、方法
class Ticket{
private int number = 300;
public synchronized void sale(){
if(number>0){
System.out.println(Thread.currentThread().getName()+"get "+number+"#");
number--;
}
}
}
这需要我们在工作加以注意
查看 api 文档
可以看到,Lock
是一个接口,有三个实现类,现在我们使用 ReentrantLock
就够用了
查看 ReentrantLock
源码,构造器
公平非公平:
ReentrantLock 构造器
我们将上面的抢票代码改造为
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for(int i = 0; i < 40; i++) ticket.sale();}, "a").start();
new Thread(()->{
for(int i = 0; i < 40; i++) ticket.sale();}, "b").start();
new Thread(()->{
for(int i = 0; i < 40; i++) ticket.sale();}, "c").start();
}
}
class Ticket {
private int ticketNum = 30;
private Lock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (this.ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "购得第" + ticketNum-- + "张票, 剩余" + ticketNum + "张票");
}
//增加错误的发生几率
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行,发现,多线程都有几率抢到票,且没有出现线程安全问题
综述,Lock 锁实现步骤:
总体来说,synchronized 本来就是一个关键字,很多规则都是定死的,灵活性差;Lock 是一个类,灵活性高
思考问题:什么是锁?锁的是什么?
面试高频考点:
解决线程之间的通信问题,比如线程操作一个公共的资源类
基本流程可以总结为:
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = new Data();
// 创建一个生产者
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
// 创建一个消费者
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//这是一个缓冲类,生产和消费之间的仓库,公共资源类
class Data{
// 这是仓库的资源,生产者生产资源,消费者消费资源
private int num = 0;
// +1,利用关键字加锁
public synchronized void increment() throws InterruptedException {
// 首先查看仓库中的资源(num),如果资源不为0,就利用 wait 方法等待消费,释放锁
if(num!=0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"=>"+num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 首先查看仓库中的资源(num),如果资源为0,就利用 wait 方法等待生产,释放锁
if(num==0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"=>"+num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
思考问题:如果存在ABCD4个线程是否安全?
查看 api 文档
解决办法:if 判断改为 while,防止虚假唤醒
修改代码为:
// ...
// 使用 if 存在虚假唤醒
while (num!=0){
this.wait();
}
// ...
while(num==0){
this.wait();
}
锁、等待、唤醒 都进行了更换
将代码改造为 JUC 版本的生产者和消费者模式,这里我们使用四个线程,ABCD,两个生产者,两个消费者,
改造之后,确实可以实现01切换,但是ABCD是无序的,不满足我们的要求,
package com.swy.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author SuperSong
* @version 1.0.0
* @ClassName juc-study.com.swy.pc.ConsumeAndProduct.java
* @Description TODO
* @createTime 2021年04月25日 07:47:00
*/
public class ConsumeAndProductLock {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data2{
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition 的优势在于,精准的通知和唤醒线程!比如,指定通知下一个进行顺序。
重新举个例子,
三个线程 A执行完调用B,B执行完调用C,C执行完调用A,分别用不同的监视器,执行完业务后指定唤醒哪一个监视器,实现线程的顺序执行
锁是统一的,但监视器是分别指定的,分别唤醒,signal,之前使用的是 signalAll
// A执行完调用B,B执行完调用C,C执行完调用A
public class ConditionDemo {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printC();
}
},"C").start();
}
}
class Data3 {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int num = 1; // 1A 2B 3C
public void printA(){
lock.lock();
try {
while (num != 1){
condition1.await();
}
System.out.println(Thread.currentThread().getName() + " Im A ");
num = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName() + " Im B ");
num = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + " Im C ");
num = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
深入理解锁
关于锁的八个问题
标准情况下,两个线程,先发短信还是先打电话?
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendMsg();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
}).start();
}
}
// 可视作资源类
class Phone{
public synchronized void sendMsg(){
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendMsg();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
}).start();
}
}
// 可视作资源类
class Phone{
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
结果依旧是先发短信,后打电话
分析:
public class Test2 {
public static void main(String[] args) {
Phone2 phone2 = new Phone2();
new Thread(()->{
phone2.sendMsg();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.hello();
}).start();
}
}
// 可视作资源类
class Phone2 {
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
public void hello(){
System.out.println("hello");
}
}
分析原因:
public class Test2 {
public static void main(String[] args) {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(()->{
phone1.sendMsg();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
}).start();
}
}
// 可视作资源类
class Phone2 {
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
public void hello(){
System.out.println("hello");
}
}
分析原因:
public class Test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
new Thread(() -> {
phone.sendMessage();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone3 {
public static synchronized void sendMessage() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "发短信");
}
public static synchronized void call() {
System.out.println(Thread.currentThread().getName() + "打电话");
}
}
结果,始终是先发短信,后打电话
分析原因:
public class Test3 {
public static void main(String[] args) {
Phone3 phone2 = new Phone3();
Phone3 phone3 = new Phone3();
new Thread(() -> {
phone2.sendMessage();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone3.call();
}, "B").start();
}
}
class Phone3 {
public static synchronized void sendMessage() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "发短信");
}
public static synchronized void call() {
System.out.println(Thread.currentThread().getName() + "打电话");
}
}
public class Test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
new Thread(()->{
phone.sendMsg();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
}).start();
}
}
// 可视作资源类
class Phone4 {
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
public void hello(){
System.out.println("hello");
}
}
public class Test4 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{
phone1.sendMsg();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
}).start();
}
}
// 可视作资源类
class Phone4 {
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
public void hello(){
System.out.println("hello");
}
}
在 JUC 并发编程情况下,适用于单线程的集合类将出现并发问题
代码举例
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
运行出现并发修改异常,java.util.ConcurrentModificationException
原因:ArrayList 在并发情况下是不安全的
解决方案1:
List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
运行测试,没有问题
分析:
CopyOnWriteArrayList 方法,先复制,再修改,再set回去
Set 和 List 同理可得:多线程情况下,普通的 Set 集合是线程不安全的
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
注意这里,不一定会立刻出现并发问题,需要多试几次,增加循环线程数量,容易出现问题
解决方案有两种;
Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
思考,HashSet 底层到底是什么?
查看 HashMap 源码
默认初始容量 24=16,最大容量为 230,加载因子 0.75
HashMap 在多线程操作情况下,也有并发修改异常的问题
解决方案:
使用时注意
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask,"a").start();
System.out.println(futureTask.get());
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("call()方法被调用了");
return 1024;
}
}
注意:
减法计数器
//计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 倒计时总数是6, 必须要执行任务的时候,再使用!
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");
}
}
原理:
每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await();就会被唤醒,继续执行
加法计数器,与 CountDownLatch 正好相反
相当于设定一个目标,线程数达到目标值之后才会执行
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 主线程,计数器到达7即满足条件
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙");
});
for (int i = 1; i <= 7; i++) {
// 子线程
int finalI = i;// 下面thread中拿不到i,所以这里提取出来
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集了第"+finalI+"颗龙珠");
try {
cyclicBarrier.await();//加法计数等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
计数信号量,比如说,有6辆车,3个停车位,汽车需要轮流等待车位
常用在需要限流的场景中,
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量,停车位,限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; 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();// 释放
}
}).start();
}
}
}
原理:
用途:
public class ReadWriteLockDemo {
public static void main(String[] args) {
// 线程操作资源类
MyCache myCache = new MyCache();
int num = 6;
for (int i = 1; i < num; i++) {
int finalI = i;
new Thread(()->{
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
},String.valueOf(i)).start();
}
for (int i = 1; i < num; i++) {
int finalI = i;
new Thread(()->{
myCache.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
// 自定义缓存
class MyCache{
private volatile Map<String,String> map = new HashMap<>();
// 存,写
public void write(String key, String value) {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程开始写入ok");
}
// 取,读
public void read(String key) {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程读取ok");
}
}
运行结果会出现问题,还没等线程写完或读完,就被其他线程写入,
如何解决这样的问题,需要加锁
之前我们使用的 Lock 锁,但粒度比较粗,只是普通的锁,
这里我们加读写锁,ReadWriteLock
,这是一个更加细粒度的锁
public class ReadWriteLockDemo {
public static void main(String[] args) {
// 线程操作资源类
MyCache myCache = new MyCache();
int num = 6;
for (int i = 1; i < num; i++) {
int finalI = i;
new Thread(()->{
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
},String.valueOf(i)).start();
}
for (int i = 1; i < num; i++) {
int finalI = i;
new Thread(()->{
myCache.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
// 自定义缓存
class MyCache{
private volatile Map<String,String> map = new HashMap<>();
private ReadWriteLock readWriteLock= new ReentrantReadWriteLock();
// 存,写,写入的时候只希望只有一个线程在写
public void write(String key, String value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程开始写入ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
// 取,读,所有线程都可以读
public void read(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程读取ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
运行可以发现,写入时不可以被插队,读取时可以被多人执行
小结:
也可以这样称呼,含义都是一样,名字不同而已
阻塞队列 BlockQueue 是Collection 的一个子类
BlockingQueue 有四组 API
方式 | 抛出异常 | 不会抛出异常,有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加操作 | add() | offer() 供应 | put() | offer(obj,int,timeunit.status)可设置时间 |
移除操作 | remove() | poll() 获得 | take() | poll(int,timeunit.status)可设置时间 |
判断队列首部 | element() | peek() 偷看,偷窥 |
代码演示:四种情况
public class BlockingQueueDemo {
// 抛异常 add remove
@Test
public void test01(){
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// 查看队首
System.out.println(blockingQueue.element());
// 如果再添加就会抛出异常 java.lang.IllegalStateException: Queue full
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// 如果再移除也会造成 java.util.NoSuchElementException
}
// 不抛出异常,offer、poll、peek
@Test
public void test02(){
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
// 再添加不会抛出异常,而是返回false
System.out.println(blockingQueue.offer("d"));
// 检测队首元素
System.out.println(blockingQueue.peek());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
// 再移除不会抛出异常,而是返回null
System.out.println(blockingQueue.poll());
}
// 如果队列满了,等待,一直阻塞
@Test
public void test03() throws InterruptedException {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// 一直阻塞,不会返回值
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
// 队列已满,再添加,则阻塞等待添加,程序一直在阻塞中,不结束
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
// 如果再移除不会抛出异常,但是会一直阻塞在这里了
}
// 这种情况也会发生阻塞等待,但会有超时结束
@Test
public void test04() throws InterruptedException {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
System.out.println("开始等待");
blockingQueue.offer("d",2, TimeUnit.SECONDS);
System.out.println("等待结束");
System.out.println("=================取值=====================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println("取值开始等待");
blockingQueue.poll(2,TimeUnit.SECONDS);//超过两秒,我们就不等待了
System.out.println("取值结束等待");
}
}
同步队列没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"put 01");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName()+"put 02");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName()+"put 03");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
运行发现,两个线程一个存,一个取,即使我们没有规定顺序,执行起来也是存一次取一次,交替执行,这是 SynchronousQueue 的特性决定的
线程池重点:三大方式、七大参数、四种拒绝策略
程序的运行的本质:占用系统的资源 ! 优化CPU资源的使用 ===>池化技术(线程池、连接池、内存池、对象池…)
池化技术:实现准备好一些资源,有人要用,就来我这里拿,用完之后还给我
线程池的好处:
如何优化:
之前我们所学知识,直接创建线程,现在我们通过线程池来创建线程,使用池化技术
代码演示
//Executors 工具类
//使用了线程池之后要使用线程池创建线程
public class Demo01 {
public static void main(String[] args) {
// ExecutorService service = Executors.newSingleThreadExecutor();//单个线程
// ExecutorService service = Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小
ExecutorService service = Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
try {
for (int i = 0; i < 10; i++) {
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + "ok");
});
}
//线程池用完要关闭线程池
} finally {
service.shutdown();
}
}
}
通过测试我们发现,固定线程池的方式至多只能创建固定数量,可伸缩的方式就会视情况而定
固定线程池(循环次数10)
可伸缩线程池,(循环次数100),
出现了 30多个线程
源码分析:
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
结论:所有线程池最终都调用的 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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
通过观察,上面的三种创建方式其实就是换了不同的参数
分析参数,才会有阿里巴巴规范所说的,不允许使用 Executors
创建线程池,而是通过 ThreadPoolExecutor
创建,因为参数可控
从这张图片来分析
理解了以上理论,我们将按照阿里巴巴手册的规范,手动创建线程池,改造代码
//自定义线程池,创建默认的线程工厂
ExecutorService threadPool = new ThreadPoolExecutor(
3, // 核心线程池大小,也是初始默认大小
5, // 最大并发数
10, // 超时时间,超过时间没有业务就会释放多余的线程回到默认数量
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(3),// 线程等候队列,这里表示队列超过3个等待就会触发最大线程数
Executors.defaultThreadFactory(), //线程创建工厂
new ThreadPoolExecutor.AbortPolicy());//拒绝策略,有四种拒绝策略,这个是默认的,表示如果队列满了就不再处理,并抛出异常
分析:
两种方案:
新时代程序员(JDK8新特性):lambda 表达式、链式编程、函数式接口、Stream 流式计算
函数式接口:只有一个方法的接口,比如 Runnable 接口
优点:简化编程,在新版本的框架底层大量使用
public class FunctionDemo {
public static void main(String[] args) {
Function<String,String> function = (str)->{
return str;
};
System.out.println(function.apply("aaaaaaa"));
}
}
public class PredicateDemo {
public static void main(String[] args) {
Predicate<String> predicate = (str)->{
return str.isEmpty();
};
System.out.println(predicate.test("aaa"));
System.out.println(predicate.test(""));
}
}
测试效果,我们可以利用这一特性,对传入的参数进行判断,返回 true or false,
public class SupplierDemo {
public static void main(String[] args) {
Supplier<String> supplier = ()->{
return "1024";
};
System.out.println(supplier.get());
}
}
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<String> consumer = (str)->{
System.out.println(str);
};
consumer.accept("abc");
}
}
简化编程模型
这里的接口只是起一个规范,具体怎么来编写业务代码,需要我们自己重写里面的方法
大数据:存储+计算
集合、MySQL 本质就是存储数据,而计算应该交给流来操作
首先查看 package java.util.stream
包下的 interface Stream
接口
可以看到,Stream 中的大量方法都是用了函数式接口
根据上述需求,编写演示代码
public class StreamDemo {
public static void main(String[] args) {
User user1 = new User(1, "a", 21);
User user2 = new User(2, "b", 22);
User user3 = new User(3, "c", 23);
User user4 = new User(4, "d", 24);
User user5 = new User(5, "e", 25);
// 集合用于存储
List<User> list = Arrays.asList(user1, user2, user3, user4, user5);
// 计算交给Stream流
// 一行代码使用了 lambda表达式 链式编程 函数式接口 Stream流式计算
list.stream()
.filter(user -> {
return user.getId() % 2 == 0;})
.filter(user -> {
return user.getAge() > 23;})
.map(user -> {
return user.getName().toUpperCase();})
.sorted((u1,u2)->{
return u2.compareTo(u1);})
.limit(1)
.forEach(System.out::println);
}
}
理解分析:
user -> {
return user.getId() % 2 == 0;}
流中的所有user只有满足ID为偶数的才会返回true,通过筛选,filter返回的流中只有ID为偶数的user
这里的流在计算判断时,会将所有的user全部都过一遍,然后按照我们的业务规则进行筛选
这些代码就是通过泛型统一的元素类型为 User
总结一下:
这种方式不仅简化代码,底层优化也使得代码效率得到提高,是不是感觉很强大!
ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!
大数据中:MapReduce 核心思想->把大任务拆分为小任务!
使用要点:
准备计算类
public class ForkJoinDemo extends RecursiveTask<Long> {
private long start; // 1
private long end; // 20_0000_0000
private long temp = 1_0000L;
public ForkJoinDemo(long start, long end) {
this.start = start;
this.end = end;
}
// 实现抽象类需要重写计算方法
@Override
protected Long compute() {
// 小于临界值则直接求和
if (end - start < temp) {
Long sum = 0L;
for (Long i = start; i < end; i++) {
sum += i;
}
return sum;
} else {
// 大于临界值则采用分支合并计算
long middle = (end + start) / 2; //中间值
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
task2.fork();// 拆分任务,把任务压入线程队列
return task1.join() + task2.join();
}
}
}
写测试类
public class ForkJoinTest {
private static final long SUM = 20_0000_0000;
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
// 使用普通方法
public static void test1(){
long star = System.currentTimeMillis();
long sum = 0L;
for (int i = 0; i < SUM; i++) {
sum+=i;
}
long end = System.currentTimeMillis();
System.out.println(sum);
System.out.println("时间:" + (end-star));
System.out.println("---------------");
}
// 使用 ForkJoin 方法
public static void test2() throws ExecutionException, InterruptedException {
long star = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L,SUM);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long along = submit.get();
System.out.println(along);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("---------------");
}
// 使用流计算
public static void test3(){
long star = System.currentTimeMillis();
long sum = LongStream.range(0L,20_0000_0000L).parallel().reduce(0,Long::sum);
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("-------------");
}
}
为什么我的 forkjoin 计算比普通计算时间更长???
分析:
注意:compute是 forkjoin 计算时底层自己调用的,这也是为什么很多人不理解他是递归的原因,我们查看源码
进入get
进入 getRawResult
选择 实现类
可以看到最终结果就是由底层 compute得来的
Future 设计的初衷: 对将来的某个事件的结果进行建模
Java也可以实现异步调用,与ajax是一个道理
我们平时都使用 CompletableFuture
没有返回值的runAsync异步回调
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 发起一个请求
System.out.println(System.currentTimeMillis());
System.out.println("-----------");
CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
// 发起一个异步任务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"......");
});
System.out.println(System.currentTimeMillis());
System.out.println("-------------------------");
System.out.println(future.get());//获取执行结果
}
}
有返回值的异步回调supplyAsync
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 发起一个请求
System.out.println(System.currentTimeMillis());
System.out.println("-----------");
CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
// 发起一个异步任务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"......");
});
System.out.println(System.currentTimeMillis());
System.out.println("-------------------------");
System.out.println(future.get());//获取执行结果
}
@Test
public void test02() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1024;
});
System.out.println(completableFuture.whenComplete((t,u)->{
// success 回调
System.out.println("t=>" +t);//正常的返回结果
System.out.println("u=>" +u);//抛出异常的错误信息
}).exceptionally((e)->{
// error 回调
System.out.println(e.getMessage());
return 404;
}).get());
}
}
whenComplete: 有两个参数,一个是t 一个是u
T:是代表的 正常返回的结果;
U:是代表的 抛出异常的错误信息;
如果发生了异常,get可以获取到exceptionally返回的值;
Volate是Java虚拟机提供轻量级的同步机制
JMM: JAVA 内存模型,不存在的东西,是一个概念,也是一个约定!
关于JMM的一些同步的约定:
线程中分为 工作内存、主内存
JMM约定有八种操作:
因此,JMM规定了八个规定
了解 JMM 八种操作和八个规定,我们开始理解 Volatile,也就是Java虚拟机提供轻量级的同步机制
public class VolatileDemo {
// 如果不加 volatile 程序会死循环
// 加了volatile 是可以保证可见性的
private volatile static Integer number = 0;
public static void main(String[] args) {
// main 线程
// 子线程1
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
number = 1;
System.out.println(number);
}
}
测试运行
加了volatile 是可以保证可见性的
如果将 volatile 去掉,主线程将number修改为1,子线程不知道,还在按照 number=0一直做死循环
原子性:不可分割;
线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。
代码举例
public class VDemo02 {
private static int number = 0;
public static void add(){
number++;
// ++ 不是一个原子操作,是2~3个操作
}
public static void main(String[] args) {
// 理论上 number === 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
// java 默认有两个线程 main 和 gc 超过2个说明还有其他线程存活
// 这里的含义就是 如果main和gc之外还有其他线程,主线程就会礼让,保证其他线程执行完
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num + "+number);
}
}
运行测试,理论上number应该为20000,实际上达不到,因为number++不是原子操作,存在并发问题
解决办法之一就是给 add 加锁
public synchronized static void add(){
number++;
}
去掉synchronized ,给 number 加 volatile
private volatile static int number = 0;
运行测试,发现总数达不到20000,说明 volatile不能保证原子性
问题:
首先分析 number++的底层实现
通过命令行进入到class文件所在目录,执行指令,查看他的字节码指令逻辑
javap -c VDemo02.class
回到这个问题,如果不加 lock 和 synchronized,如何保证原子性
使用原子类解决原子性问题
public class VDemo02 {
// private volatile static int number = 0;
private static volatile AtomicInteger number = new AtomicInteger();
public static void add(){
// number++;
// ++ 不是一个原子操作,是2~3个操作
number.incrementAndGet();//底层是 CAS 保证原子性
}
public static void main(String[] args) {
// 理论上 number === 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
// java 默认有两个线程 main 和 gc 超过2个说明还有其他线程存活
// 这里的含义就是 如果main和gc之外还有其他线程,主线程就会礼让,保证其他线程执行完
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num + "+number);
}
}
为什么要使用原子类?
这些类的底层都直接和操作系统挂钩!是在内存中修改值。Unsafe 类是一个很特殊的存在;
什么是指令重排?
我们写的程序,计算机并不是按照我们自己写的那样去执行的
源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
int x=1; //1
int y=2; //2
x=x+5; //3
y=x*x; //4
//我们期望的执行顺序是 1_2_3_4 可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的
因为,处理器在进行指令重排的时候,会考虑数据之间的依赖性!
这个例子是指令重排没有造成影响,也有造成影响的时候
比如,假设abxy默认都是0,执行以下线程操作
线程A | 线程B |
---|---|
x = a | y = b |
b = 1 | a = 2 |
正常的结果应该是 x = 0; y =0;
然而可能会出现以下的执行顺序
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
可能在线程A中会出现,先执行b=1,然后再执行x=a;
在B线程中可能会出现,先执行a=2,然后执行y=b;
那么就有可能结果如下:x=2; y=1.
这种情况可能很难出现,但是还是有一定概率,需要注意
volatile可以避免指令重排:
volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序
内存屏障:CPU指令。作用:
1、保证特定的操作的执行顺序;
2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)
public class SingleHungryDemo {
/*
* 可能会浪费空间
* */
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
private SingleHungryDemo(){
}
private final static SingleHungryDemo hungry = new SingleHungryDemo();
public static SingleHungryDemo getInstance(){
return hungry;
}
}
程序一加载就可能占用大量资源
// 懒汉式单例,单线程安全
public class LazyMan {
// 构造器私有
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
// 多线程会出现并发问题
public static void main(String[] args) {
for (int i= 0;i<10;i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
但是多线程下有并发问题,这里仍然出现了多个线程,本应该只有一个
因此我们需要加锁
// 懒汉式单例
public class LazyMan {
// 构造器私有
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
private static LazyMan lazyMan;
// 双重检测模式 简称 DCL
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
// 多线程会出现并发问题
public static void main(String[] args) {
for (int i= 0;i<10;i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
lazyMan = new LazyMan();
不是原子性操作,里面会经历三步操作
这就有可能出现指令重排问题
为了安全,我们要避免指令重排,我们就可以添加 volatile 保证指令重排问题
// 静态内部类实现单例
public class Holder {
// 单例模式一定是构造器私有
private Holder() {
}
public static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass {
private static final Holder HOLDER = new Holder();
}
}
但是目前,单例仍然不安全,因为可以使用反射
// 懒汉式单例
public class LazyMan {
// 构造器私有
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
private static LazyMan lazyMan;
// 双重检测模式 简称 DCL
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
// 反射
public static void main(String[] args) throws Exception {
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);// 无视私有构造器
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
运行发现,两个对象不一样,不是单例
如何解决这种问题?我们可以在构造器中加锁判断,防止别人使用反射破坏单例
// 构造器私有
private LazyMan() {
synchronized (LazyMan.class) {
if (lazyMan != null) {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
但是,如果两个instance都是通过反射创建的,如何判断呢?
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();
我们可以设置一个标志位,来判断对象是否已经被创建了无需再创建,
添加标志位,将构造器改造为
private static boolean swy = false;
// 构造器私有
private LazyMan() {
synchronized (LazyMan.class) {
if (swy == false) {
swy = true;
} else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
只要构造器被调用,表示位就会变化,然后无法再次创建对象,
除非对方有反编译器看到了标志位,否则仅仅使用反射也不能创建多例,标志也可以再进一步加密,进一步提高了安全性
假设对手更高级,找到了标志位,并通过反射将标志位破坏了
// 反射
public static void main(String[] args) throws Exception {
Field swy = LazyMan.class.getDeclaredField("swy");
swy.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);// 无视私有构造器
LazyMan instance = declaredConstructor.newInstance();
// 通过反射破坏标志位
swy.set(instance, false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
都是反射惹的祸,这就需要引入枚举
反射不能破坏枚举
// 枚举本身是一个类,JDK1.5出现
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) {
EnumSingle instance1 = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE;
System.out.println(instance1);
System.out.println(instance2);
}
}
查看target目录中 EnumSingle 编译后的 class 代码
发现有无参构造,因此我们尝试再次使用反射
// 枚举本身是一个类,JDK1.5出现
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);// 破除私有
EnumSingle instance2 = declaredConstructor.newInstance();// 反射创建对象
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果报异常
异常的含义是,我们的枚举类中没有空参数构造器
但是我们在IDEA中 查看 EnumSingle.class 显示有无参构造,为什么?
我们使用命令行反编译查看一下,还是无参构造,还是有问题,这个代码也欺骗了我们,因为异常枚举类没有无空参构造
我们使用反编译神器 jad.exe
链接:https://pan.baidu.com/s/155pwsY7F4cLnN4KUxhK-ag
提取码:ztyp
把它复制到,class文件所在目录,在这个目录下执行命令行jad -sjava EnumSingle.class
,把它反编译成Java文件
打开这个Java文件,得到枚举类型的最终反编译源码
package com.swy.single;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/swy/single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i);
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
发现这里使用了一个有参构造器
private EnumSingle(String s, int i)
{
super(s, i);
}
所以我们再次改造上面的反射创建枚举对象的代码
class Test {
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
// 通过反编译器发现,枚举使用的是有参构造
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);// 破除私有
EnumSingle instance2 = declaredConstructor.newInstance();// 反射创建对象
System.out.println(instance1);
System.out.println(instance2);
}
}
运行测试,终于得到我们想要的结果
枚举不能通过反射创造对象,所以,枚举不能被反射破坏单例
为什么深入理解CAS?大厂必须深入研究底层!!!!修内功!操作系统、计算机网络原理、组成原理、数据结构
CAS:compareAndSet 比较并交换
传统原子类
public class CASDemo {
// CAS: compareAndSet 比较并交换
public static void main(String[] args){
AtomicInteger atomicInteger = new AtomicInteger(2020);
// boolean compareAndSet(int expect, int update)
// 第一个参数:期望值,第二个参数:更新值
// 如果实际值 和 我期望值相同,那么就更新
// 如果实际制 和 我期望值不同,那么就不更新
System.out.println(atomicInteger.compareAndSet(2020,2021));
System.out.println(atomicInteger.get());
// 因为期望值是 2020 ,实际值却变成了2021 所以会修改失败
// CAS 是 CPU 的并发原语
atomicInteger.getAndIncrement();//++操作
System.out.println(atomicInteger.compareAndSet(2020,2021));
System.out.println(atomicInteger.get());
}
}
进入 getAndIncrement 查看
进入 unsafe
valueOffset:内存地址的偏移值,而且value也被volatile修饰,保证不被指令重排
Unsafe 类提供的都是本地方法(native),包括上面的 getAndAddInt(+1的操作)
这个的方法的含义:如果var1这个对象,对应的值var2,是var5,那么就给var5+var4
这是一个内存操作,效率很高
Java 的 CAS 底层就是上述这个CAS,
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
这段代码本身也是一个自旋锁
CAS:
CAS 缺点:
解释一下:
public class CASDemo {
// CAS: compareAndSet 比较并交换
public static void main(String[] args){
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 捣乱的线程
System.out.println(atomicInteger.compareAndSet(2020,2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// 期望的线程
System.out.println(atomicInteger.compareAndSet(2020,666));
System.out.println(atomicInteger.get());
}
}
可以看到A不知情,并且正常进行了更新值,(这也是乐观锁的原理)
这是我们不期望的,如何解决这个问题?
解决 ABA 问题,对应的思想:就是使用了乐观锁
解决办法:带版本号的原子操作!
public class CASDemo {
// CAS: compareAndSet 比较并交换
public static void main(String[] args){
// AtomicInteger atomicInteger = new AtomicInteger(2020);
// 给值的同时 添加一个版本号,这里我们定为1
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
// 开启两个线程
new Thread(()->{
int stamp = atomicStampedReference.getStamp();// 获得版本号
System.out.println("a1=>"+stamp);
// 为保证两者的版本号是同一个,我们暂停一下
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 开始CAS 操作
// 参数1:期望值,参数2:更新值,参数3:版本号,参数4:版本号下一步的操作
System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2=>"+stamp);
// 再将期望值改回去,但是版本号还在累加
System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2=>"+stamp);
}, "a").start();
// b线程也对atomicStampedReference操作
// 和乐观锁原理相同
new Thread(()->{
int stamp = atomicStampedReference.getStamp();// 获得版本号
System.out.println("b1=>"+stamp);
// 为保证两者的版本号是同一个,暂停时间更长一些
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("b2=>"+stamp);
}, "b").start();
}
}
这样,在操作过程中,即使期望值发生了变化,我们也可以通过版本号得知发生了变化
这里有个小坑:
AtomicStampedReference
公平锁: 非常公平,先来后到,不允许插队
非公平锁: 非常不公平, 允许插队,默认都是非公平
public ReentrantLock() {
sync = new NonfairSync(); //无参默认非公平锁
}
// 重载方法可以传参
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();//传参为true为公平锁
}
释义:
public class Demo01 {
public static void main(String[] args) {
TestPhone phone = new TestPhone();
new Thread(()->{
//在调用sendMessage的方法时已经为phone加上了一把锁
//而call方法又为其加上了一把锁
phone.sendMessage();
}, "A").start();
new Thread(()->{
phone.sendMessage();
}, "B").start();
}
}
class TestPhone {
public synchronized void sendMessage() {
System.out.println(Thread.currentThread().getName() + "sendMessage");
call();// 这里也有所
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName() + "call");
}
}
按照正常逻辑,A执行完发短信,应该释放锁,B也有机会在A打电话之前就能发短信,
然而运行测试发现,每次都是A发短信、打电话之后,B才有机会开始发短信
这说明,A进入到内层锁的时候仍然保留着外层的锁
public class Demo02 {
public static void main(String[] args) {
Phone2 phone2 = new Phone2();
new Thread(()->{
phone2.sms();
}).start();
new Thread(()->{
phone2.sms();
}).start();
}
}
class Phone2{
Lock lock = new ReentrantLock();
public void sms(){
lock.lock();//细节:这个两把锁,两个钥匙
// lock 锁必须配对,否则就是死锁在里面,配对指的就是每加一把锁就要对应一个解锁
try {
System.out.println(Thread.currentThread().getName()+"=>sms");
call();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void call(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"=>call");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行测试,也会得到同样的效果,需要注意的是lock/unlock必须成对出现,否则程序会死在里面
在上面的案例中,已经看到自旋锁
不断地的循环去尝试,直到成功为止
手写代码实现一个自旋锁
public class SpinlockDemo {
// 默认
// int为 0
// thread 为 null
AtomicReference<Thread> atomicReference=new AtomicReference<>();
// 加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"===> mylock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){
System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
}
}
// 解锁
public void myUnlock(){
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"===> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
开始使用这个自旋锁,t2直到发现满足条件才 可以执行,一直在自旋
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
// 这是Java提供的锁,我们暂时不用
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
// 使用CAS实现我们写的自旋锁
SpinlockDemo spinlockDemo = new SpinlockDemo();
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t2").start();
}
}
比如说,两个线程互相抢夺资源,谁都不释放锁就会形成死锁
怎么排除死锁,让死锁的四个条件中至少有一个不成立
public class DeadLock {
public static void main(String[] args) {
String lockA= "lockA";
String lockB= "lockB";
new Thread(new MyThread(lockA,lockB),"t1").start();
new Thread(new MyThread(lockB,lockA),"t2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
}
}
}
}
程序卡死,两个线程都拿着对方想要的锁不释放
当程序卡着不输出,如何排查是死锁问题?
使用Java自带工具
在命令行输入 jps -l
查看目前运行的Java进程
2. 使用 jstack 进程号查看进程信息,找到死锁问题
查看详细的堆栈信息,通常可以在最后找到
面试中,工作如何排查
大部分人会说查看日志,但是你可以说查看堆栈信息!