面试常见:
JRE:java 开发环境,包含了 JVM( C++ 语言编写的)
一个个(.class)类文件 |
---|
JRE–JVM |
操作系统(Windows,Linux,Mac) |
硬件体系(Intel,Spac…) |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56dSQAjb-1648561802064)(link-picture\image-20220107161935856.png)]
Java栈、本地方法栈、程序计数器不会有垃圾回收
,否则程序会死掉
99% JVM 调优都是在方法区和堆中调优,Java 栈、本地方法栈、程序计数器是不会有垃圾存在的
类是模板,是抽象的,类实例化得到的对象是具体的。所有的对象反射回去得到的是同一个类模板
作用:加载 .class 文件,得到 Class
类加载器的种类
虚拟机自带的
加载器
启动类(根)
加载器:BootstrapClassLoader
扩展类
加载器:ExtClassLoader
应用程序类
加载器:AppClassLoader
双亲委派机制:保证安全,逐级查找:AppCL–>ExtCL–>BootstrapCL
双亲委派机制
类加载器收到类加载
的请求,将这个请求向上委托
给父类加载器
去完成,一 直向上委托,直到启动类加载器
,启动加载器检查是否能够加载当前这个类,有就加载就结束, 使用当前的加载器;没有抛出异常,通知子加载器
进行加载
java通过 native 调用操作系统
的方法
native
底层c语言
的库本地方法栈
,调用本地方法
本地接口 JNI ( Java Native Interface )
JNI作用:拓展 Java 的使用,融合不同的编程语言为 Java 所用(最初: C、C++),它在内存区域
中专门开辟了一块标记区域
:本地方法栈(Native Method Stack),登记 native
方法,在最终执行的时候,加载本地方法库
中的方法
通过 JNI,如Java程序驱动打印机或者Java 系统管理设备
本地方法栈(Native Method Stack)
它的具体做法是:本地方法栈
中标记为native方法,在执行引擎
( Execution Engine ) 执行的时候加载本地库
(Native Libraies)
沙箱安全机制
Java 安全模型的核心就是 Java 沙箱
。沙箱是一个限制
程序运行的环境
。
沙箱机制就是将 Java 代码限定
在虚拟机 ( JVM ) 特定的运行范围中,并且严格限制
代码对本地系统资源
访问,通过这样的措施来保证对代码的有效隔离
,防止对本地系统
造成破坏。沙箱主要限制系统资源访问,系统资源
包括:CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
在 Java 中将执行程序分成:本地代码
、远程代码
。本地代码默认视为可信任
的,而远程代码则被看作是不受信
的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制。
在Java1.2版本中,改进了安全机制,增加了代码签名。不论本地代码
或是远程代码
,都会按照用户的安全策略
设定,由类加载器
加载到虚拟机中权限不同的运行空间
,来实现差异化的代码执行权限控制
。
组成沙箱的基本组件:
内存保护
。但并不是所有的类文件都会经过字节码校验,比如核心类双亲委派机制
)方法区
是被所有线程共享,所有字段
和方法字节码
,以及一些特殊方法
(如:构造函数,接口代码也在此定义)。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
静态变量(static)、常量(final)、类信息(构造方法、接口定义)、运行时的常量池
都存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区储存的是:static
、final
、Class
、常量池
每个线程都有一个程序计数器,是线程私有
的,就是一个指针,指向方法区
中的方法字节码
(用来存储指向像一条指令的地址
, 也即是将要执行的指令代码
),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计
栈(数据结构):先进后出、后进先出
队列:先进先出( FIFO : First Input First Output )
栈内存
生命周期
和线程同步
栈存储的是:8大基本类型
、对象引用
、实例方法
栈帧:局部变量表 + 操作数栈
每执行一个方法,就会产生一个栈帧。程序正在运行
的方法永远都会在栈顶
栈满了,就会报错 StackOverflowError
栈、堆、方法区的交互关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i3d1K6dq-1648561802067)(link-picture\image-20220107165620842.png)]
一个 JVM 只有一个
堆内存,堆内存的大小是可调节的
。
类加载器
读取了类文件
(.class)后,一般会把类、方法、常量、变量放到堆
中,保存所有引用类型
的真实对象
堆内存
堆内存分为三个区域:
新生区
Young:类诞生成长或死亡的地方
new
出来的养老区
old永久区
Perm(在 JDK8 以后,永久存储区改名为元空间
)
常驻内存
的,用来存放 JDK 自身携带的Class对象
、Interface接口
元数据,存储的是 Java 运行时
的一些环境或类信息注意
:
方法区
;堆
中元空间
GC 垃圾回收:主要在新生区和养老区
轻GC:轻量级垃圾回收,主要是在新生区
重GC:重量级垃圾回收,主要是在养老区
,重 GC 就说明内存都要爆了(如:OOM
(Out Of memory))
堆内存调优
Java 虚拟机默认情况下:分配的总内存是电脑内存的 1/4,而初始化的内存是电脑内存的 1/64
可以通过调整(Edit Configuration—>VM options
)这个参数控制 Java 虚拟机初始内存
和分配的总内存
的大小
# 设置虚拟机的总内存和初始占用内存为:1G,并打印日志
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
# 设置虚拟机的总内存和初始占用内存为:1G,并假如堆内存heap出现了OOM则dump出这个异常
-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryErro
当新生代、老年代、元空间内存都满了之后才会报 OOM
内存快照分析工具
内存快照分析工具有:MAT,Jprofiler
Eclipse
中IDEA
中可以使用 Jprofiles 插件,在 Settings—>Plugins 中搜索 Jprofiles,安装改插件即可使用工具作用
:
内存
文件,快速定位
内存泄露;(dump出的文件应该在src目录
下)堆中
的数据大的对象
JVM 在进行 GC 时,并不是对这三个区域统一回收。大部分时候,回收都是新生代
新生代
幸存区
(form , to):幸存0区 和 幸存1区 两者是会交替的,from和to的关系会交替变化
老年区
GC 两种类:轻 GC (普通的 GC ),重 GC (全局 GC )
GC常用算法
标记清除法
扫描对象,对对象进行标记
;清除:对没有标记
的对象进行清除
优缺点
:
浪费时间
,会产生内存碎片
标记压缩
改良
:标记清除再压缩(压缩:防止内存碎片产生,再扫描,向一端移动存活的对象)
再改进
:先标记清除几次之后,再压缩1次
复制算法
新生区主要是用复制算法,to 永远是干净的,空的
每次 GC 都会将 Eden 区
活的对象移到幸存区
中,一旦 Eden 区被 GC 后,就会是空的
当一个对象经历了15次 GC 后,都还没死,可通过 -XX:MaxTenuringThreshold=9999
这个参数设定进入老年代的时间
复制算法最佳使用场景:1. 对象存活度较低
的时候 2. 新生区
优缺点
:
没有内存碎片
浪费内存
空间(一个幸存区的空间永远是空:to)引用计数器法(不常用)
GC算法总结
年轻代:存活率低,复制算法
老年代:区域大,存活率高,标记清除
(内存碎片不是太多) + 标记压缩
混合实现
JUC(java.util.concurrent下面的类包),专门用于多线程
的开发
线程和进程
进程:是操作系统中的应用程序、是资源分配的基本单位
线程:是用来执行具体的任务和功能,是CPU调度和分派的最小单位
对于Java而言:Thread、Runable、Callable 进行开启线程的
Java是没有权限
去开启线程、操作硬件的,这是一个 native
本地方法,它底层调用的 C++ 代码
并行和并发
并发
:多线程操作同一个资源
充分利用CPU的资源
并行
: 多个人一起行走
线程池
**!Runtime.getRuntime().availableProcessors(); // 获取 cpu 的核数
线程的状态
wait/sleep 的区别
来自不同的类
wait => Object
sleep => Thread
TimeUnit.DAYS.sleep(1); //休眠1天
TimeUnit.SECONDS.sleep(1); //休眠1s
关于锁的释放
wait:会释放锁
sleep:不会释放锁
使用的范围是不同
的
wait:必须在同步代码块中
sleep:可以在任何地方
是否需要捕获
异常
wait:是不需捕获异常
sleep:必须要捕获异常
synchronized 与 Lock
传统的 synchronized
线程就是一份单独的资源类
(包含属性
、方法
),没有任何的附属操作
public class Demo {
public static void main(String[] args) {
// 并发:多线程操作同一资源,把资源放入线程
final 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();
}
}
// 线程就是一份单独的资源类,没有任何的附属操作,因此一般用实现 Runnable 的方式
class Ticket {
private int number = 30;
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName()
+ "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
}
}
Lock
ReentrantLock
:可重入锁(非公平锁)
ReentrantReadWriteLock.ReadLock
:可重入读锁
ReentrantReadWriteLock.WriteLock
:可重入写锁
公平锁: 十分公平,必须先来后到
非公平锁:十分不公平,可以插队
// 主方法与上面相同
class Ticket2 {
private int number = 30;
Lock lock = new ReentrantLock(); // 1.创建锁
public synchronized void sale() {
lock.lock(); // 2.加锁
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName()
+ "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
}finally {
lock.unlock(); // 3.解锁
}
}
}
synchronized 与 Lock 的区别
关键字
,Lock 是一个 Java 类
锁的状态
,Lock 可以判断锁的状态自动释放锁
,lock 必须要手动加锁
和手动释放锁
,如果不释放可能会死锁等待
);lock 就不一定会一直等待下去,lock 会有一个 trylock 去尝试获取锁
,不会造成长久的等待不中断的
,非公平的
(可插队);Lock 是可重入锁,可以判断锁
,可以设置公平锁和非公平锁少量代码
同步问题,Lock 适合锁大量代码
同步问题线程之间的通信问题:生产者和消费者问题(等待唤醒、通知唤醒)
线程交替执行
synchronized 实现
if 判断
会出现虚假唤醒(解决:等待应该总是放在循环
中(不能使用 if 判断))
//
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(); // 加1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement(); // 减1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
// 等待 业务 通知
class Data {
private int num = 0;
public synchronized void increment() throws InterruptedException { // +1
while (num != 0) { // 判断等待:if 判断会出现虚假唤醒
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
this.notifyAll(); // 通知其他线程 +1 执行完毕
}
public synchronized void decrement() throws InterruptedException { // -1
while (num == 0) { // 判断等待:if 判断会出现虚假唤醒
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
this.notifyAll(); // 通知其他线程 -1 执行完毕
}
}
Lock 实现
// 主方法与上面的相同
// 等待 业务 通知
class Data {
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException { // +1
lock.lock(); // 加锁
try{
while (num != 0) { // 判断等待:if 判断会出现虚假唤醒
condition.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll(); // 通知其他线程 +1 执行完毕
}finally{
lock.unlock(); //解锁
}
}
public void decrement() throws InterruptedException { // -1
lock.lock(); // 加锁
try{
while (num == 0) { // 判断等待:if 判断会出现虚假唤醒
condition.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll(); // 通知其他线程 +1 执行完毕
}finally{
lock.unlock(); //解锁
}
}
}
Condition 的优势:精准的通知
和唤醒
的线程
用 Condition 来指定通知
下一个进行顺序(按顺序执行)
public class ConditionDemo {
public static void main(String[] args) {
Data3 data = new Data();
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 Data {
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() + "==> AAAA" );
num = 2;
condition2.signal(); // 唤醒 2
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (num != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "==> BBBB" );
num = 3;
condition3.signal(); // 唤醒 3
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (num != 3) {
condition1.await(); // 唤醒 1
}
System.out.println(Thread.currentThread().getName() + "==> CCCC" );
num = 1;
condition1.signal();
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
锁是谁? 锁的是谁?(对象
、Class
)
两个同步方法
,先执行发短信还是打电话?结果:---->发短信先 (锁的是调用的对象)
public class dome01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> { phone.sendMs(); }).start();
TimeUnit.SECONDS.sleep(1); // 睡1秒
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
public synchronized void sendMs() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
普通方法
,不受锁影响
把 synchronized
的方法加上 static 变成静态方法
(锁的是Class类的模板
)
单线程
操作集合是安全的
,多线程
操作集合就是不安全的
List 不安全
ArrayList 在并发情况下是不安全的
解决方案
:
1. List<String> list = new Vector<>(); //Vector是线程安全的
2. List<String> list = Collections.synchronizedList(new ArrayList<>());
3. List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略
CopyOnWriteArrayList
核心思想是:如果有多个调用者
同时调用相同的资源
(如内存
或者是磁盘上的数据存储
),会共同获取相同的指针
指向相同的资源
,直到某个调用者试图修改
资源内容时,系统才会真正复制
一份专用副本
给该调用者
,而其他调用者仍然保持不变。这过程对其他的调用者都是透明的。此做法主要的优点是如果调用者没有修改
资源,就不会有副本
被创建,因此多个调用者只是读取操作时
可以共享同一份
资源。读的时候不需要加锁,如果读的时候有多个线程正在向 CopyOnWriteArrayList 添加数据,读还是会读到旧的数据
,因为写的时候不会锁住旧的 CopyOnWriteArrayList 。
多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题
**CopyOnWriteArrayList 与 Vector **的区别
synchronized
关键字来实现的,效率特别低下
Lock
锁,效率会更加高效
Set 不安全
普通的 Set 集合在并发情况下是不安全的
解决方案
:
使用 Collections
工具类的 synchronized 包装的 Set 类
使用 CopyOnWriteArraySet
写入时复制的 JUC 解决方案
1. Set<String> set = Collections.synchronizedSet(new HashSet<>());
2. Set<String> set = new CopyOnWriteArraySet<>();
hashSet底层
:就是一个HashMap ,所以,HashMap 基础类也存在并发修改异常
Map 不安全
new HashMap<>; 默认等价 new HashMap<>(16,0.75); // 初始化容量16,加载因子0.75
解决方案
:
使用 Collections
工具类的 synchronized 包装的 Map 类
使用 ConcurrentHashMap
的 JUC 解决方案
1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
2. Map<String, String> map = new ConcurrentHashMap<>();
Callable 与 Runnable 的区别
call()
public class CallableTest {
public static void main(String[] args)
throws ExecutionException, InterruptedException {
for (int i = 1; i < 10; i++) {
MyThread myThread = new MyThread();
// FutureTask 是 Runnable 实现类,可以接收 Callable
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
// 放入Thread中使用,结果会被缓存
new Thread(futureTask,String.valueOf(i)).start();
// get方法可能会被阻塞,如果在call方法中是一个耗时的方法,
// 所以一般情况会把这个放在最后或者使用异步通信
int a = futureTask.get();
System.out.println("返回值:" + s);
}
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("call()");
return 1024;
}
}
CountDownLatch
:减法计数器
主要方法
:
减一
操作;归零
,归零就唤醒,再继续向下运行public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6); // 总数是6
for (int i = 1; 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");
}
}
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++) {// 子线程
int finalI = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName()
+ "收集了第" + finalI + "颗龙珠");
try {
cyclicBarrier.await(); // 加法计数 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
:并发限流
作用: 多个共享资源互斥使用
, 并发限流
,控制最大的线程数
原理:
semaphore.acquire
():获得
资源,如果资源使用完
,就等待资源释放后
再进行使用!
semaphore.release
():释放
资源,会将当前的信号量释放
,然后唤醒
等待的线程!
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); // 线程数量,停车位,限流
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获得资源 阻塞式等待
System.out.println(
Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(
Thread.currentThread().getName() + "离开车位");
}catch (Exception e) {
e.printStackTrace();
}finally {
semaphore.release(); // 释放资源
}
}).start();
}
}
}
ReentrantLock
:可重入锁(默认是非公平锁)
ReentrantReadWriteLock.ReadLock
:可重入读锁
ReentrantReadWriteLock.WriteLock
:可重入写锁
如果不加锁的
情况,多线程的读写
会造成数据不可靠
的问题。
数据可靠
数据可靠
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占有
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
int num = 6;
for (int i = 1; i <= num; i++) {
final 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 MyCache2 {
private volatile Map<String, String> map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock(); // 读写锁
public void write(String key, String value) {
lock.writeLock().lock(); // 写锁
try {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入ok");
}finally {
lock.writeLock().unlock(); // 释放写锁
}
}
public void read(String key) {
lock.readLock().lock(); // 读锁
try {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程写读取ok");
}finally {
lock.readLock().unlock(); // 释放读锁
}
}
}
// 方法未加锁,导致写的时候被插队
class MyCache {
// volatile 保证共享资源的可见性
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");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0iI3Gd29-1648561802070)(link-picture\image-20220108131237100.png)]
BlockingQueue:阻塞队列
BlockingQueue
:是 Collection
的一个子类
使用阻塞队列的情况:多线程并发处理、线程池
BlockingQueue 有四组 api :
方式 | 抛出异常 | 不会抛出异常,有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(timenum.timeUnit) |
移除 | remove | poll | take | poll(timenum,timeUnit) |
判断队首元素 | element | peek | - | - |
// 抛出异常
public static void test1(){
//需要初始化队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
//如果多添加一个 抛出异常:java.lang.IllegalStateException: Queue full
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//如果多移除一个 抛出异常:java.util.NoSuchElementException
System.out.println(blockingQueue.remove());
}
// 不抛出异常,有返回值
public static void test2(){
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
//如果多添加一个 只会返回 false 不会抛出异常
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//如果多移除一个 只会返回 null 不会抛出异常
System.out.println(blockingQueue.poll());
}
// 等待 一直阻塞
public static void test3() throws InterruptedException {
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2);
//一直阻塞 不会返回
blockingQueue.put("a");
blockingQueue.put("b");
//如果多添加一个 会一直等待这个队列 什么时候有了位置再进去,程序不会停止
//blockingQueue.put("c");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//如果多移除一个 也会等待,程序会一直运行 阻塞
System.out.println(blockingQueue.take());
}
//等待 超时阻塞 也会等待队列有位置 或者有产品 但是会超时结束
public static void test4() throws InterruptedException {
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2);
blockingQueue.offer("a");
blockingQueue.offer("b");
System.out.println("开始等待");
//超时时间2s 等待如果超过2s就结束等待
blockingQueue.offer("c",2, TimeUnit.SECONDS);
System.out.println("结束等待");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println("开始等待");
//超过两秒 我们就不要等待了
blockingQueue.poll(2,TimeUnit.SECONDS);
System.out.println("结束等待");
}
SynchronousQueue:同步队列
SynchronousQueue
:是 BlockingQueue
阻塞队列的一个实现类
同步队列:没有容量,也可视为容量为1的队列
特点
:一进一出(进去一个元素,必须等待取出来后,才能再往里面放入一个元素)
put 进去一个元素,就必须从里面先 take 出来,否则不能再 put 进去值
SynchronousQueue 和 其他的 BlockingQueue 区别
SynchronousQueue
不存储元素take
是使用了 lock锁保证线程安全 的public class SynchronousQueue {
public static void main(String[] args) {
// SynchronousQueue 不能初始化队列的大小
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();
}
}
池化技术
程序的运行的本质:占用系统资源
!我们需要去优化资源
的使用 ===> 池化技术
池化技术:资源的创建、销毁十分消耗资源,可以事先准备好
一些资源,如果要用,就从这里来拿,用完之后还回来来,以此来提高效率
池的种类:线程池、JDBC的连接池、内存池、对象池等等
线程池
线程池的好处
:线程复用、可以控制最大并发数、管理线程
线程池:三大方式、七大参数、四种拒绝策略
SingleThreadExecutor
(); // 单个线程FixedThreadPoo
l(5); // 固定大小的线程池CachedThreadPool
(); // 大小可伸缩的线程池public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //固定的线程池
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
try {
for (int i = 1; i <=100 ; i++) {
// 通过线程池创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown(); // 线程池用完必须要关闭线程池
}
}
}
Executors
创建线程池的本质是:使用 ThreadPoolExecutor
来创建线程池的
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大的线程池大小
long keepAliveTime, // 超时了没有调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂 创建线程的 一般不用动
RejectedExecutionHandler handler // 拒绝策略
) {
}
一般情况我们要使用底层
的 ThreadPoolExecutor 来自定义创建线程池
public class PollDemo {
public static void main(String[] args) {
int max = Runtime.getRuntime().availableProcessors(); // 获取cpu 的核数
// 使用底层的 ThreadPoolExecutor 来自定义创建线程池
ExecutorService service =new ThreadPoolExecutor(
2, // 核心线程池大小
max, // 最大的线程池大小
3, // 超时了,没有调用就会释放
TimeUnit.SECONDS, // 超时单位
new LinkedBlockingDeque<>(3), // 阻塞队列
Executors.defaultThreadFactory(), // 线程工厂 创建线程的
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
try {
for (int i = 1; i <= 10; i++) {
// 通过线程池创建线程
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + "ok");
});
}
}catch (Exception e) {
e.printStackTrace();
}
finally {
service.shutdown(); // 线程池用完必须要关闭线程池
}
}
}
队列满了,不处理
这个,并抛出异常
哪来的,去哪里
,由 main 线程进行处理队列满了,丢掉异常
,不会抛出
异常最早的
进程竞争
,不会抛出
异常CPU密集型电脑的核数是几核就选择几,来确定线程池 maximunPoolSize
的大小
Runtime.getRuntime().availableProcessors(); // 获取cpu 的核数
I/O密集型
I/O 十分占用资源
I/O 密集型就是判断程序中十分耗 I/O 的线程数量
,大约是最大 I/O 数的一倍到两倍之间
lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口:就是只有一个方法
的接口
四大原生的
函数式接口:Consumer、Function、Predicate、Supplier
Consumer:消费型接口(只有参数,没有返回值)
// 源码
@FunctionInterface
public interface Consumer<T>{
void accept(T t);
}
// 简单应用
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<String> consumer = (str)->{ System.out.println(str);};
consumer.accept("abc");
}
}
Function:函数型接口(传入参数 T,返回参数 R)
// 源码
@FunctionInterface
public interface Function<T,R>{
R apply(T t);
}
// 简单应用
public class FunctionDemo {
public static void main(String[] args) {
/* 不使用 Lambda 表达式的写法
Function function = new Function(){
public String apply(String str){
return str;
}
}*/
// 函数式接口 可以使用 Lambda 表达式简化
Function<String, String> function = (str) -> {return str;};
System.out.println(function.apply("aaaaaaaaaa"));
}
}
Predicate:断定型接口(只能传入一个参数 T,返回一个 Boolean的值)
// 源码
@FunctionInterface
public interface Predicate<T>{
Boolean test(T t);
}
// 简单应用
public class PredicateDemo {
public static void main(String[] args) {
Predicate<String> predicate = (str) -> {return str.isEmpty();};
System.out.println(predicate.test("aaa"));// false
System.out.println(predicate.test("")); // true
}
}
Supplier:供给型接口(没有参数,只有返回值)
// 源码
@FunctionInterface
public interface Supplier<T>{
T get();
}
// 简单应用
public class SupplierDemo {
public static void main(String[] args) {
Supplier<String> supplier = ()->{return "1024";};
System.out.println(supplier.get());
}
}
大数据:储存 + 计算
集合、MySQL
本质就是储存数据的流 Stream
来计算的/**
* 题目要求: 用一行代码实现
* 1. Id 必须是偶数
* 2.年龄必须大于23
* 3. 用户名转为大写
* 4. 用户名倒序
* 5. 只能输出一个用户
**/
public class StreamDemo {
public static void main(String[] args) {
User u1 = new User(1, "a", 23);
User u2 = new User(2, "b", 23);
User u3 = new User(3, "c", 23);
User u4 = new User(6, "d", 24);
User u5 = new User(4, "e", 25);
List<User> list = Arrays.asList(u1, u2, u3, u4, u5); // 储存交给集合或MySQL
// lambda、链式编程、函数式接口、流式计算
list.stream() // 计算交给流 Stream
.filter(user -> {return user.getId()%2 == 0;}) // 断定型接口
.filter(user -> {return user.getAge() > 23;}) // 断定型接口
.map(user -> {return user.getName().toUpperCase();}) //函数型接口
.sorted((user1, user2) -> {return user2.compareTo(user1);})//函数型接口
.limit(1)
.forEach(System.out::println);
}
}
ForkJoin
在 JDK1.7,并行执行任务,提高效率(在大数据量速率会更快,在小数据量就不用了)
大数据:将大任务拆分
为小任务
ForkJoin 特点: 工作窃取(提高效率,可以调优)
实现原理
是:双端队列 先执行完毕的进程
会把没执行完的进程任务
窃取过来执行
ForkJoin 的使用
通过ForkJoinPool
来异步执行给定任务
执行
计算任务 ForkJoinPool.execute(ForkJoinTask> task)
计算任务类
要去继承 ForkJoinTask
ForkJoinTask
的子类有:
RecursiveTask
(递归任务):有返回值RecursiveAction
(递归事件):没有返回值CountedCompleter
(数字比较器)ForkJoin 的计算类:继承ForkJoinTask
import java.util.concurrent.RecursiveTask; // 递归任务 ForkJoinTask的子类
public class ForkJoinDemo extends RecursiveTask<Long> {//任务类 继承ForkJoinTask
private long star;
private long end;
private long temp = 1000000L; /** 临界值 100万 */
public ForkJoinDemo(long star, long end) {
this.star = star;
this.end = end;
}
@Override
protected Long compute() { // 计算方法
if ((end - star) < temp) { // 用一般方法来计算
Long sum = 0L;
for (Long i = star; i < end; i++) {
sum += i;
}
return sum;
}else { // 使用ForkJoin 分而治之 的方法 计算
long middle = (star + end) / 2; //计算平均值
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(star, middle);//拆分任务1
forkJoinDemo1.fork(); // 拆分任务1,把线程压入线程队列
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle, end);//拆分任务2
forkJoinDemo2.fork(); // 拆分任务2,把线程压入线程队列
long taskSum = forkJoinDemo1.join() + forkJoinDemo2.join();
return taskSum;
}
}
}
ForkJoin 的测试类
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class ForkJoinTest {
private static final long SUM = 20_0000_0000; // 20亿
public static void main(String[] args)
throws ExecutionException, InterruptedException {
test1(); // 使用普通方法 最慢
test2(); // 使用 ForkJoin 方法 还行,可以调优
test3(); // 使用 Stream 流计算 最快
}
//使用普通方法,不能调优
public static void test1() {
long star = System.currentTimeMillis(); // 开始时间
long sum = 0L;
for (long i = 1; i < SUM ; i++) { sum += i; }
long end = System.currentTimeMillis(); // 结束时间
System.out.println(sum);
System.out.println("时间:" + (end - star));
}
// 使用ForkJoin 方法计算,可以调优
public static void test2() throws ExecutionException, InterruptedException {
long star = System.currentTimeMillis(); // 开始时间
ForkJoinPool forkJoinPool = new ForkJoinPool(); //1.创建 ForkJoin池
ForkJoinTask<Long> task = new ForkJoinDemo(0L, SUM); //2.创建计算任务,并初始化
// forkJoinPool.execute(task); // 执行任务,返回void 不同
// task.fork(); //也可以使用任务 task 来执行
ForkJoinTask<Long> submit = forkJoinPool.submit(task); //3.提交任务
Long along = submit.get(); //4.得到任务结果
System.out.println(along);
long end = System.currentTimeMillis(); // 结束时间
System.out.println("时间:" + (end - star));
}
//使用 Stream 流计算,更加简洁,效率更高
public static void test3() {
long star = System.currentTimeMillis(); // 开始时间
long sum = LongStream.rangeClosed(0L, 20_0000_0000L) //计算范围,包含首尾
.parallel() // 并行计算
.reduce(0, Long::sum); // 求和
System.out.println(sum);
long end = System.currentTimeMillis(); // 结束时间
System.out.println("时间:" + (end - star));
}
}
Future 设计的初衷:对将来
的某个事件结果
进行建模
异步回调 类似于
前端发送ajax异步
请求给后端
Future 是一个接口
,平时使用他的实现类 CompletableFuture ,使用他的一些方法
来实现
**CompletableFuture 方法的应用 **
没有返回值
的 runAsync
异步回调
public static void main(String[] args)
throws ExecutionException, InterruptedException {
System.out.println(System.currentTimeMillis());
// 创建 发起一个异步请求任务
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(future.get()); //阻塞式,获取异步任务的执行结果
}
有返回值
的supplyAsync
异步回调
public static void main(String[] args)
throws ExecutionException, InterruptedException {
// 创建 发起一个异步请求任务
CompletableFuture<Integer> future=CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
int i=1/0;
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1024;
});
System.out.println(future.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()); // get 得到异常的返回值
}
JMM(Java Memory Model):抽象的概念、理论,不存在的东西,是一个概念,也是一个约定
作用:缓存一致性协议
,用于定义数据读写
的规则
JMM 定义了线程和主内存(只有一个)之间的抽象关系
线程之间的共享变量
存储在主内存
(Main Memory)中,每个线程都有一个私有的本地内存
(Local Memory),是从主内存拷贝过来的
线程中分为: 工作内存
、主内存
关于JMM的一些同步的约定:
解锁
unlock 前,必须把共享变量
立刻刷回主存
;加锁
lock 前,必须读取主存
中的最新值到工作内存
中;同一把锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ObP619rA-1648561802073)(link-picture\image-20220108172131208.png)]
JMM 的八种指令
:read 和 load、store 和 write、lock 和 unlock、use 和 assign
主内存
变量,把一个变量的值从主内存
传输到线程的工作内存
中,以便随后的 load 动作使用;工作内存
变量,把 read 操作从主内存
变量放入工作内存
中;工作内存
变量,把工作内存
变量传输给执行引擎
,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;工作内存
变量,把一个从执行引擎
中接受到的值放入工作内存
的变量副本中;主内存
变量,把一个从工作内存
中一个变量的值传送到主内存
中,以便后续的 write 使用;主内存
变量,把 store 操作从工作内存
中得到的变量的值放入主内存
的变量中;主内存
的变量,把一个变量标识
为线程独占状态;主内存
的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定JMM 对八种指令的使用,制定了如下规则:
不允许 read 和load
、store和write
操作单独出现。即使用了 read 必须 load,使用了 store 必须 write
不允许线程丢弃他最近的 assign
操作,即:工作变量
的数据改变
了之后,必须告知主内存
不允许一个线程将没有 assign
的数据从工作内存同步回主内存
一个新变量必须在主内存中
诞生,不允许工作内存直接使用一个未被初始化
的变量。就是对变量 use 、store 操作之前,必须经过assign 和 load
操作
一个变量
同一时间只有一个线程
能对其进行 lock
。多次lock后,必须执行相同次数
的 unlock 才能解锁
如果对一个变量进行 lock 操作
,会清空所有工作内存
中此变量的值,在执行引擎使用这个变量前,必须重新 load 或 assign
操作初始化
变量的值
如果一个变量没有被 lock
,就不能对其进行 unlock
操作。也不能 unlock 一个被其他线程锁住
的变量
一个变量进行 unlock
操作之前,必须把此变量同步回主内存
JMM 对这八种操作规则
和 volatile
的一些特殊规则就能确定哪些操作是线程安全
,哪些操作是线程不安全
的。但是这些规则实在复杂,很难在实践中直接分析。所以一般也不会通过上述规则进行分析。更多的时候,使用 java 的 happen-before 规则来进行分析
volilate:解决共享对象
可见性,一旦刷新了就会很快的同步
到主内存
中
Volatile 是 Java 虚拟机
提供 轻量级的同步机制
volatile
修饰的共享变量
,在进行写操作的时候会多出一行汇编
Lock 前缀的指令在多核CPU
下会引发两件事情:
当前 CPU 缓存行
的数据
写回到系统内存
。其他 CPU 里缓存
了该内存地址的数据无效
多处理器总线嗅探
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作不知道何时会写到内存。
如果对声明了 volatile 的变量进行写操作,JVM就会向处理器发送一条 lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
但是在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据库读到处理器缓存中
public class JMMDemo {
private static Integer number = 0;
public static void main(String[] args) { //main线程
new Thread(()->{ // 子线程
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
number=1;
System.out.println(number);
}
}
遇到问题:子线程不知道主存中的值已经被修改过了,而导致死循环,这是因为子线程
的 number 对主内存
的 number 不可见
导致的,加上 volatile (可以保证子线程
对主内存
的可见性
)就可以了。
volatile 的特性
volatile 保证可见性
private volatile static Integer number = 0; // volatile的可见性可解决上面死循环问题
不保证原子性
原子性:不可分割,要么同时成功,要么同时失败
// 不保证原子性
public class VolatileDemo {
// private static synchronized int number = 0; // synchronized 保证原子性
private static volatile int number = 0; // volatile 保证可见性
public static void add(){
number++; //++ 不是一个原子性操作,是2个~3个操作
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) { //理论上number最后 == 20000
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ // 判断线程存活的个数 main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
如何来保证原子性?
synchronized 锁
或 Lock 锁
来保证原子性(synchronized
可以解决,但较耗资源 )原子类
来保证原子性原子类 Automic 是 java.util.concurrent 包下的类
原子类的底层
都直接和操作系统挂钩!是在内存中
修改值
原子类底层使用了一个 Unsafe 类,Unsafe类是一个很特殊的存在,可以直接和内存
进行交互
// 使用原子类保证原子性
public class VolatileDemo {
// volatile 保证可见性
private static volatile AtomicInteger number = new AtomicInteger();
public static void add(){
number.incrementAndGet(); // 底层是通过 CAS 保证的原子性
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) { //理论上number最后 == 20000
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ // 判断线程存活的个数 main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
禁止指令重排
指令重排:我们写的程序,计算机并不是按照我们自己写的那样去执行的
程序执行过程:源代码–>编译器
优化重排–>指令并行
也可能会重排
–>内存系统
也会重排–>执行
系统重排的前提:处理器在进行指令重排的时候,会考虑数据之间的依赖性
volatile 可以避免指令重排,在运行的程序上会加一道内存屏障,这个内存屏障可以保证在这个屏障中
的指令顺序
内存屏障:就是一些 CPU 指令,volatile 就是使用内存屏障
来保证可见性的
内存屏障的作用:
特定操作
的执行顺序
某些变量
的内存可见性
(利用这些特性,就可以保证 volatile 实现的可见性)内存屏障使用最多的地方
:单例模式(饿汉式
,DCL懒汉式
(就使用了内存屏障))
volatile 总结
可见性
,不能
保证原子性
内存屏障
,可以保证避免指令重排
单例模式:饿汉式、DCL懒汉式
单例模式的特点
private
)public static
)private final static
)注意:单例不安全, 因为反射
可能会浪费空间
public class Hungry {
// 可能会浪费空间
private byte[] data1=new byte[1024*1024]; // 1M
private byte[] data2=new byte[1024*1024];
private byte[] data3=new byte[1024*1024];
private byte[] data4=new byte[1024*1024];
private Hungry(){} // 私有化构造器
private final static Hungry hungry = new Hungry(); // 创建唯一的对象
public static Hungry getInstance(){ // 提供公共的获得方法
return hungry;
}
}
简单的懒汉式
这样的懒汉式单例模式,在单线程是没问题
的,在多线程下是有问题的
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){ } // 私有化构造器
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
完善后的懒汉式(适合多线程)
class LazyMan {
private static boolean key = false; // 红绿灯
private volatile static LazyMan lazyMan; // volatile 保证可见性和避免指令重排
private LazyMan(){ // 私有化构造器
synchronized (LazyMan.class){ // 保证安全性,避免反射破坏
if (key==false){
key=true;
}else{
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName()+" ok");
}
public static LazyMan getInstance(){ //双重检测锁模式 简称 DCL 懒汉式
if(lazyMan==null){ //需要加锁
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
}
public class LazyManDemo{
public static void main(String[] args) throws Exception {
// LazyMan instance = LazyMan.getInstance(); // 正常的获得单例对象
//Java中有反射,利用反射破坏单例模式
Field key = LazyMan.class.getDeclaredField("key"); // 获得 key 这个属性
key.setAccessible(true); //无视了私有性
Constructor<LazyMan> declaredConstructor =
LazyMan.class.getDeclaredConstructor(null); //获得空参构造器
declaredConstructor.setAccessible(true); //无视构造器的私有性
LazyMan lazyMan1 = declaredConstructor.newInstance(); //创建实例
key.set(lazyMan1,false);
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(lazyMan1);
System.out.println(instance == lazyMan1);
}
}
public class Holder {
private Holder(){} // 私有化构造器
public static Holder getInstance(){ // 提供公共的获得方法
return InnerClass.holder;
}
public static class InnerClass{ // 静态内部类
private static final Holder holder = new Holder(); // // 创建唯一的对象
}
}
Enum 本身就是一个 Class 类
枚举 Enum 可解决单例模式的不安全问题
反射不能破坏枚举
,使用枚举,我们就可以防止反射破坏了
反编译方法:javap -p .class文件
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(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance(); //反射得到 会报错
//java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.()
System.out.println(instance1);
System.out.println(instance2);
}
}
CAS(compareAndSet): 比较并交换
(如果期望的值达到了,就更新,否则就不更新)
CAS 是 CPU 的并发原语
Java 层面的 CAS
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020); // 默认值是 2020
//boolean compareAndSet(int expect, int update) // 期望值、更新值
//如果实际值 和 我的期望值相同,那么就更新;不同,那么就不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));//达到期望值
System.out.println(atomicInteger.get()); // 得到更新后的值 2021
//因为期望值是2020 实际值却变成了2021 所以会修改失败
atomicInteger.getAndIncrement(); //++操作 底层通过 Unsafe 类来实现++操作的
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
CAS 的原理
CAS:比较当前工作内存
中的值 和 主内存
中的值,如果这个值是期望的(即是相等的)
,那么则执行更新交换操作,如果不是就一直循环
,使用的是自旋锁
//atomicInteger.getAndIncrement() 的底层就是使用的 while 自旋锁
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//while 自旋锁
return var5;
}
CAS缺点:
循环会耗时
;
一次性只能保证一个
共享变量的原子性
;
它会存在ABA问题(狸猫换太子)(使用 乐观锁 解决 ABA 问题
)
原子类 Automic 的底层是通过 CAS
保证的原子性
原子类底层使用了一个 Unsafe 类和内存
进行交互的,所以效率很高
Unsafe 类
java 无法操作内存,但可以通过 native 方法
调用 C++ ,C++可以调用内存
Unsafe 类相当于 java 的后门,可以通过 Unsafe
类来操作内存
CAS 的 ABA问题
public class ABADemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020); //比较值2020
// 线程1 拿到值2020并修改值,后又将值改为2020
System.out.println(atomicInteger.compareAndSet(2020, 2021));//true
System.out.println(atomicInteger.get());// 2021
System.out.println(atomicInteger.compareAndSet(2021, 2020));//true
System.out.println(atomicInteger.get());//2020
//线程2 还是拿到2020,以为没有被修改,其实是已修改过后的值 这就是ABA狸猫换太子的问题
System.out.println(atomicInteger.compareAndSet(2020, 6666));//true
System.out.println(atomicInteger.get());//6666
}
}
平时写的 SQL:使用 乐观锁 来解决 ABA
问题
使用原子引用类(AtomicReference
类)来解决 ABA
问题
原子引用:带版本号
的 原子操作
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo {
// AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
// 正常在业务操作,这里面比较的都是一个个对象
static AtomicStampedReference<Integer> atomicStampedReference =
new AtomicStampedReference<>(1, 1); //参数为:初始值(比较值),初始戳(版本号)
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改操作时,版本号更新 + 1
atomicStampedReference.compareAndSet(1, 2, // 期望值,更新值
atomicStampedReference.getStamp(), // 获得版本号
atomicStampedReference.getStamp() + 1); // 版本号更新 + 1
System.out.println("a2=>" + atomicStampedReference.getStamp());
// 重新把值改回去, 版本号更新 + 1
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 乐观锁的原理相同!
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, 3,
stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
Integer 使用了对象缓存机制
,默认范围是-128~127,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new
,因为 valueOf
使用缓存,而 new 一定会创建新的对象
分配新的内存空间
。
公平锁:非常公平,不能插队
,必须先来后到
非公平锁:非常不公平,允许插队
,可以改变顺序
Synchonized 锁:默认是非公平锁
ReentrantLock
:默认就是非公平锁,可以使用他的重载构造方法
修改为公平锁
// ReentrantLock 的默认构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
// 使用他的重载构造方法更改锁的公平性
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
拿到外面的锁之后,就可以拿到里面的锁,自动获得
Synchonized 锁(默认是非公平锁)
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{ phone.sms(); },"A").start();
new Thread(()->{ phone.sms(); },"B").start();
}
}
class Phone{
public synchronized void sms(){ // synchronized 就一把锁
System.out.println(Thread.currentThread().getName()+"=> sms");
call();//这里也有一把锁
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=> call");
}
}
Lock 锁
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{ phone.sms(); },"A").start();
new Thread(()->{ phone.sms(); },"B").start();
}
}
class Phone{
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(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "=> call");
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
自旋锁(SpinLock
)
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
自定义自旋锁( CAS 原理
,使用原子类
的原子引用
)
自旋锁
public class SpinlockDemo {
AtomicReference<Thread> atomicReference=new AtomicReference<>(); //原子引用
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"===> mylock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){ // CAS比较
System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
}
}
//解锁
public void myUnlock(){
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"===> myUnlock");
atomicReference.compareAndSet(thread,null); // CAS比较
}
}
测试自旋锁
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
SpinlockDemo spinlockDemo=new SpinlockDemo(); //使用CAS实现自旋锁
new Thread(()->{
spinlockDemo.myLock(); // 加锁
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock(); // 3秒后 解锁
}
},"t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
spinlockDemo.myLock(); // 加锁
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock(); // 3秒后 解锁
}
},"t2").start();
}
}
运行结果:t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待
死锁:各自拥有对方的锁,相互抢夺对方的锁
import java.util.concurrent.TimeUnit;
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){ // lockA 锁
System.out.println(
Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){ // lockB 锁
System.out.println(
Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
}
}
}
}
如何解开死锁
使用 jps 定位进程号,jdk的bin目录下: 有一个 jps
命令:jps -l
:定位进程号
使用 jstack 进程进程号 找到死锁信息
命令:jstack 进程进程号
:找到死锁信息
排查问题的方法
日志信息
堆栈信息
网络编程时常用类和方法
InetAddress // 获取地址
InetAddress.getCanonicalHostName // 规范的名字
InetAddress.getHostAddress // IP
InetAddress.getHostName // 域名或自己电脑的名字
InetSocketAddress // 实现 IP 地址及端口
InetAddress // 实现 IP 地址
ServerSocket // 建立服务的端口
.accept // 阻塞监听等待连接
Socket // 创建连接
.getInputStream // 获取IO输入流
.getOutputStream // 获取IO输出流
ByteArrayOutputStream // byte类型数组管道输出流
FileOutputStream // 文件字符输出流
FileInputStream // 文件字符输入流
shutdownOutput // 停止输出
DatagramSocket // 数据包端口
DatagramPacket // 数据包
.send // 发送
.receive // 阻塞接收
BufferedReader // 缓存区读取
InputStreamReader // 输入流读取
.readLine // 读取的一行内容
URL // 统一资源定位符
.openConnection // 打开连接
HttpURLConnection // 指定协议HTTP
计算机网络:是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统
网络编程的目的:数据交换,通信
网络通信要思考的问题:
ping url
网络通信的组成要素:IP
、端口号
、通信协议
TCP/IP参考模型
OSI七层网格模型 | TCP/IP四层概念模型 | 对应网络协议 |
---|---|---|
应用层(Application) | 应用层 | HTTP , TFTP, FTP , NFS, WAIS, SMTP |
表示层(Presentation) | … | Telnet, Rlogin, Snmp, Gopher |
会话层(Session) | … | SMTP, DNS |
传输层(Transport) | 传输层 | TCP , UDP |
网络层(Network) | 网络层 | IP , ICMP, ARP, RARP, AKP, UUCP |
数据链路层(Data link) | 数据链路层 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP |
物理层(Physical) | … | IEEE 802.1A , IEEE 802.2 到 IEEE 802.11 |
java.net 包下的类
InetAddress:IP 地址类,操作 IP 的类
IP 地址分类
IPV4
:127.0.0.1 ,4个字节组成,0~255,总共42亿;30亿都在北美,亚洲4亿,2011年就已经用尽IPV6
:128位。8个无符号整数。2001:acca:0ac1:0002:0ab7:1153:2210:ccc1
域名:来标识 IP,方便 IP 的记忆,如:www.baidu.com
IP 常用操作:
ping url
:连接网络
ipconfig
:查看 ip
IP 常用类和方法:
InetAddress // 获取地址 由于没有构造器,不能new出来。只能利用静态方法
// 查看本机地址
InetAddress.getLocalHost() // 本机地址
InetAddress.getByName("localhost") // 本机地址
InetAddress.getByName("127.0.0.1") // 本机地址
InetAddress.getByName("www.baidu.com") // 查看网站ip地址 相当于 ping www.baidu.com
// 常用方法
InetAddress.getAddress() // 返回一个 IP 数组
InetAddress.getCanonicalHostName // 规范的名字
InetAddress.getHostAddress // IP
InetAddress.getHostName // 域名或自己电脑的名字
public class InetAddressDemo {
public static void main(String[] args) throws UnknownHostException {
//查询本机地址
InetAddress inetAddress1 = InetAddress.getByName("127.0.0.1");
InetAddress inetAddress3 = InetAddress.getByName("localhost");
InetAddress inetAddress4 = InetAddress.getLocalHost();
System.out.println(inetAddress1+":"+inetAddress3+":"+inetAddress4);
//查询网站ip地址
InetAddress inetAddress2 = InetAddress.getByName("www.baidu.com");
System.out.println(inetAddress2);
//常用方法
System.out.println(Arrays.toString(inetAddress2.getAddress()));//返回数组
System.out.println(inetAddress2.getCanonicalHostName()); //规范的名字
System.out.println(inetAddress2.getHostAddress()); //ip
System.out.println(inetAddress2.getHostName()); //域名,或者自己电脑的名字
}
}
InetSocketAddress: IP 地址及端口类
端口:表示计算机上的一个程序的进程
,用来区分软件 IP 用来定位计算机
不同的进程
有不同的端口
一个协议下被规定 0~65535 个端口,不能使用相同的端口
单个协议下,端口号不能冲突;协议不同,使用相同的端口号不冲突:tcp : 80, udp : 80 这样不影响
TCP:065535,UDP:065535 所以端口号总共可有:65535*2 个
端口分类
公有端口 0~1023 (尽量不用
)
程序注册端口:1024~49151,分配给用户或者程序
动态、私有:49152~65535(尽量不用
)
常用 dos 命令
netstat -ano #查看所有的端口
netstat -ano|findstr "5900" #查看指定的端口
tasklist|findstr "8696" #查看指定端口的进程
InetSocketAddress 类的简单应用
//端口
public class InetSocketAddressDemo {
public static void main(String[] args) {
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1",8080);
InetSocketAddress socketAddress2 = new InetSocketAddress("localhost",8080);
System.out.println(socketAddress);
System.out.println(socketAddress2);
System.out.println(socketAddress.getAddress()); // IP地址 /127.0.0.1
System.out.println(socketAddress.getHostName()); // 主机名 127.0.0.1
System.out.println(socketAddress.getPort()); // 端口 8080
}
}
通信协议(或约定):双方使用相同可识别的语言
网络通信协议包含的内容:速率、传输码率、代码结构、传输控制… …
主要通信协议:
传输
协议 { 类似于打电话,需要两边进行连接 }数据报
协议 { 类似于发短信,不需要两边连接也可以发出,但不一定能送到 }TCP 特点
三次握手、四次挥手
TCP 应用
TCP 简单应用
服务端
public class TcpServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);//1.建立一个地址
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null; // 字节数组管道流 避免中文乱码
int i = 0;
while (true){
//2.等待客户端连接过来
System.out.println("等待客户端连接");
socket = serverSocket.accept();
//3.读取客户端的信息
is =socket.getInputStream();
System.out.println("读取信息成功"+i);
baos = new ByteArrayOutputStream();
//创建一个接收数据的byte[]数组,及数组的有效长度len
byte[] buffer = new byte[1024];
int len;
while ((len=is.read(buffer))!=-1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
/*//一种方法 会出现中文乱码的情况
//创建一个接收数据的byte[]数组,及数组的有效长度len
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer))!=-1){
String msg = new String(buffer,0,len);
System.out.println(msg);
}*/
}
//关闭资源,先开后关,后开先关。
baos.close();
is.close();
socket.close();
serverSocket.close();
}
}
客户端
public class TcpClientDemo {
public static void main(String[] args) throws IOException {
//1.要知道服务器的地址,和端口号
InetAddress serverIP = InetAddress.getByName("127.0.0.1");
int port = 9999;
System.out.println("客户端连接成功");
Socket socket = new Socket(serverIP,port); //2.创建一个 socket 连接
OutputStream os = socket.getOutputStream(); //3.发送消息 IO 流
os.write("你好,欢迎学习狂神说Java".getBytes());
System.out.println("已发送");
socket.close();//4.关流
os.close();
}
}
TCP 文件上传
服务器端
public class TcpServiceDemo {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);//1.创建服务
System.out.println("等待连接");
Socket socket = serverSocket.accept(); //2.阻塞式监听,会一直等待客户端连接
InputStream is = socket.getInputStream();//3.获取输入流
//文件输出
FileOutputStream fos = new FileOutputStream(new File("666.jpg"));
byte[] buffer = new byte[1024];
int len;
while ((len=is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
//客户端传输完了,服务器接收完,通知客户端我接收完毕了
//服务器端接收完毕,并回复信息
OutputStream os = socket.getOutputStream();
os.write("我接收完毕,你可以断开".getBytes());
//关闭资源
os.close();
fos.close();
is.close();
socket.close();
serverSocket.close();
}
}
客户端
public class TcpClientDemo {
public static void main(String[] args) throws Exception {
//1.创建一个Socket连接
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9000);
OutputStream os = socket.getOutputStream();//2.创建一个输出流
//3.读取文件
FileInputStream fis = new FileInputStream(new File("321.jpg"));
//4.写出文件
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer))!=-1){
os.write(buffer,0,len);
}
//传输完后,通知服务器,确定服务器接收完毕,才能断开连接
socket.shutdownOutput(); //客户端已经传输完毕,关闭输出流,停止输出
InputStream inputStream = socket.getInputStream(); //接收服务端完毕信息
//由于收到的是 String byte[]数组,使用byte输出管道流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while((len2 = inputStream.read(buffer))!=-1){
baos.write(buffer,0,len2);
}
System.out.println(baos.toString());
//关闭资源
baos.close();
inputStream.close();
fis.close();
os.close();
socket.close();
}
}
UDP 需要使用的类:DatagramSocket 、DatagramPacket
UDP 特点
不连接( 只需知道对方的地址 )、不稳定
客户端、服务端:没有明确的界限
不管有没有准备好,都可以发送
UDP 简单应用
发生消息
发送端
public class UdpClientDemo {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(8080); // 1.建立一个socket
String msg = "你好啊,服务器!"; //发送的数据
InetAddress localhost = InetAddress.getByName("localhost"); //
int port = 9090;
// 2.建立一个能发送的包,包的内容:数据,数据长度,要发送给谁
DatagramPacket packet = new DatagramPacket(msg.getBytes(), // 数据
0,
msg.getBytes().length, // 数据长度
localhost,port); // 要发送给谁
socket.send(packet); // 3.发送包
socket.close(); // 4.关闭流
}
}
接收端
public class UdpServerDemo {
public static void main(String[] args) throws Exception {
// 1.开放端口,等待客户端的连接
DatagramSocket socket = new DatagramSocket(9090);
byte[] buffer = new byte[1024]; //接收临时区域
// 2.接收数据包
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
socket.receive(packet); // 3.阻塞接收
System.out.println(packet.getAddress().getHostAddress());
System.out.println(
new String(packet.getData(),0,packet.getLength()));
socket.close(); // 4.关闭流
}
}
持续发送
发送端
public class UdpSenderDemo01 {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(8888);
while(true){
//准备数据:控制台读取 System.in
BufferedReader reader =
new BufferedReader(new InputStreamReader(System.in));
String data = reader.readLine(); //包内数据
byte[] datas = data.getBytes();
DatagramPacket packet = new DatagramPacket(datas,0,
datas.length, new InetSocketAddress("localhost",6666));
socket.send(packet); //发送包
if (data.equals("bye")){
break;
}
}
socket.close(); //关闭流
}
}
接收端
public class UdpReceiveDemo01 {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(6666);
while(true){
byte[] container = new byte[1024]; //准备接收包裹
DatagramPacket packet =
new DatagramPacket(container,0,container.length);
socket.receive(packet); //阻塞式接收
byte[] data = packet.getData(); //断开连接,将接收包转换为 String 格式
String receiveData = new String(data,0,data.length);
System.out.println(receiveData);
if (receiveData.equals("bye")){
break;
}
}
socket.close(); //关闭流
}
}
多线程发送
发送线程
public class TalkSend implements Runnable{
DatagramSocket socket = null;
BufferedReader reader = null;
private int fromPort;
private String toIP;
private int toPort;
public TalkSend(int fromPort, String toIP, int toPort) {
this.fromPort = fromPort;
this.toIP = toIP;
this.toPort = toPort;
try {
socket = new DatagramSocket(fromPort);
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while(true){
//准备数据:控制台读取 System.in
BufferedReader reader =
new BufferedReader(new InputStreamReader(System.in));
String data = reader.readLine(); //发包内数据
byte[] datas = data.getBytes();
DatagramPacket packet = new DatagramPacket(datas,0,
datas.length,new InetSocketAddress(this.toIP,this.toPort));
socket.send(packet); //发送包
if (data.equals("bye")){
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
socket.close(); //关闭流
}
}
接收线程
public class TalkReceive implements Runnable{
DatagramSocket socket =null;
private int port;
private String msgfrom;
public TalkReceive(int port, String msgfrom) {
this.port = port;
this.msgfrom = msgfrom;
try {
socket = new DatagramSocket(port);
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while(true){
//准备接收包裹
byte[] container = new byte[1024];
DatagramPacket packet =
new DatagramPacket(container,0,container.length);
socket.receive(packet); //阻塞式接收
byte[] data = packet.getData();//断开连接,将接收包转换为 String
String receiveData = new String(data,0,data.length);
System.out.println(msgfrom+":"+receiveData);
if (receiveData.equals("bye")){
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
socket.close();
}
}
学生端
public class StudentTalk {
public static void main(String[] args) { // 开启两个线程
//学生自己的发送端口、接收IP、接收端口
new Thread(new TalkSend(6666,"localhost",9999)).start();
//学生自己接收端口、发送过来者
new Thread(new TalkReceive(8888,"老师")).start();
}
}
老师端
public class TeacherTalk {
public static void main(String[] args) {
//老师自己的发送端口、接收IP、接收端口
new Thread(new TalkSend(7777,"localhost",8888)).start();
//老师自己接收端口、发送过来者
new Thread(new TalkReceive(9999,"学生")).start();
}
}
URL:是统一资源定位符
的简称,用来定位资源
的,定位互联网上的某一个资源
https://www.baidu.com/
协议(https)://ip地址:端口/项目名/资源
DNS:表示域名解析
的过程就叫 DNS(将 www.baidu.com
的域名解析为:xxx.x…x…x 的 IP 号)
URL常用方法
url.getProtocol() // 协议 http
url.getHost() // 主机ip localhost
url.getPort() // 端口 8080
url.getPath() // 文件的地址
url.getFile() // 文件全路径
url.getQuery() // 参数
url.openConnection // 打开连接,连接到这个资源
HttpURLConnection
public class DRLDemo {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("http://localhost:8080/helloworld/index.jsp?username=kuangshen&password==123");
System.out.println(url.getProtocol()); // 协议http
System.out.println(url.getHost()); // 主机ip,localhost
System.out.println(url.getPort()); // 端口,8080
System.out.println(url.getPath()); //文件,/helloworld/index.jsp
//全路径,/helloworld/index.jsp?username=kuangshen&password==123
System.out.println(url.getFile());
System.out.println(url.getQuery()); //参数,username=kuangshen&password==123
}
}
简单应用:URL网络下载
public class UrlDown {
public static void main(String[] args) throws Exception {
URL url = new URL("//www.baidu.com/img/PCtm_d.png"); // 1.下载地址
// 2.连接到这个资源 HTTP
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream inputStream = urlConnection.getInputStream(); // 3.输入流
FileOutputStream fos = new FileOutputStream("123.jpg"); // 4.下载到存放地址
byte[] buffer = new byte[1024]; // 5.写出数据
int len ;
while((len = inputStream.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close(); // 6.断开连接
inputStream.close();
urlConnection.disconnect();
}
}
多任务:同时做多件事(单 CPU
实际上是分时进行的,分时处理,时间交替很快)
多线程:多条线路同时执行任务
进程:在操作系统中运行的程序(qq,播放器,游戏,IDE)
进程:是系统分配资源的单位(程序跑起来才叫一个进程)
线程:是CPU调度和执行的单位
进程和线程的关系:一个进程可以包含若干个线程,一个进程至少包含一个线程
真正的多线程是指有多个 CPU
(即多核)
单 CPU
的多线程是模拟出来的,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换得很快,所以就有同时执行的错局
总结
线程就是独立的执行路径
在程序运行时,即使没有自己创建线程,后台也会有多个线程【如:main,GC】
在一个进程中,如果开辟了多个线程,线程的运行由调度器调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
对同一资源
操作时,会存在资源抢夺的问题,需要加入并发控制
线程会带来额外的开销【如:CPU调度时间、并发控制开销(让线程排队执行)】
每个线程在自己的工作内存,内存控制不当会造成数据不一致
注意
:
线程创建方式
继承 Thread
类(不建议使用:为了避免 OOP 单继承局限性
)
Thread 类本身实现了 Runnable 接口 (使用了静态代理模式)
实现 Runnable
接口(推荐使用:方便同一资源
被多个线程使用)
实现 Callable
接口(有返回值
)
Executors
.newFixedThreadPool(1); 线程池Future
result1 = ser.submit
(t1);get
()shutdownNow
();简单应用
买票问题(实现 Runnable 接口)
public class ThereadDemo implements Runnable{
private int ticketNums=100;//票数
public static void main(String[] args) {
ThereadDemo test = new ThereadDemo();
new Thread(test,"1").start();
new Thread(test,"2").start();
new Thread(test,"3").start();
}
@Override
public void run() {
while (true){
if(ticketNums<=0){
break;
}
try {
Thread.sleep(200);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName()+"-->拿到了第"+ticketNums--+"张票");
}
}
}
结果:出现数据紊乱,线程不安全
龟兔赛跑(实现 Runnable 接口)
public class Race implements Runnable{
private static String winner; //用static,保证只有一个胜利者
public static void main(String[] args) {
new Thread(new Race();,"兔子").start();
new Thread(new Race();,"乌龟").start();
}
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
String threadName = Thread.currentThread().getName();
//模拟兔子休息,每10步休息一下
if(threadName.equals("兔子")&&i%10==0){ //注意不要用==
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(gameOver(i)){ //判断比赛是否结束,如果比赛结束,停止程序
break;
}
System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
}
}
private boolean gameOver(int steps){ //判断是否完成比赛
if(winner!=null){
return true;
}
if(steps>=1000) {
winner= Thread.currentThread().getName();
System.out.println("winneer is"+winner);
return true;
}
return false;
}
}
Callable 的简单应用
public class CallableDemo implements Callable<Boolean> {
private String url; // 网络图片地址
private String name;// 保存的文件名
public CallableDemo(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call(){
WebDownLoader webDownLoader=new WebDownLoader();
webDownLoader.downloader(url,name);
System.out.println("下载了文件名为"+name);
return true;
}
public static void main(String[] args) throws Exception {
CallableDemo t1=new CallableDemo("https://image.baidu.com/dog.jpeg","dog1");
CallableDemo t2=new CallableDemo("https://image.baidu.com/dog2.jpeg","dog2");
CallableDemo t3=new CallableDemo("https://image.baidu.com/dog3.jpeg","dog3");
// 创建执行服务
ExecutorService ser= Executors.newFixedThreadPool(3);//线程池,放3个线程
Future<Boolean> r1=ser.submit(t1); // 提交执行
Future<Boolean> r2=ser.submit(t2);
Future<Boolean> r3=ser.submit(t3);
boolean rs1=r1.get(); // 获取结果
boolean rs2=r2.get();
boolean rs3=r3.get();
ser.shutdownNow(); //关闭服务
}
}
class WebDownLoader{
public void downloader(String url,String name) {
try { // 使用 FileUtils 类需要导入 commons-io 的jar 包
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
函数式接口:只有一个抽象方法的接口
对于函数式接口,可通过 Lamda 表达式来创建该接口的对象,Lamda 可简化程序
简化过程:函数式接口(前提
) --> 实现类 --> 静态内部类 --> 局部内部类 --> 匿名内部类 --> Lamda 表达式
注意
:
代码块
包裹去掉参数类型
,要去掉就都去掉,必须加上括号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ysPxnyic-1648561802075)(link-picture\20210123215142964.png)]
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
join() | 等待该线程终止 |
yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
interrupt() | 中断线程,别用这个方式 |
isAlive() | 测试线程是否处于活动状态 |
stop()/destory()—过时,不使用 | 停止线程(不推荐使用 JDK 提供的这两个方法) |
setDaemon(boolean flag) | 设置是否为守护线程 |
停止线程(使用一个标志位
进行终止线程)
不推荐使用 JDK 提供的 stop(),destory()
方法,建议使用一个标志位进行终止线程,当 flag=false ,则线程终止运行
public class Teststop implements Runnable {
private boolean flag = true; //1.定义一个标识位
@Override
public void run (){
while (flag) { //2.判断标识位
systepaoit.println ( "run. . . Thread" );
}
}
public void stop(){ //3.对外提供方法改变标识
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i <1000 ; i++) {
System.out.println("main"+i);
if (i==900){
testStop.stop(); // 停止线程
System.out.println("run线程停止了!");
}
}
}
}
线程休眠(sleep)
注意
:
就绪状态
不会释放锁
sleep 可以模拟网络延时
(放大问题的发生性,如多线程卖票,一票多卖),倒计时
等
public class TestSleep implements Runnable {
public static void main(String[] args) throws InterruptedException {
tenDown();
Date startTime=new Date(System.currentTimeMillis()); //打印当前系统时间
while(true){
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime=new Date(System.currentTimeMillis());
}
}
public static void tenDown() throws InterruptedException {//模拟 10s 倒计时
int num=10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if(num<=0) {
break;
}
}
}
@Override
public void run() {}
}
线程礼让(yield)-- 礼让不一定成功
礼让线程:让当前正在执行的线程暂停
,但不阻塞
,将线程从运行状态转为就绪状态
public class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束执行");
}
public static void main(String[] args) {
MyYield myYield=new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
线程强制执行(join)
join:加入线程,待此线程执行完毕之后,在执行其他线程,其他线程阻塞,可以想象成插队
public class TestJoin implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread= new Thread(new TestJoin());
thread.start();
for (int i = 0; i < 50; i++) {
if(i==25){
thread.join();
}
System.out.println("主线程"+i);
}
}
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("join线程"+i);
}
}
}
线程的优先级
线程优先级高
不一定优先执行,但是优先执行的权重就大
了
先设置
优先级,再start
线程
thread.getPriority() // 得到优先级
thread.setPriority(int xxx) // 设置优先级
守护线程(daemon)
线程分为:用户线程
和 守护线程
( 后台记录操作日志,监控内存,垃圾回收等 )
守护线程在用户线程执行完毕之后
也会执行完毕
,不过 JVM 需要一点时间
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
线程状态 | 说明 |
---|---|
NEW | 尚未启动的线程处于此状态 |
RUNNABLE | 在Java虚拟机中正在执行的线程处于此状态 |
BLOCKED | 被阻塞等待监视器锁定的线程处于此状态 |
WA工TING | 正在等待另一个线程执行特定动作的线程处于此状态 |
TIMED_ WA工TING | 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态 |
TERMINATED | 已退出的线程处于此状态 |
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
for (int i = 0; i <5 ; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread.State state = thread.getState();
System.out.println(state); //new
thread.start(); //启动线程
state=thread.getState(); //runnable
System.out.println(state);
while (state!= Thread.State.TERMINATED){ //只要线程不终止就输出线程状态
Thread.sleep(100);
state=thread.getState(); //更新线程状态
System.out.println(state); //TIME_WAITING
}
}
}
线程并发:多个线程
同时
操作同一资源
的情况下,出现不同的线程抢夺
同一资源,线程不安全,数据紊乱
线程同步
处理多线程
问题时,多个线程访问同一个对象
,并且某些线程还想修改这个对象
,这时就需要线程同步。线程同步其实就是一种等待机制。多个需要同时访问此对象的线程进入这个对象的等待池形成**队列
**,等待前面线程使用完毕,下一个线程再使用
由于同一进程的多个线程
共享同一块存储空间
,就带来了访问冲突
问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized
,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,但也存在以下问题:
持有锁
会导致其它所有需要此锁的线程挂起
;加锁,释放锁
会导致比较多的上下文切换
和调度延时
,引起性能问题;优先级高
的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题线程同步形成条件:队列 + 锁(synchronized
)
买票
线程不安全
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"1").start();
new Thread(buyTicket,"2").start();
new Thread(buyTicket,"3").start();
}
}
class BuyTicket implements Runnable{
private int tickNums=10; // private 表示操作的是同一对象的
boolean flag=true; // 标志位,用于线程的外部停止方式
@Override
public void run() {
while(flag){
buy();
}
}
private void buy() { //买票
if (tickNums <= 0){ //判断是否有票
flag=false;
return;
}
try {
Thread.sleep(100); //模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到"+tickNums--);
}
}
结果出现了负数,分析:只剩一张票的时候,3个线程都可以抢票,记住每个线程都有自己的工作内存,内存控制不当会造成数据不一致,每个线程会把这个剩下的1放到自己的内存,于是都买了
线程安全(synchronized
)
//synchronized同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
if (tickNums <= 0){ //判断是否有票
flag=false;
return;
}
// Thread.sleep(10); //模拟延时
System.out.println(Thread.currentThread().getName()+"拿到"+tickNums--);
}
银行取钱
不安全取钱
public class UnsafeBank {
public static void main(String[] args) {
Account account=new Account(100,"结婚基金");
Drawing you=new Drawing(account,50,"你");
Drawing girl=new Drawing(account,100,"girlFriend");
you.start();
girl.start();
}
}
class Account{ // 账户
int money; // 余额
String name;// 卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
// 银行
class Drawing extends Thread{ //使用继承Thread的原因是:不涉及多个多个线程操作同一对象???
Account account; // 账户 account 不能加 private,不然就是同一对象的
int drawingMoney; // 取多少钱
int nMoney; // 现在有多少钱
public Drawing(Account account, int drawingMoney, String name) {
super(name); // 调用父类的方法
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
if(account.money-drawingMoney<0){ // 判断有没有钱
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
try {
Thread.sleep(100); // 模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money-drawingMoney; // 卡内余额
nMoney = nMoney+drawingMoney; // 现在手里的钱
System.out.println(account.name+"余额为:"+account.money);
//Thread.currentThread().getName()=this.getName()
System.out.println(this.getName()+"手里的钱"+nMoney);
}
}
结果出现负数,不安全
安全取钱( synchronized 同步块
)
public synchronized void run() {...} // 锁的银行
@Override
public void run() {
synchronized(account){ // 锁 account:共享资源
if(account.money-drawingMoney<0){ //判断有没有钱
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
try {
Thread.sleep(100); //模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money-drawingMoney; // 卡内余额
nMoney = nMoney+drawingMoney; // 现在手里的钱
System.out.println(account.name+"余额为:"+account.money);
//Thread.currentThread().getName()=this.getName()
System.out.println(this.getName()+"手里的钱"+nMoney);
}
}
因为 synchronized 默认所得独享是 this ,那么这里就是锁的银行
,但是我们操作是对 account 进行操作的,银行是没有变的,所以我们需要 synchronized 同步块
,锁应该是 account
由于可以通过 private 关键字来保证数据对象只能被方法访问
,所以只需要针对方法
提出一套机制。这套机制就是 synchronized 关键字,它包括两种用法: synchronized 方法
和 synchronized 代码块
synchronized 方法控制对“对象”的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞
,方法一旦执行,就独占该锁
,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
同步方法弊端:
大的方法
申明为 synchronized 将会影响效率
只读
代码是不需要加锁
的,方法里面需要修改的内容
才需要锁,锁的太多,浪费资源
格式:synchronized(Obj
){}
Obj 称之为同步监视器,Obj 可以是任何对象
,但是推荐使用共享资源作为同步监视器
同步方法中
无需指定同步监视器,因为同步方法的同步监视器
就是 this ,就是这个对象本身,或者是 class
在集合上的应用
多线程不安全的
集合,使用同步方法处理
public class UnsafeList{
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for(int i = 0; i < 10000; i++){
new Thread(()->{
synchronized(list){ // 同步代码块 同步监视器为list
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(3000); //延时 3s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size()); // 10000
}
}
多线程安全的集合:CopyOnWriteArrayList(写入时复制)
import java.util.concurrent.CopyOnWriteArrayList;
public class testJuuc implements Runnable {
static CopyOnWriteArrayList<String>list=new CopyOnWriteArrayList<String>();
public static void main(String[] args) throws InterruptedException {
testJuuc thread=new testJuuc();
for (int i = 0; i < 10; i++) {
new Thread( thread).start();
}
Thread.sleep(3000);
System.out.println(list.size());
System.out.println(list.toString());
}
@Override
public void run() {
list.add(Thread.currentThread().getName());
}
}
从 JDK 5.0
开始,Java 提供了更强大的线程同步机制——通过显式定义
同步锁对象
来实现同步。同步锁
使用 Lock 对象充当
java.util.concurrent.locks.Lock
接口是控制多个线程
对共享资源进行访问的工具。锁提供了对共享资源
的独占访问
,每次只能有一个线程
对 Lock 对象加锁
,线程开始访问共享资源之前
应先获得
Lock 对象
ReentrantLock (可重入锁) 类实现了 Lock ,它拥有与 synchronized 相同的并发性
和内存语义
,在实现线程安全的控制中,比较常用的是 ReentrantLock
,可以显式加锁、释放锁
class A{
private final ReentrantLock lock = new Reen TrantLock();
public void m(){
try{
lock.lock(); // 加锁 保证线程安全的代码;
}
finally{
lock.unlock(); // 解锁 如果同步代码有异常,要将unlock()写入finally语句块
}
}
}
Synchronized 与 Lock 对比
Lock 是显式锁(手动开启和关闭,别忘记关闭锁)
Synchronized 是隐式锁,出了作用域自动释放
Lock 只有代码块锁
Synchronized 有代码块锁和方法锁
使用 Lock 锁,JVM 将花费较少的时间
来调度线程,性能更好。并且有更好的扩展性(提供更多的子类
)
优先使用顺序
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
多个线程
各自占有一些共享资源
,并且互相
等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行
的情形,某一个同步块
同时拥有“两个以上对象的锁
”时,就可能会发生死锁的问题
public class DeadLock {
public static void main(String[] args) {
Makeup girl1=new Makeup(0,"灰姑凉");
Makeup girl2=new Makeup(1,"白雪公主");
girl1.start();
girl2.start();
}
}
class Lipstick{ } //口红
class Mirror{ }//镜子
class Makeup extends Thread{
static Lipstick lipstick=new Lipstick(); //需要的资源只有一份,用static修饰来保证只有一份
static Mirror mirror=new Mirror();
int choice;
String girlName; //使用化妆品的人
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if(choice==0){
synchronized (lipstick){//获得口红的锁
System.out.println(this.girlName+"获得口红的锁");
Thread.sleep(1000);
synchronized (mirror){//一秒中后获得镜子的锁
System.out.println(this.girlName+"获得镜子的锁");
}
}
}else{
synchronized (mirror){//获得镜子的锁
System.out.println(this.girlName+"获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick){//一秒中后获得口红的锁
System.out.println(this.girlName+"获得口红的锁");
}
}
}
}
}
解决方法
把同步块拿出来,不同时获得两把锁
private void makeup() throws InterruptedException {
if(choice==0){
synchronized (lipstick){//获得口红的锁
System.out.println(this.girlName+"获得口红的锁");
Thread.sleep(1000);
}
synchronized (mirror){//一秒中后获得镜子的锁
System.out.println(this.girlName+"获得镜子的锁");
}
}else{
synchronized (mirror){//获得镜子的锁
System.out.println(this.girlName+"获得镜子的锁");
Thread.sleep(2000);
}
synchronized (lipstick){//一秒中后获得口红的锁
System.out.println(this.girlName+"获得口红的锁");
}
}
}
产生死锁的四个必要条件
互斥条件
:一个资源每次只能被一个进程使用请求与保持条件
:一个进程因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件
:进程已获得的资源,在末使用完之前,不能强行剥夺循环等待条件
:若干进程之间形成一种头尾相接的循环等待资源关系线程通信:就是线程之间的交流
并发协作模型:生产者/消费者模式
生产者生产,消费者消费,两个线程之间可以通信
生产者和消费者共享同一资源,并且生产者和消费者之间相互依赖,互为条件
对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
在生产者消费者问题中,仅有 synchronized 是不够的
synchronized 可阻止并发更新
同一个共享资源实现同步, 不能
用来实现不同线程之间的消息传递(通信)
解决线程通信的方法 (等待唤醒)
Java提供了几个方法解决线程之间的通信问题
注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常
方法 | 描述 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,会释放锁,sleep 不会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyall() | 唤醒同一个对象上所有调用 wait 方法的线程,优先级别高的线程优先调度 |
生产者消费者问题解决方式
管程法、信号灯法
生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
缓冲区∶消费者不能直接使用生产者的数据,他们之间有个缓冲区
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
public class TestPC {
public static void main(String[] args) {
SynContainer container=new SynContainer();
new Productor( container).start();
new Productor( container).start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
@Override
public void run() { //生产
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了"+i+"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
public SynContainer getContainer() {
return container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了"+ container.pop().id+"只鸡");
}
}
}
//产品
class Chicken{
int id;//产品编号
public Chicken(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer{
Chicken[] chickens=new Chicken[10]; // 需要一个容器大小 能放10只鸡
int count=0; //容器计数器
//生产者放入产品 涉及到并发,需要同步
public synchronized void push(Chicken chicken){
if(count==chickens.length){ //如果容器满了,就需要等待消费者消费
try {
this.wait(); //通知消费者消费,生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count]=chicken; //如果没有满,就需要加入产品
count++;
this.notifyAll(); //可以通知消费者消费了
}
//消费者消费产品
public synchronized Chicken pop(){
if(count==0){ //判断能否消费
try {
this.wait(); //等待生产者生产,消费者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--; //如果可以消费
Chicken chicken=chickens[count];
this.notifyAll(); //通知生产者生产
return chicken;
}
}
信号灯法:即设置标志位
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者:演员
class Player extends Thread{
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {;
for (int i = 0; i < 20; i++) {
if(i%2==0){
this.tv.play("快乐大本营"); // 节目
}else{
this.tv.play("抖音:记录美好生活"); // 广告
}
}
}
}
//消费者:观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品:节目
class TV{
String voice; //表演的节目
boolean flag=true; //true:演员表演。false:演员等待
public synchronized void play(String voice){ //表演
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了"+voice);
this.notifyAll();//通知唤醒 通知观众观看
this.voice=voice;
this.flag=!this.flag;
}
public synchronized void watch(){ //观看
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:"+voice);
this.notifyAll(); //通知演员表演
this.flag=!flag;
}
}
背景:线程经常创建和销毁
、使用量特别大的资源,比如并发
情况下的线程,对性能影响很大
思路:提前创建
好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
好处:
线程池
JDK 5.0 起提供了线程池
相关API:ExecutorService 和 Executors(工具类、线程池的工厂类)
ExecutorService
:真正的线程池接口。常见子类 ThreadPoolExecutor
线程池核心参数
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止线程池常用方法
方法 | 说明 |
---|---|
execute(Runnable command) | 执行任务/命令,没有返回值 ,一般用来执行 Runnable |
submit(Callables task) | 执行任务,有返回值 ,一般用来执行 Callable |
shutdown() | 关闭连接池 |
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
线程池的简单使用
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestPool {
public static void main(String[] args) {
// 1.创建服务,创建线程池
ExecutorService service= Executors.newFixedThreadPool(10); // 线程池大小
service.execute(new MyThread()); // 2.执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.shutdown(); // 3.关闭连接
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"此生辽阔");
}
}
Stream 它并不是一个容器,它只是对容器的功能
进行了增强,添加了很多便利的操作,如查找、过滤、分组、排序
等一系列的操作。并且有串行、并行两种执行模式,并行模式
充分的利用了多核处理器
的优势,使用 ForkJoin 框架进行了任务拆分
,同时提高了执行速度
简而言之,Stream 就是提供了一种高效且易于使用的处理数据方式
Stream 特点
不存储元素
源对象
。相反,会返回一个持有结果的新 Stream
需要结果
的时候才执行,也就是执行终端操作
的时候Stream 执行流程
获取一个流
数据进行处理
返回结果
为什么需要流式操作
集合 API 是 Java API 中最重要的部分。基本上每一个 java 程序都离不开集合,但现有的集合处理
在很多方面都无法满足需要
现有集合处理数据,代码繁琐,不够简洁,需要用控制流程
自己实现
所有数据查询的底层细节
许多其他的语言
或者类库
以声明的方式
来处理特定的数据模型
,比如 SQL 语言(可从表中查询,按条件过滤数据
,并且以某种形式将数据分组
,而不需了解查询是如何实现的——数据库帮你做所有的脏活。这样做的好处是代码很简洁)。而 Java 需要用控制流程自己实现
所有数据查询的底层细节
现有集合无法处理大量数据
传统情况下,为了加快处理过程,你会利用多核架构
。但是并发程序不太好写,而且很容易出错
Stream API 很好的解决了这两个问题。它抽象出一种叫做流的东西以声明的方式处理数据,更重要的是,它还实现了多线程,帮你处理底层
诸如线程、锁、条件变量、易变变量等
Stream 的创建方式:
Java 8 中的 Collection 接口
被扩展,提供了两个获取流的方法,这两个方法是 default 方法
,也就是说所有实现 Collection 的接口都不需要实现就可以直接使用
Collection 接口
获取流的方法:
stream
() : 返回一个串行流
parallelStream
() :返回一个并行流
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Stream<Integer> stream = list.stream(); // 获得串行流
Stream<Integer> stream1 = list.parallelStream(); // 获得并行流
Java 8 中的 Arrays
的静态方法 stream() 可以获取数组流
int[] array = {1,2,3};
Stream<Integer> stream = Arrays.stream(array);
使用静态方法 Stream.of(),通过显示值
创建一个流。它可以接收任意数量
的参数
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8);
如果 Stream 只有中间操作是不会执行的
,当执行终端操作
的时候才会执行中间操作,这种方式称为延迟加载或惰性求值。多个中间操作组成一个中间操作链,只有当执行终端操作的时候才会执行一遍中间操作链
准备数据
//计算机俱乐部
private static List<Student> computerClub = Arrays.asList(
new Student("2015134001", "小明", 15, "1501"),
new Student("2015134003", "小王", 14, "1503"),
new Student("2015134006", "小张", 15, "1501"),
new Student("2015134008", "小梁", 17, "1505")
);
//篮球俱乐部
private static List<Student> basketballClub = Arrays.asList(
new Student("2015134012", "小c", 13, "1503"),
new Student("2015134013", "小s", 14, "1503"),
new Student("2015134015", "小d", 15, "1504"),
new Student("2015134018", "小y", 16, "1505")
);
//乒乓球俱乐部
private static List<Student> pingpongClub = Arrays.asList(
new Student("2015134022", "小u", 16, "1502"),
new Student("2015134021", "小i", 14, "1502"),
new Student("2015134026", "小m", 17, "1504"),
new Student("2015134027", "小n", 16, "1504")
);
private static List<List<Student>> allClubStu = new ArrayList<>();
allClubStu.add(computerClub);
allClubStu.add(basketballClub);
allClubStu.add(pingpongClub);
distinct 可以对 Stream 中包含的元素进行去重操作
(去重逻辑依赖元素的 equals 方法),新生成的 Stream 中没有重复的元素
List<String> list = Arrays.asList("b","b","c","a");
list.forEach(System.out::print); //bbca
list.stream().distinct().forEach(System.out::print);//bca
filter 可以对 Stream 中包含的元素使用给定的过滤函数
进行过滤操作
,新生成的 Stream 只包含符合条件的元素
private static List<List<Student>> allClubStu = new ArrayList<>(); // 创建集合
allClubStu.add(computerClub);
computerClub.stream().filter( //筛选1501班的学生
e -> e.getClassNum().equals("1501")).forEach(System.out::println);
List<Student> collect = computerClub.stream().filter( //筛选年龄大于15的学生
e -> e.getAge() > 15).collect(Collectors.toList());
map 可以对 Stream 中包含的元素使用给定的转换函数
进行转换操作
,新生成的 Stream 只包含转换生成的元素。
新的 Stream
,这个新生成的 Stream 中的元素都是 int 类型之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗
//篮球俱乐部所有成员名 + 暂时住上商标^_^,并且获取所有队员名
List<String> collect1 = computerClub.stream()
.map(e -> e.getName() + "^_^")
.collect(Collectors.toList());
collect1.forEach(System.out::println);
flatMap 和 map
类似,不同的是其每个元素转换得到的是 Stream对象,会把子 Stream 中的元素
压缩到父集合中
//获取年龄大于15的所有成员
List<Student> collect2 = Stream.of(basketballClub, computerClub, pingpongClub)
.flatMap(e -> e.stream().filter(s -> s.getAge() > 15))
.collect(Collectors.toList());
collect2.forEach(System.out::println);
//用双层list获取所有年龄大于15的俱乐部成员
List<Student> collect3 = allClubStu.stream()
.flatMap(e -> e.stream().filter(s -> s.getAge() > 15))
.collect(Collectors.toList());
collect3.forEach(System.out::println);
peek 生成一个包含原 Stream 的所有元素的新 Stream,同时会提供一个消费函数
(Consumer实例),新 Stream 每个元素被消费的时候
都会执行给定的消费函数
//篮球俱乐部所有成员名 + 赞助商商标^_^,并且获取所有队员详细内容
List<Student> collect = basketballClub.stream()
.peek(e -> e.setName(e.getName() + "^_^"))
.collect(Collectors.toList());
collect.forEach(System.out::println);
//Student{idNum='2015134012', name='小c^_^', age=13, classNum='1503'}
limit 可对 Stream 进行截断操作
,获取其前 N 个元素
,如果原 Stream 中包含的元素个数小于 N
,那就获取其所有的元素
List<String> list = Arrays.asList("a","b","c");
// 获取 list 中 top2 即截断取前两个
List<String> collect1 = list.stream().limit(2).collect(Collectors.toList());
collect1.forEach(System.out::print); // ab
skip 返回一个丢弃原
Stream 的前 N 个
元素后剩下元素
组成的新 Stream,如果原Stream中包含的元素个数小于 N
,那么返回空 Stream
List<String> list = Arrays.asList("a","b","c");
// 获取 list 中 top2 即截断取前两个
List<String> collect1 = list.stream().skip(2).collect(Collectors.toList());
collect1.forEach(System.out::print); // c
sorted有两种形式存在:
Comparator
):指定比较规则
进行排序。自然顺序
排序List<String> list = Arrays.asList("b","c","a");
// 获取 list 中 top2 即截断取前两个
List<String> collect1 = list.stream().sorted().collect(Collectors.toList());
collect1.forEach(System.out::print);//abc
Stream 中间操作
返回的是 Stream,终端操作
返回的就是最终转换的结果
foreach(Consumer c) :遍历
操作
collect(Collectors): 将流转化
为其他形式
其中 Collectors 具体方法有:
toList
: List 把流中元素收集到 List 中toSet
:Set 把流中元素收集到 Set 中toCollection
:Coolection 把流中元素收集到 Collection 中groupingBy
:MappartitioningBy
:MapgroupingByConcurrent
:Mapjoining
:字符串拼接List<Student> collect = computerClub.stream().collect(Collectors.toList());
Set<Student> collect1 = pingpongClub.stream().collect(Collectors.toSet());
//注意 key 必须是唯一的 如果不是唯一的会报错而不是像普通map那样覆盖 把流中元素收集到 Map 中
Map<String, String> collect2 =
pingpongClub.stream()
.collect(Collectors.toMap(Student::getIdNum, Student::getName));
//分组 类似于数据库中的 group by 分组
Map<String, List<Student>> collect3 =
pingpongClub.stream()
.collect(Collectors.groupingBy(Student::getClassNum));
//字符串拼接 参数1:分隔符 参:2:前缀 参:3:后缀
String collect4 = pingpongClub.stream()
.map(Student::getName)
.collect(Collectors.joining(",", "【", "】")); //【小u,小i,小m,小n】
//三个俱乐部符合年龄要求的按照班级分组
Map<String, List<Student>> collect5 =
Stream.of(basketballClub, pingpongClub, computerClub)
.flatMap(e -> e.stream().filter(s -> s.getAge() < 17)) // 选出所有小于17岁的
.collect(Collectors.groupingBy(Student::getClassNum)); // 按班级分组
//按照是否年龄>16进行分组 key为true和false
ConcurrentMap<Boolean, List<Student>> collect6 =
Stream.of(basketballClub, pingpongClub, computerClub)
.flatMap(Collection::stream)
.collect(Collectors.groupingByConcurrent(s -> s.getAge() > 16));
匹配操作的方法有:
allMatch
(Predicate) :都符合anyMatch
(Predicate): 任一元素符合noneMatch
(Predicate) :都不符合boolean b = basketballClub.stream().allMatch(e -> e.getAge() < 20);
boolean b1 = basketballClub.stream().anyMatch(e -> e.getAge() < 20);
boolean b2 = basketballClub.stream().noneMatch(e -> e.getAge() < 20);
寻找操作的方法有:
findFirst
:返回当前流中的第一个
元素findAny
:返回当前流中的任意
元素Optional<Student> first = basketballClub.stream().findFirst(); // 返回当前流中的第一个元素
if (first.isPresent()) { // 判断有没有元素
Student student = first.get();
System.out.println(student);
}
Optional<Student> any = basketballClub.stream().findAny(); // 返回当前流中的任意元素
if (any.isPresent()) { // 判断有没有元素
Student student2 = any.get();
System.out.println(student2);
}
Optional<Student> any1 = basketballClub.stream().parallel().findAny(); // 开启并行流,获得
System.out.println(any1);
计数和极值的方法有:
long count = basketballClub.stream().count(); // 返回流中元素的总个数
Optional<Student> max = basketballClub.stream()
.max(Comparator.comparing(Student::getAge)); // max
if (max.isPresent()) {
Student student = max.get();
}
Optional<Student> min = basketballClub.stream()
.min(Comparator.comparingInt(Student::getAge));
if (min.isPresent()) {
Student student = min.get();
}
Stream 的并行模式使用了 Fork/Join 框架
Fork/Join框架是 java 7 中加入的一个并行任务框架,可以将任务拆分
为多个小任务,每个小任务执行完的结果再合并
成为一个结果。在任务的执行过程中使用工作窃取(work-stealing)算法,减少线程之间的竞争
工作窃取算法:就是多线程同步执行,当一个线程把自己队列任务完成后
去“窃取
”其他线程队列任务
继续干。而在这时它们会访问同一个队列
,所以为了减少窃取
任务线程和被窃取
任务线程之间的竞争
,通常会使用双端队列,被窃取
任务线程永远从双端队列的头部
拿任务执行,而窃取
任务的线程永远从双端队列的尾部
拿任务执行
Stream 没用之前,针对集合的遍历筛选等操作更多的是for-loop/while-loop,用了 Stream 后发现原来代码可以更加简洁
,并且类似 SQL 语句,甚至可以做很多复杂的动作
Stream 使用方式类似 SQL 语句,提供对集合运算的高阶抽象
,可以将其处理的元素集合看做一种数据流
,流在管道中传输,数据在管道节点上进行处理,比如筛选、排序、聚合等。
数据流
在管道中经过中间操作处理
,由终止操作得到前面处理的结果
Stream操作分为两类:
中间操作:将流一层层的进行处理,并向下一层进行传递,如 filter map sorted等
有状态
:必须等上一步操作完拿到全部元素后才可操作,如 sorted
无状态
:该操作的数据不受上一步操作的影响,如 filter map
终止操作:触发数据的流动,并收集结果,如 collect findFirst forEach等。
短路操作
:会在适当的时刻终止遍历
,类似于 break,如 anyMatch findFirst等非短路操作
:会遍历所有
元素,如 collect max等Stream 的实现使用流水线的方式巧妙的避免了多次迭代
,基本思想是一次迭代
中尽可能多的执行用户指定的操作
。
Stream 采用某种方式记录用户每一步的操作
,中间操作会返回流对象
,多个操作
最终串联
成一个管道,管道并不直接操作数据
,当用户调用终止操作时将之前记录的操作叠加
到一起,尽可能地在一次迭代中
全部执行掉
操作如何记录
Stream 中使用 Stage 的概念来描述一个完整的操作,并用某种实例化
后的 PipelineHelper 来代表 Stage,将各 Pipeline 按照先后顺序连接到一起,就构成了整个流水线
与 Stream 相关类和接口的继承关系如下图,其中蓝色表示继承关系,绿色表示接口实现:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DiZN0dFl-1648561802077)(link-picture\20210221182246981.png)]
使用 Collection.stream
、Arrays.stream
或 Stream.of
等接口会生成 Head
Head 用于表示第一个 Stage
,该 Stage 不包含任何操作
。StatelessOp 和 StatefulOp 分别表示无状态
和有状态
的Stage
操作如何叠加
叠加后的操作如何执行
执行结果在哪儿
坑点,这些有点复杂,就不深究了
使用 Collection.parallelStream
或 Stream.parallel
等方法可以将当前的 Stream 流标记为并行执行
在坑点中提到,在调用 Stream 的终止操作时,会执行 AbstractPipeline.evaluate
方法,根据 paraller 标识是执行并行
操作还是串行
操作
return isParallel()
?terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) //并发执行
:terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));//串行执行
如果被标记为 sequential ,则会调用 TerminalOp.evaluateSequential
串行执行
(evaluateSequential的调用过程在坑点中提到)
如果被标记为 parallel ,则会调用 TerminalOp.evaluateParallel
并行执行
对于AbstractPipeline.evaluate
方法,不同的 TerminalOp 会有不同的实现,但都使用了 ForkJoin 框架,将原始数据不断拆分
为更小的单元,对每一个单元做 evaluateSequential 类似的动作,最后将每一个单元计算的结果依次整合
,得到最终结果
默认情况下,ForkJoin 的线程数即为机器的 CPU 核数
Stream.of(basketballClub, pingpongClub, computerClub)
.flatMap(Collection::stream)
.collect(Collectors.groupingByConcurrent(s -> s.getAge() > 16));
### 4.2 匹配操作
匹配操作的方法有:
1. booelan `allMatch`(Predicate) :都符合
2. boolean `anyMatch`(Predicate): 任一元素符合
3. boolean `noneMatch`(Predicate) :都不符合
~~~java
boolean b = basketballClub.stream().allMatch(e -> e.getAge() < 20);
boolean b1 = basketballClub.stream().anyMatch(e -> e.getAge() < 20);
boolean b2 = basketballClub.stream().noneMatch(e -> e.getAge() < 20);
寻找操作的方法有:
findFirst
:返回当前流中的第一个
元素findAny
:返回当前流中的任意
元素Optional<Student> first = basketballClub.stream().findFirst(); // 返回当前流中的第一个元素
if (first.isPresent()) { // 判断有没有元素
Student student = first.get();
System.out.println(student);
}
Optional<Student> any = basketballClub.stream().findAny(); // 返回当前流中的任意元素
if (any.isPresent()) { // 判断有没有元素
Student student2 = any.get();
System.out.println(student2);
}
Optional<Student> any1 = basketballClub.stream().parallel().findAny(); // 开启并行流,获得
System.out.println(any1);
计数和极值的方法有:
long count = basketballClub.stream().count(); // 返回流中元素的总个数
Optional<Student> max = basketballClub.stream()
.max(Comparator.comparing(Student::getAge)); // max
if (max.isPresent()) {
Student student = max.get();
}
Optional<Student> min = basketballClub.stream()
.min(Comparator.comparingInt(Student::getAge));
if (min.isPresent()) {
Student student = min.get();
}
Stream 的并行模式使用了 Fork/Join 框架
Fork/Join框架是 java 7 中加入的一个并行任务框架,可以将任务拆分
为多个小任务,每个小任务执行完的结果再合并
成为一个结果。在任务的执行过程中使用工作窃取(work-stealing)算法,减少线程之间的竞争
工作窃取算法:就是多线程同步执行,当一个线程把自己队列任务完成后
去“窃取
”其他线程队列任务
继续干。而在这时它们会访问同一个队列
,所以为了减少窃取
任务线程和被窃取
任务线程之间的竞争
,通常会使用双端队列,被窃取
任务线程永远从双端队列的头部
拿任务执行,而窃取
任务的线程永远从双端队列的尾部
拿任务执行
Stream 没用之前,针对集合的遍历筛选等操作更多的是for-loop/while-loop,用了 Stream 后发现原来代码可以更加简洁
,并且类似 SQL 语句,甚至可以做很多复杂的动作
Stream 使用方式类似 SQL 语句,提供对集合运算的高阶抽象
,可以将其处理的元素集合看做一种数据流
,流在管道中传输,数据在管道节点上进行处理,比如筛选、排序、聚合等。
数据流
在管道中经过中间操作处理
,由终止操作得到前面处理的结果
Stream操作分为两类:
中间操作:将流一层层的进行处理,并向下一层进行传递,如 filter map sorted等
有状态
:必须等上一步操作完拿到全部元素后才可操作,如 sorted
无状态
:该操作的数据不受上一步操作的影响,如 filter map
终止操作:触发数据的流动,并收集结果,如 collect findFirst forEach等。
短路操作
:会在适当的时刻终止遍历
,类似于 break,如 anyMatch findFirst等非短路操作
:会遍历所有
元素,如 collect max等Stream 的实现使用流水线的方式巧妙的避免了多次迭代
,基本思想是一次迭代
中尽可能多的执行用户指定的操作
。
Stream 采用某种方式记录用户每一步的操作
,中间操作会返回流对象
,多个操作
最终串联
成一个管道,管道并不直接操作数据
,当用户调用终止操作时将之前记录的操作叠加
到一起,尽可能地在一次迭代中
全部执行掉
操作如何记录
Stream 中使用 Stage 的概念来描述一个完整的操作,并用某种实例化
后的 PipelineHelper 来代表 Stage,将各 Pipeline 按照先后顺序连接到一起,就构成了整个流水线
与 Stream 相关类和接口的继承关系如下图,其中蓝色表示继承关系,绿色表示接口实现:
[外链图片转存中…(img-DiZN0dFl-1648561802077)]
使用 Collection.stream
、Arrays.stream
或 Stream.of
等接口会生成 Head
Head 用于表示第一个 Stage
,该 Stage 不包含任何操作
。StatelessOp 和 StatefulOp 分别表示无状态
和有状态
的Stage
操作如何叠加
叠加后的操作如何执行
执行结果在哪儿
坑点,这些有点复杂,就不深究了
使用 Collection.parallelStream
或 Stream.parallel
等方法可以将当前的 Stream 流标记为并行执行
在坑点中提到,在调用 Stream 的终止操作时,会执行 AbstractPipeline.evaluate
方法,根据 paraller 标识是执行并行
操作还是串行
操作
return isParallel()
?terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) //并发执行
:terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));//串行执行
如果被标记为 sequential ,则会调用 TerminalOp.evaluateSequential
串行执行
(evaluateSequential的调用过程在坑点中提到)
如果被标记为 parallel ,则会调用 TerminalOp.evaluateParallel
并行执行
对于AbstractPipeline.evaluate
方法,不同的 TerminalOp 会有不同的实现,但都使用了 ForkJoin 框架,将原始数据不断拆分
为更小的单元,对每一个单元做 evaluateSequential 类似的动作,最后将每一个单元计算的结果依次整合
,得到最终结果
默认情况下,ForkJoin 的线程数即为机器的 CPU 核数