JUC
Java JUC简介
【JUC就是以下三种包的简称】
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
【意】 在此包中增加了在 并发编程 ** 中很常用 的实用工具类,用于定义类似于线程的自定义子 系统,包括线程池、异步 IO 和轻量级任务框架。 提供可调的、灵活的线程池**。还提供了设计用于 多线程上下文中的 Collection 实现等。
先回顾一下以往线程的知识。
一、并发与并行区别
java真的能开启线程吗?
不能,Thread.start()调用的是一个本地方法,这个方法底层是C++
private native void start0();
并发: 多个线程操作一个资源(交替执行)
- CPU一核,模拟出来多条线程
- 本质:充分利用CPU的资源,所有公司都很看重
**并行:**多个线程同时执行(同时执行)
- CPU多核,多个线程同时执行
- 可以用线程池提高性能
通过代码获得电脑的核数:
public class demo1 {
public static void main(String[] args) {
//获取电脑CPU核数
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
线程的六个状态: Thread.State
public enum State {
/**
* 新建
*/
NEW,
/**
* 运行
*/
RUNNABLE,
/**
* 阻塞
*/
BLOCKED,
/**
* 等待,死死的等
*/
WAITING,
/**
*超时等待,时间过了就不等
*/
TIMED_WAITING,
/**
* 结束
*/
TERMINATED;
}
二、wait与sleep区别
【意】 都是让线程进行休眠
【注】 调用这两个方法一般都会用一个JUC里的工具类 TimeUnit ,TimeUnit 是一个枚举类,可以设置时、分、秒、微秒、天等休眠时间。TimeUnit.SECONDS.wait(100): 就是休眠100秒
1、来自不同包
TimeUnit.SECONDS.wait(): java.lang.Object
TimeUnit.SECONDS.sleep(): java.util.concurrent
2、锁的释放不同
wait: 会释放锁,交给系统管理
sleep: 不会释放锁,就像抱着锁睡觉。休眠后不会交出线程控制权,”占着茅坑不拉屎“。
3、使用的范围不同
wait: 必须再同步代码块使用
sleep: 可以再任何地方使用
三、Lock锁
【意】 Lock是一个接口,它所在的包是java.util.concurrent.locks
Lock锁与Synchronized区别
Synchronized 是关键字。Lock是一个类
Synchronized 无法判断获取锁的状态。Lock可以判断是否获取到锁
Synchronized 会自动释放锁 。 Lock锁必须手动释放锁,否则会发生死锁。
Synchronized 当线程1获得锁进入阻塞状态,线程2会一直等待。Lock不会一直等待
Synchronized 可重入锁,不可能中断,非公平锁。Lock可以中断,可以设置公平锁和非公平锁
Synchronized 适合锁少量的代码同步问题。Lock锁适合大量同步代码。
Lock接口的实现类
1. 可重入锁: ReentrantLock 类(常用)
【意】 可重入就是说一个线程已经获得某个锁,再进入该线程内部方法遇到别的锁会自动获取锁(前提锁对象是同一个对象,线程也是同一个线程),可以再次获取锁而不会出现死锁,创建lock对象
【构造方法】 ReentrantLock实现类有两个构造方法,他们分别是公平锁和非公平锁
公平锁: 顾名思义,先来后到,后面的线程必须等前面的线程执行完才能进入。
非公平锁: 可以插队,java默认使用非公平锁.
【为什么默认使用非公平锁?】
如:
一个线程执行3小时先获得锁,另一个线程执行3秒后到,如果使用公平锁 执行3秒的线程 要等这个 3小时线程执行完才能进入。
而因为非公平锁可以插队,可以让这个3秒的线程先执行,这就是为什么默认使用非公平锁,Synchronized同步锁也是如此。
//非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2、读写锁
【官方】
- A
ReadWriteLock
维护一对关联的locks
,一个用于只读操作,一个用于写入。read lock
可以由多个阅读器线程同时进行,只要没有作者。write lock
是独家的
【意】 共享锁和排他锁也叫读写锁,共享锁可以让多条线程同时读取数据,排他锁只能有一条线程进行写入。
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock(); //写锁 lock.writeLock().unlock();//释放
lock.readLock().lock();//读锁 lock.readLock().unlock();//释放
【代码】
package com.readwritelock;
import org.omg.PortableServer.THREAD_POLICY_ID;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
public static void main(String[] args) {
test2 test1 = new test2();
for (int i = 0; i < 6; i++) {
final int a = i;
new Thread(()->{test1.set(a+"a",a+"");},String.valueOf(i)).start();
}
for (int i = 0; i < 6; i++) {
final int a = i;
new Thread(()->{test1.get(a+"a");},String.valueOf(i)).start();
}
}
}
class test2 {
//读写锁
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Map<String, Object> map = new HashMap<>();
//写锁
public void set(String key, Object value) {
lock.writeLock().lock(); //上锁
try {
System.out.println(Thread.currentThread().getName() + "写入中");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();//释放
}
}
//读锁
public void get(String key) {
lock.readLock().lock();//上锁
try {
System.out.println(Thread.currentThread().getName() + "读取中");
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();//释放
}
}
}
//自定义缓存,未加读写锁
class Test1 {
private Map<String, Object> map = new HashMap<>();
public void set(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "写入中");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入成功");
}
public Object get(String key) {
System.out.println(Thread.currentThread().getName() + "读取成功" + map.get(key));
return map.get(key);
}
}
什么是锁,如何判断锁的是谁?
四、生产者和消费者
什么是生产者于消费者(就是读和写)?
生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
有实现线程之间的通信问题:那么面试官就会问你生产者和消费者
1、老版( Synchronized)的生产者和消费者
线程之间实现通信方法 Synchronized的 wait();等待唤醒,notify();通知唤醒
【代码】:
多个线程操作 num = 0;进行加减
【问题】 当只有A、B线程可以完美实现同步操作,但是当出现C、D线程后会出现了不同步操作。
以下代码为什么会出现不同步的情况?
package com;
public class demo3 {
public static void main(String[] args) {
A a = new A();
new Thread(() -> {
//生产者
try {
for (int i = 0; i < 10; i++) a.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
//消费者
try {
for (int i = 0; i < 10; i++)
a.deccrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
/*
有C、D线程后出现不同步
*/
new Thread(() -> {
//消费者
try {
for (int i = 0; i < 10; i++)
a.deccrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "C").start();
new Thread(() -> {
//生产者
try {
for (int i = 0; i < 10; i++)
a.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "D").start();
}
}
//生产者与消费者口诀:判断等待、业务、通知
class A {
private int num = 0;
public synchronized void increment() throws InterruptedException {
if (num != 0) {
this.wait();//等待唤醒
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程,+1完毕
this.notifyAll();//唤醒其他线程
}
public synchronized void deccrement() throws InterruptedException {
if (num == 0) {
this.wait(); //等待唤醒
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程,-1完毕
this.notifyAll();//唤醒其他线程
}
}
不同步流程图:
为什么出现了不同步?
这个问题也叫 虚假唤醒 原因是等待唤醒是使用了 if进行判断,if 只会进行一次判断,当两个线程同时进入+1方法,可能只会进行一次操作。
而jdk文档是让我们用while:
像在一个参数版本中,中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
所以防止虚假唤醒,我们只需把if改成while判断就可以了,为什么while就可以解决这个问题?
因为while进行一次判断后不会停,还会继续判断。而if只会判断一次就结束了。
2、JUC版的生产者和消费者
【意】 Lock可以代替 Synchronize类和方法wait(),notifyAll()。
【使用】
ReentrantLock(): Lock接口的实现类ReentrantLock(); 可重入锁。
Lock lock = new ReentrantLock(); //创建可重入锁对象
创建Condition()对象,调用代替等待唤醒,和通知唤醒方法。
**await :**代替wait()
**signalAll :**代替notifyAll()
Condition condition = lock.newCondition();
改变之前Synchronized示例代码
class B {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private int num = 0;
public void increment() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.tryLock();
}
}
public void deccrement() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
condition.await(); //等待
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程,-1完毕
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.tryLock();
}
}
}
【意】 使用Lock运行的结果与Synchronized结果是一样的。
【注】 一个新技术出现便不是只对之前的技术进行覆盖,如果是这样就没有必要使用Lock了。Lock能够实现Synchronized做不到的 精准唤醒 。
3、Lock精准唤醒
【意】 精准的唤醒对应的线程,也就是按自己需要的顺序执行线程,比如 A线程 —> B线程 —> C线程
【实现代码】
package com;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Lock2 {
public static void main(String[] args) {
C a = new C();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++)
a.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++)
a.b();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++)
a.c();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "C").start();
}
}
//口诀:判断等待、业务、通知
class C {
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
private int num = 1;
public void a() throws InterruptedException {
lock.lock();
try {
while (num != 1) {
condition1.await();
}
num = 2;
System.out.println(Thread.currentThread().getName() + "=> AAAAAAAAAA" );
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.tryLock();
}
}
public void b() throws InterruptedException {
lock.lock();
try {
while (num != 2) {
condition2.await(); //等待
}
num = 3;
System.out.println(Thread.currentThread().getName() + "=> BBBBBBBBBB");
//通知其他线程,-1完毕
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.tryLock();
}
}
public void c() throws InterruptedException {
lock.lock();
try {
while (num != 3) {
condition3.await(); //等待
}
num = 1;
System.out.println(Thread.currentThread().getName() + "=>CCCCCC");
//通知其他线程,-1完毕
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.tryLock();
}
}
}
五、八锁现象
【注】
1、Synchronized 锁的是调用者,两个对象调用者的锁互不影响。
2、static Synchronized 锁的是Class,是一个模板。一个类只有一个Class。
3、如果一个调用者调用static Synchronized 锁另一个调用Synchronized 锁,因为这两个锁的概念是不同的,并不是同一个锁,所以这两个调用者的锁互不影响。所以不会进行同步操作。
六、Callable< V >
【JDK文档说明】
Callable接口类似于 Runnable,因为它们都是为其实例可能由另一个线程执行的类设计的。然而, Runnable 不返回结果,也不能抛出被检查的异常。
【意】 可以有返回值,可以抛出异常。
【方法】
**V call() :**返回值需要与接口的集合类型相同。
因为Thread只与Runnable接口有关系,Callable不能直接联系到Thread,所以需要一个中间人。
FurureTask实现了Runnable接口,FurureTask的有两个有参构造,一个有参是Runnable,另一个有参是Callable。
【代码】
package com.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Callabletest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Helloworld helloworld = new Helloworld();
FutureTask futureTask = new FutureTask(helloworld); //调用Callable构造
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start();
String o = (String) futureTask.get();//获得Callable返回值,get会有阻塞,一般放最后一行,获着异步操作。
System.out.println(o);
}
}
class Helloworld implements Callable<String> { //实现Claable接口
public String call(){ //返回值必须与接口的泛型类型一样。
System.out.println("嘿嘿");
return "你好";
}
}
七、常用辅助类
1、CountDownLatch 减法计数器
【意】 允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助
【方法】
-
-
Modifier and Type Method and Description void**
**await()
计数器归0后被唤醒,允许继续往下执行。boolean
await(long timeout, TimeUnit unit)
使当前线程等待直到锁存器计数到零为止,除非线程为 interrupted或指定的等待时间过去。void
countDown()
减少锁存器的计数,如果计数达到零,释放所有等待的线程。long
getCount()
返回当前计数。String
toString()
返回一个标识此锁存器的字符串及其状态。
-
【代码】
package com.callable;
import java.util.concurrent.CountDownLatch;
public class CountDownLathTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"出去");
cdl.countDown(); //每次减1
},String.valueOf(i)).start();
}
cdl.await();//全部减完下面继续运行,否则阻塞
System.out.println("全部都运行完了!");
}
}
2、 CyclicBarrier 加法计数器
【意】 与减法差不多,一个是加一个是减
【方法】
【代码】
package com.callable;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(5,()->{
System.out.println("达到运行目标");
});
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"运行");
try {
barrier.await();//当运行的线程达到5时,运行lambda表达式
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
3、Semaphore 信号量
【意】 设置线程最多有多少个,多个共享资源互斥的使用!并发限流,控制大的线程数!
【代码】
package com.callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.jar.Manifest;
public class SemaphoreTest {
public static void main(String[] args) {
//线程数量:停车位 3 !在限流方面用的多
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
final int a = i;
new Thread(()->{
try {
semaphore.acquire();//获得车位
System.out.println(a+"号进来");
TimeUnit.SECONDS.sleep(2);
System.out.println(a+"号出去");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//释放车位
}
},String.valueOf(i)).start();
}
}
}
原理:
semaphore.acquire() : 获得,假设线程已满,其他线程会等待,知道被释放为止
semaphore.release():释放,将当前的信号量释放+1,然后唤醒等待的线程。
八、线程不安全
package com.collections;
import java.util.*;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArrayList;
public class demo1 {
// java.util.ConcurrentModificationException 并发修改异常
public static void main(String[] args) {
// List list = new ArrayList(); 并发下集合读写会不安全
/* 解决办法:
1、List list = new Vector(); Vector的add()方法使用了synchronized同步锁
2、List list = Collections.synchronizedList(new ArrayList<>());
3、List list = new CopyOnWriteArrayList(); 写入时复制
*/
Set set = new ConcurrentSkipListSet();
for (int i = 0; i < 100; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
九、阻塞队列
背景
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
【意】
FIFO就是先进先出
有一个通道,左边口子是写数据,右边口子是读数据,如果右边没有读取数据,这个通道就会一直被写入,当这个通道满了就要阻塞等待。反之 当这个通道的数据是零时,读取数据是读不到的,也要阻塞。必须等待这个通道有数据。
**写:**如果队列满了,就必须阻塞等待。
**读:**如果队列是空的,必须阻塞等待生产。
不是新东西,BlockingQueue继承Queue =>Queue继承Collection
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KV2Agitv-1655823180745)(C:\Users\独依栏窗\AppData\Roaming\Typora\typora-user-images\image-20200225164513667.png)]
【实现阻塞队列】
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | boolean add(E e) | boolean offer(E e) | void put(E e) | offer(E,Long,TimeUnit) |
输出 | T remove() | T poll() | T take() | poll(Long,TimeUnit) |
检查首个元素 | element() |
抛出异常:
//会抛出异常
class Test1{
public void test(){
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
System.out.println(arrayBlockingQueue.add("a"));
System.out.println(arrayBlockingQueue.add("b"));
System.out.println(arrayBlockingQueue.add("c"));
//当队列满了,再添加抛出 异常 IllegalStateException: Queue full 队列已满
// System.out.println(arrayBlockingQueue.add("c"));
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
//如果首元素也没有,抛出:java.util.NoSuchElementException 没有这个元素
System.out.println(arrayBlockingQueue.element());
// 对列没数据了,抛出:java.util.NoSuchElementException
// System.out.println(arrayBlockingQueue.remove());
}
}
不抛出异常:
//不抛出异常
class Test2{
public void test(){
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
System.out.println(linkedBlockingQueue.offer("a"));
System.out.println(linkedBlockingQueue.offer("b"));
System.out.println(linkedBlockingQueue.offer("c"));
//当队列满了,返回flase
// System.out.println(linkedBlockingQueue.offer("d"));
System.out.println(linkedBlockingQueue.poll());
System.out.println(linkedBlockingQueue.poll());
System.out.println(linkedBlockingQueue.poll());
//如果首元素也没有,输出空
// System.out.println(linkedBlockingQueue.poll());
}
}
阻塞等待:
//阻塞等待
class Test3{
public void test() throws InterruptedException {
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
linkedBlockingQueue.put("a");
linkedBlockingQueue.put("c");
linkedBlockingQueue.put("b");
//当队列满了,进入阻塞等待
// linkedBlockingQueue.put("c");
System.out.println(linkedBlockingQueue.take());
System.out.println(linkedBlockingQueue.take());
System.out.println(linkedBlockingQueue.take());
//如果首元素也没有,进入阻塞等待
// System.out.println(linkedBlockingQueue.takce());
}
}
超时等待:
//超时等待
class Test4{
public void test() throws InterruptedException {
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
System.out.println(linkedBlockingQueue.offer("a" ));
System.out.println(linkedBlockingQueue.offer("c" ));
System.out.println(linkedBlockingQueue.offer("c"));
//当队列满了,进入超时等待,等待2秒,没有位置就离开
System.out.println(linkedBlockingQueue.offer("c", 2, TimeUnit.SECONDS));
System.out.println(linkedBlockingQueue.poll());
System.out.println(linkedBlockingQueue.poll());
System.out.println(linkedBlockingQueue.poll());
//当队列空了,进入超时等待,等待2秒,没有数据就离开
System.out.println(linkedBlockingQueue.poll(2,TimeUnit.SECONDS));
}
}
SynchronousQueue 同步队列
【意】 没有容量,只能放一个元素,必须等这个元素出来,才能放下一个元素。
//同步队列
class Test5{
public void test() throws InterruptedException {
SynchronousQueue synchronousQueue = new SynchronousQueue();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName()+"2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName()+"3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+synchronousQueue.poll());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+synchronousQueue.poll());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+synchronousQueue.poll());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
十、线程池
**记住:**三大方法、七大参数、四种拒绝策略
【背景】
如果程序有大量短时间的线程任务,由于创建线程和销毁线程需要和底层操作系统交互,大量时间都耗费在创建和消费的线程上,因而比较浪费时间,效率低。
而线程池里的每一个线程任务结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率
【优点】
1、提高程序响应速度,降低资源消耗
2、控制线程数量,防止程序崩溃
3、方便管理。
4、线程复用、可以控制最大并发数、管理线程
1、创建线程的:三大方法
【阿里巴巴开发手册规定】
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资
源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题。【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
Integer.MAX_VALUE = 最大值21亿
**OOM **: 内存用完了 推荐:https://blog.csdn.net/qq_42447950/article/details/81435080
【三大方法】
//阿里巴巴不推荐使用以下三种方法
public class Demo1 {
public static void main(String[] args) {
// ExecutorService executorService = Executors.newSingleThreadExecutor();//单个线程池 最大一起执行线程数1条
// ExecutorService executorService =Executors.newFixedThreadPool(5); //固定线程池 最大一起执行线程数5条
ExecutorService executorService = Executors.newCachedThreadPool(); //可伸缩线程 按正在执行的线程数伸缩
try {
for (int i = 0; i < 10; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
2、七大参数
【源码分析】
阿里巴巴不推荐的以上三大方法都是调用了 ThreadPoolExecutor的构造方法。
所以需要我们自己去实现 ThreadPoolExecutor 定义线程池,因为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;
}
【调用 ThreadPoolExecutor】
自定义 ThreadPoolExecutor
package com.pool;
import java.util.concurrent.*;
// Executors 工具类、3大方法
// Executors 工具类、3大方法
/**
拒绝策略
* new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和早的竞争,也不会 抛出异常!
*/
public class Demo2 {
public static void main(String[] args) {
//自定义线程池! 使用ThreadPoolExecutor 执行器
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
//这个线程池最大承载数:Deque + max = 8
//超过会抛出 RejectedExecutionException
//异常处理块 保证循环正常结束后才关闭线程
try {
for (int i = 0; i < 9; i++) {
threadPoolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}
3、四种拒绝策略
/**
* new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和早的竞争,也不会 抛出异常!
*/
十一、四种函数式接口
新时代的程序写代码:lambda 、链表 、 函数式接口 、Stream流式计算
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
// 泛型、枚举、反射
// lambda表达式、链式编程、函数式接口、Stream流式计算
// 超级多FunctionalInterface // 简化编程模型,在新版本的框架底层大量应用!
// foreach(消费者类的函数式接口)
【意】 只有一个方法的接口就是函数式接口,下面画红线的就是要说的四种函数式接口,其余的都是根据这四种函数接口的衍生,在很多框架种常用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2JpXvfU1-1655823180747)(C:\Users\独依栏窗\AppData\Roaming\Typora\typora-user-images\image-20200226205310489.png)]
1、消费型接口(Consumer)
【意】 只输入不输出,也就是只有参数,没有返回值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w9dcgY5c-1655823180749)(C:\Users\独依栏窗\AppData\Roaming\Typora\typora-user-images\image-20200226211604297.png)]
package com.function;
import java.util.function.Consumer;
/**
* Consumer 消费型接口
* (str)->{ System.out.println(str); }; 等于实现类
* consumer.accept(s); 调用上面的输出方法
*/
public class ConsumerTest {
public static void main(String[] args) {
String s = "你好啊";
Consumer<String> consumer = (str)->{ System.out.println(str); }; //创建对象
consumer.accept(s);
}
}
2、函数式接口(Function)
【意】 有一个输入,有返回值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bsxvQarU-1655823180749)(C:\Users\独依栏窗\AppData\Roaming\Typora\typora-user-images\image-20200226213602740.png)]
package com.function;
import java.util.function.Function;
public class FuncationTest {
public static void main(String[] args) {
//参数必须为String,返回值必须为Integer
Function<String,Integer> function = (str)->{
System.out.println("输入:"+str);
return 123;
};
System.out.println("返回:"+function.apply("321"));
}
}
3、断定型接口(Predicate)
【意】有一个输入参数、返回值只能是boolean值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BvNy7Sdo-1655823180750)(C:\Users\独依栏窗\AppData\Roaming\Typora\typora-user-images\image-20200226212359215.png)]
package com.function;
import java.util.function.Predicate;
public class PredicateTest {
public static void main(String[] args) {
Integer i = 0;
Predicate<Integer> predicate = (a)->{return a==0;};
System.out.println(predicate.test(i)); //返回true
}
}
4、供给型接口 (Supplier)
【意】 没有参数,只有返回值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZdK9vbKB-1655823180750)(C:\Users\独依栏窗\AppData\Roaming\Typora\typora-user-images\image-20200226214300504.png)]
package com.function;
import java.util.function.Supplier;
public class SupplierTest {
public static void main(String[] args) {
Supplier<String> supplier = ()->{return "123";}; //输出必须是String
System.out.println("输出:"+supplier.get());
}
}
十二、Stream流式计算
什么是Stream流式计算?
在大数据本质就是存储和计算,像集合、数据库就是存储数据,而计算都应该交给流来操作。
Stream是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合讲的是数据,流讲的是计算。
Stream API(java.util.stream.*) Stream 是JAVA8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。
使用Stream API对集合数据进行操作,就类似于使用SQL执行数据查询一样。也可使用StreamAPI做并行操作,总之,StreamAPI提供了一种高效且易于使用的处理数据的方式。
【注】
1 . Stream自己不会存储元素。
2 . Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream
3 . Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tSMmMgQS-1655823180751)(C:\Users\独依栏窗\AppData\Roaming\Typora\typora-user-images\image-20200226223713869.png)]
package com.stream;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* 题目要求:一分钟内完成此题,只能用一行代码实现! *
* 现在有5个用户!筛选:
* 1、ID 必须是偶数
* 2、年龄必须大于23岁
* 3、用户名转为大写字母
* 4、用户名字母倒着排序
* 5、只输出一个用户!
*/
public class StreamTest {
public static void main(String[] args) {
User u1 = new User(1, 23, "a");
User u2 = new User(2, 24, "b");
User u3 = new User(3, 25, "c");
User u4 = new User(4, 26, "d");
User u5 = new User(5, 27, "e");
User u6 = new User(6, 26, "f");
List<User> list = Arrays.asList(u1,u2,u3,u4,u5,u6); //只存储
//计算交给Stream流 filter筛选
list.stream()
.filter((u)->{return u.getId()%2==0;})//1、ID 必须是偶数
.filter((u)->{return u.getAge()>23;})//2、年龄必须大于23岁
.map((u)->{return u.getName().toUpperCase();})//3、用户名转为大写字母
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})//4、用户名字母倒着排序
.limit(1)//5、只输出一个用户!
.forEach(System.out::println);//输出
}
}
class User {
private int id;
private int age;
private String name;
//...省略封装
}
十三、ForkJoin
什么是ForkJoin?
ForkJoin是JAVA7的一个用于并行执行任务工具类,ForkJoin也叫拆分连接,可以把一个大任务拆成一个小任务,最终汇总。
拆分连接:
工作窃取算法
工作窃取(work-stealing)算法是指某个线程执行完后从其他未执行完的队列里窃取任务来执行。
为什么需要工作窃取?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。
有的队列的线程执行完了,让它空在那里很浪费,所以就让这个线程去其他未执行的队列里窃取一个任务来执行。
而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
优点 充分利用线程进行计算,减少了线程间的竞争
缺点 在某些情况下还是会有竞争,比如当一个队列已经执行到了最后一个任务当是没执行完,另一个执行完的进来窃取,就会出现竞争。
【代码步骤】
1、创建一个类让它继承 ForkJoinTask 接口 的抽象实现类
ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它有两个抽象子类:RecursiveAction和RecursiveTask。
RecuresiveAction (递归事件): 它的compute()方法没有返回值,抽象方法必须实现,代表有返回值的任务。
RecuresiveTask (递归任务):它的compute()方法有返回值,抽象方法必须实现,代表没有返回值的任务。
2、实现compute()方法
/*
计算 1 加到 10_0000_0000的结果
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long strat; //初始值
private Long end; //结束值
private Long temp = 1000L; //临界点
public ForkJoinDemo(Long strat, Long end) {
this.strat = strat;
this.end = end;
}
@Override
protected Long compute() {
if ((end - strat) < temp) {
Long sum = 0L;
for (Long i = strat; i <= temp; i++) {
sum += i;
}
return sum;
} else {
Long middle = (strat + end) / 2;
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(strat, middle); //划分为子任务计算 0 ~ 5000
forkJoinDemo1.fork(); //拆分任务,会判断当前线程是否为 ForkJoinPool 类型的线程,不是则会抛出AbstractQueuedSynchronizer$Node
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle + 1, end); //划分为子任务计算 5000 ~ 10000
forkJoinDemo2.fork();//拆分任务,异步计算
return forkJoinDemo1.join() + forkJoinDemo2.join();
}
}
}
3、用 ForkJoinPool 调用自己写的类
什么是ForkJoinPool?
java提供了ForkJoinPool来支持将一个任务拆成多份计算,再汇总。
ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池
【注】必须使用ForkJoinPool的submit(ForkJoinTask task)或者invoke(ForkJoinTask task)来执行指定的ForkJoinTask任务。
因为FockJoinTast的fork()方法会判断是否为ForkJoinPool类型的线程。
//调用FockJoinDemo
class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
}
//普通方式计算10_0000_0000用时 12959 毫秒
//使用ForkJoin 用时8958毫秒
static void test1() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool(); //创建 ForkJoinPool
ForkJoinTask<Long> forkJoinTask = new ForkJoinDemo(0L, 10_0000_0000L); //demo中初始化参数
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTask); //提交一个ForkJoinTask来执行。
Long sum = submit.get(); //获得返回的值
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + " 时间:" + (end - start));
}
}
最高效计算,Stream并行流
上面是ForkJoin的计算方式,比起普通计算方式的确提高了许多,都是并不是最高效的,我们还可以使用Stream并行流进行计算
并行流
并行流就是把一个内容分成多个数据块,并用不同的线程分成多个数据块,并用不同的线程分别处理每个数据块的流。实现方式与ForkJoin差不多
JAVA8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过parallel() 与sequential() 在并行流与顺序流之间进行切换。其实JAVA8底层是使用JAVA7新加入的Fork/Join框架
//使用Stream 并行流 效率最高 耗时396
static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
Long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + " 时间:" + (end - start));
}
十四、异步回调
多线程处理提高性能的根本本质在哪?其实就是将串行的处理步骤进行并行的处理,其实总时间是没有缩短的。也就是以前一个人干活需要10个小时,而十个人干同样的活需要1小时,从而缩短处理时间。但是如果干活有先后限制怎么办?例如工作中:测试前必须编码,编码前必须设计,设计前必须需求分析,分析前……如何提高这种情况的性能呢?或者说是如何让这中情况下的线程更加充分利用呢?Future模式——异步调用
【调用类】
使用Future的实现类CompletableFuture
Future: 表示未来希望执行的任务。Future 初衷:对未来的某个事件的结果建模
CompletableFuture :表示 已完成的未来任务,意思是 用这个类可以让未来的任务完成。
package com.asyn;
import javax.rmi.CORBA.Util;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class CompletableFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//runAsync() 没有返回结果的异步回调
// CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println("异步执行完成");
// });
// System.out.println("异步任务休眠了2秒");
// completableFuture.get(); //等待这个未来完成的任务,然后返回结果。不能放在前面会阻塞
//
//supplyAsync 供给型异步回调 有返回值
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("完成异步任务");
int i = 10/0;
return 1024;
});
System.out.println("异步任务休眠了2秒");
System.out.println(completableFuture.whenComplete((t,u)->{
System.out.println("t=>" + t); // 正常的返回结果
System.out.println("u=>" + u); // 错误信息,没有异常为 null
}).exceptionally(e->{
System.out.println(e.getMessage());
return 500;
}).get());
}
}
十五、JMM
什么是JMM?
JMM(Java Memory Model):Java内存模型。
JMM是一种约定或者概念,是一种不存在的东西,
【个人理解】
JMM内存模型中规定了所有的变量都存储在主内存中。
而每个线程都有一个工作内存,工作内存保存了这个线程所使用到的主内存变量副本拷贝。
对变量的所有操作(读取、赋值等)都必须在工作内存进行,不允许直接读取主内存的变量。
不同的线程之间无法直接访问到对方的工作内存,需要同过主内存进行传递。
而当一个线程拷贝的变量被修改后,必须立刻刷新到主内存中。并且要读取主内存最新的值到工作内存中。
以达到 缓存一致性协议。
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
主内存工作内存的8种操作
对这些8种操作分析:
但是引发了一个问题:
A线程并不知道flag变量被B线程修改了。引发了可见性问题。
代码演示:
【结果】使用没用声明volatile的num,while会一直循环,因为while所在线程它不知道num被改变为1了。
package com.tvolatile;
import java.util.concurrent.TimeUnit;
public class TestVolatile {
//private static volatile int num = 0; //保证可见性
private static volatile int num = 0; //不保证可见性
public static void main(String[] args) {
new Thread(()->{
while (num == 0){ //无限循环
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
**那怎么保证数据同步问题呢?**可以使用Volatile、final或synchronized,Lock。
十六、Volatile
什么是Volatile?
是一个轻量级的同步关键字。
作用:
1、保证可见性
2、不保证原子性
3、防止指令重排。
保证可见性
【意】是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。
在主存和工作内存之间有一条总线,并且线程会启用嗅探机制(监测)一旦主存发生改变就会把数据拷贝过去。
修改上面的代码,给变量添加volatile关键字,这样就保证了变量的可见性,while不会再死循环。
【代码】
package com.tvolatile;
import java.util.concurrent.TimeUnit;
public class TestVolatile {
private volatile static int num = 0; //保证可见性
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
不保证原子性
什么是原子性?
它所做的对数据修改操作要么全部执行,要么完全不执行,不能被打扰的,也不能被分割。
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。
package com.tvolatile;
public class TestVolatile2 {
/**
*不保证原子性,每次计算的值都不同,计算的值应为30000
*/
private static volatile int num = 0;
static void numPlus(){
num++; // num++ 不是原子性操作,可以反编译看到它
}
public static void main(String[] args) {
for (int i = 0; i < 30; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
numPlus();
}
}).start();
}
while (Thread.activeCount()>2){ //为什么要大于2,因为JAVA有两条线程 一条是主线程,一条是GC线程
Thread.yield();
}
System.out.println(num);
}
}
//输出:27597 反正很难为正确的:30000
那怎么保证原子性呢?可以使用JUC的原子类型 所在包:java.util.concurrent.atomic
修改上面代码:
package com.tvolatile;
import java.util.concurrent.atomic.AtomicInteger;
public class TestVolatile2 {
/**
*既保证可见性又保证了保证原子性,计算的值为30000
*/
private volatile static AtomicInteger num = new AtomicInteger(0);
static void numPlus(){
num.getAndIncrement(); //每次+1
}
public static void main(String[] args) {
for (int i = 0; i < 30; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
numPlus();
}
}).start();
}
//为什么线程数量要大于2,因为JAVA有两条线程 一条是主线程,一条是GC线程
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(num);
}
}
防止指令重排
什么是指令重排?
【个人理解】系统不会按我们预想的顺序去执行指令。
比如:有A B C D四个属性 ,按我们预期它们的执行顺序应该是 ABCD ,
但是由于系统会指令重排,所以可能会变成DCBA或者BCDA,CBDA。
Volatile利用内存屏障就防止了这种情况的发生。
Volatile内存屏障作用:
1、保证特定的操作的执行顺序
2、可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)
十七、彻底玩转单例模式
什么是单例模式?
保证类在内存中只能有一个对象,且构造私有。
**主要解决:**一个全局使用的类频繁地创建与销毁。
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
**缺点:**没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
饿汉模式
【意 】在类加载就创建了实例,
【优点】多线程安全,在类一加载就初始化了实例
【缺点】资源浪费
package com.singleton;
/**
* 饿汉模式
* 在类一加载的时候就创建了实列
* 缺点是,会出现资源浪费
*/
public class Hungry {
//资源浪费了
Byte[] byte1 = new Byte[1024];
Byte[] byte2 = new Byte[1024];
Byte[] byte3 = new Byte[1024];
Byte[] byte4 = new Byte[1024];
private Hungry(){
}
private static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
【注】这个单例是不安全的,我们可以用反射破坏这个单例。
【使用反射破坏】
class test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<Hungry> declaredConstructor = Hungry.class.getDeclaredConstructor(null);
//为true 表示可以获得私有成员
declaredConstructor.setAccessible(true);
//没通过这个单例类u getInstance()方法 就创建了实例。
Hungry hungry = declaredConstructor.newInstance();
}
}
懒汉模式
【意】当需要的时候才会创建类的实例
【优点】多线程不安全
**【缺点】**必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)
package com.singleton;
/**
* 当要使用的时候才创建实例
*/
public class Lazy {
private Lazy(){
System.out.println("线程"+Thread.currentThread().getName()+"创建了一个实例");
}
private static Lazy lazy ;
//会出现线程安全问题,
public static Lazy getInstance(){
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
【模拟多线程不安全】
class test2{
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
Lazy.getInstance();
},i+"").start();
}
}
}
/*输出:
线程1创建了一个实例
线程3创建了一个实例
线程4创建了一个实例
线程2创建了一个实例
线程0创建了一个实例
*/
为什么会出现这种情况?
指令重排,并且创建实例不是一个原子性操作。
创建实例的顺序:
1、分配内存空间
2、执行构造方法、初始化对象
3、把这对象指向这个内存空间
到了多线程,就会出现——A线程还没执行完,B线程来了发现为null继续创建对象。
【注】依然可以用反射来破坏这种单例
【使用反射破坏】
class test2{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
//为true 表示可以获得私有成员
declaredConstructor.setAccessible(true);
//没通过这个单例 getInstance()方法 就获得了实例。
Lazy lazy = declaredConstructor.newInstance();
}
}
DCL懒汉模式
什么是DCL?
DCL也叫 双检锁/双重校验锁(DCL,即 double-checked locking)。
也就是说我们给这个单例上双重检测,并且加上锁。防止多线程的不安全发生。
【优点】多线程安全
package com.singleton;
public class DCLLazy {
private DCLLazy() {}
//volatile防止指令重排,synchronized可以保证原子性
private static volatile DCLLazy dclLazy = null;
public static DCLLazy getInstance(){
//如果这个实例不为空了其他线程都不能进来,还可以防止线程重复拿到锁,浪费资源
if (dclLazy == null) {
//当第一个线程进入后会给这个类上锁
synchronized (DCLLazy.class) {
if (dclLazy == null) {
dclLazy = new DCLLazy();
}
}
}
return dclLazy;
}
}
【注】还是不能防止反射破坏这个单例
【使用反射破坏】
class test3{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<DCLLazy> declaredConstructor = DCLLazy.class.getDeclaredConstructor(null);
DCLLazy dclLazy = declaredConstructor.newInstance();
DCLLazy dclLazy2 = declaredConstructor.newInstance();
System.out.println(dclLazy.equals(dclLazy2)); //输出false ,说明不是同一个对象
}
}
那么我们给这个DCL加强下,在构造加上
private DCLLazy() {
synchronized (DCLLazy.class) { //加锁,保证多线程下原子性
if (judge == false) { //第一次创建实例为false,并修改judge为true
judge = true;
} else {
throw new RuntimeException("不要使用反射来破坏");
}
}
}
再次运行main方法,发现报错了,控制台出现了我们抛出了异常,那是不是代表解决了呢?并不是,反射能获取私有的属性并且改变值。
Caused by: java.lang.RuntimeException: 不要使用反射来破坏
at com.singleton.DCLLazy.<init>(DCLLazy.java:15)
... 6 more
修改反射代码:
class test3 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Field judge = DCLLazy.class.getDeclaredField("judge"); //获得属性
judge.setAccessible(true);
Constructor<DCLLazy> declaredConstructor =DCLLazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//创建了第一个实例
DCLLazy dclLazy = declaredConstructor.newInstance();
//出现把judge属性设为false,这样就能够继续创建实例了
judge.set(declaredConstructor,false);
DCLLazy dclLazy2 = declaredConstructor.newInstance();
//依然输出false ,说明依然不是同一个对象
System.out.println(dclLazy.equals(dclLazy2));
}
}
静态内部类
实现了懒加载,只有当内部类被调用了,才会被类加载器加载。
【优点】多线程安全,这种方式能达到双检锁方式一样的功效,但实现更简单
package com.singleton;
public class StaticSingleton {
private StaticSingleton(){}
public StaticSingleton getInstance(){
return instanceClass.STATIC_SINGLETON;
}
//静态内部类
public static class instanceClass{
private static final StaticSingleton STATIC_SINGLETON = new StaticSingleton();
}
}
【注】依然能用反射破坏这个单例
【反射破坏】
class test4{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<StaticSingleton> declaredConstructor = StaticSingleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //为true 表示可以获得私有成员
StaticSingleton ss = declaredConstructor.newInstance(); //没通过这个单例 getInstance()方法 就获得了实例。
}
}
枚举
【优点】这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,多线程安全。
**描述:**这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
【注】可以防止反射。
public enum EnumSingleton {
INSTANCE;
public void outPut(){
System.out.println("红火火恍恍惚惚");
}
}
我们来看看编译后的class文件:
发现被编译后的class文件有一个私有的构造方法,那我们试试用反射能不能调用到这个构造。
package li;
public enum EnumSingleton {
INSTANCE;
private EnumSingleton() {
}
public void outPut() {
System.out.println("红火火恍恍惚惚");
}
}
【使用反射尝试破坏】
class test5{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
enumSingleton.outPut();
}
}
发现报错了,告诉我们,没有这个构造方法。
Exception in thread "main" java.lang.NoSuchMethodException: li.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at li.test5.main(EnumSingleton.java:15)
我们使用jad进行反编译,先要打开jad,然后在cmd输入:jad -sjava EnumSingleton.class
然后出现了一个java后缀的源文件:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingleton.java
package li;
import java.io.PrintStream;
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(li/EnumSingleton, name);
}
//看这里,发现是有两个参数的有参构造方法
private EnumSingleton(String s, int i)
{
super(s, i);
}
public void outPut()
{
System.out.println("\u7EA2\u706B\u706B\u604D\u604D\u60DA\u60DA");
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
那我们用反射看能不能使用这个有参构造
class test5{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//使用有参构造
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
enumSingleton.outPut();
}
}
抛出了异常:告诉你不能使用反射来创建枚举对象
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at li.test5.main(EnumSingleton.java:17)
总结
一般情况下,不建议使用第 2 懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。
十八、深入理解CAS
什么是CAS?
就是比较并交换
十九、原子引用
什么是原子引用?
主物理内存有一个共享变量值为5,有两个线程T1,T2,他们都有自己的工作内存,并且有变量的拷贝(快照5),T1线程现在把值改为2019,然后,写回主物理内存并通知其它线程可见(加volatile),这个过程中。T1的期望值为5,要跟主物理内存的值5进行对比,如果相同,说明没有其它线程改变,则将主物理内存的值改为2019,并返回true。这时,T2线程也将自己工作内存的值改为了1024,但是,当写回主物理内存时,发现自己的期望值(5),与现在的主物理内存(2019)不一样了,就会写入失败,返回false,此时需要重新获得主物理内存的新值
————————————————
原文链接:https://blog.csdn.net/qq_38826019/article/details/110704711
乐观锁,设置版本号
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(li/EnumSingleton, name);
}
//看这里,发现是有两个参数的有参构造方法
private EnumSingleton(String s, int i)
{
super(s, i);
}
public void outPut()
{
System.out.println("\u7EA2\u706B\u706B\u604D\u604D\u60DA\u60DA");
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
那我们用反射看能不能使用这个有参构造
```java
class test5{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//使用有参构造
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
enumSingleton.outPut();
}
}
抛出了异常:告诉你不能使用反射来创建枚举对象
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at li.test5.main(EnumSingleton.java:17)
总结
一般情况下,不建议使用第 2 懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。
十八、CAS
CAS是用于保证原子类线程安全的。
在jdk5之前是用sync锁来实现,需要加锁。
CAS就是比较并交换
十九、原子引用
什么是原子引用?
主物理内存有一个共享变量值为5,有两个线程T1,T2,他们都有自己的工作内存,并且有变量的拷贝(快照5),T1线程现在把值改为2019,然后,写回主物理内存并通知其它线程可见(加volatile),这个过程中。T1的期望值为5,要跟主物理内存的值5进行对比,如果相同,说明没有其它线程改变,则将主物理内存的值改为2019,并返回true。这时,T2线程也将自己工作内存的值改为了1024,但是,当写回主物理内存时,发现自己的期望值(5),与现在的主物理内存(2019)不一样了,就会写入失败,返回false,此时需要重新获得主物理内存的新值
————————————————
原文链接:https://blog.csdn.net/qq_38826019/article/details/110704711
乐观锁,设置版本号