Java-JUC并发编程
学习视频: B站 狂神说java –【狂神说Java】JUC并发编程最新版通俗易懂_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
1.什么是JUC
JUC就是java.util.concurrent下面的类包,专门用于多线程的开发。
java.util工具包。
业务:对于我们原来实现多线程的代码,实现三种接口, Thread、Runnable、Callable 三种创建线程的方式。
普通的线程代码 :Thread。而Runnable接口,是要将实现Runnable接口的 run 方法 再次丢入到 Thread中, 不过它没有返回值。 而Callable有返回值。
- Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹地去执行 run()方法中的代码而已;
- Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask配合可以用来获取异步执行的结果。
2.线程和进程
2.1 进程:
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。
在java中,当我们启动一个 main函数就是启动了一个 JVM 的进程,而main函数所在的线程就是主线程, 然后还会间隔的执行多个线程,就是同一进程中的多个线程在并发执行。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
2.2 线程
线程是进程的一个实体,是 cpu 调度和分派的基本单 位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
对于Java而言:Thread、Runable、Callable进行开启线程的。
Java中默认有几个线程?2个线程! main线程、GC线程 垃圾回收。
提问?JAVA真的可以开启线程吗?
-
开不了的!
-
Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。
例子:
// 开启一个线程, 查看它运行的 start方法
package com.AL.demo01;
public class Test1 {
public static void main(String[] args) {
new Thread().start();
//获取cpu的核数
System.out.println(Runtime.getRuntime().availableProcessors());
//Thread.State. //观察线程的状态
}
}
start 方法的源码:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0(); // strat0 方法,
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//这是一个native的一个本地方法,它调用的底层的C++代码。Java是没有权限操作底层硬件的
private native void start0();
此时的 start0 被关键字 native修饰,表示本地方法,调用的底层 C++ 代码。
2.3 并发和并行
并发:多线程操作同一个资源。
CPU 只有一核,模拟出来多条线程,天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。
所以并发编程的本质:充分利用 CPU 资源。
并行: 多个人一起行走
CPU多核,多个线程可以同时执行。 我们可以使用线程池!
-
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
-
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop 分布式集群。所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
2.4线程的状态
public enum State {
//新生. 新生后立马进入就绪状态。
NEW,
//运行。 runnable里面其实有两个 一个是 就绪runnable,另一个是运行 running。 不过这两个都被认为是 运行中。 就绪:new完之后马上就是 就绪,而不是直接去运行, 要等待CPU的调度和分配 才进入 running。
RUNNABLE,
//阻塞
BLOCKED,
//等待。 死死的等
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
2.5、wait和sleep的区别
1、来自不同的类:
wait => Object
sleep => Thread
一般情况企业中使用休眠是:不会去使用 Thread的sleep。
TimeUnit.DAYS.sleep(1); //休眠1天
TimeUnit.SECONDS.sleep(1); //休眠1s
2、关于锁的释放
wait 会释放锁;
sleep睡觉了,不会释放锁;
-
sleep() 方法正在执行的线程主动让出 CPU(然后CPU就可以去执行其他任务),在 sleep 指定时间后CPU 再回到该线程继续往下执行(注意:sleep 方法 只让出了 CPU,而并不会释放同步资源锁);
-
wait() 方法则是指当前线程让自己暂时退让 出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了 notify()方法,之前调用 wait() 的线程才会解除 wait 状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify 的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify 只是让之前调用 wait 的线程有权利重新参与线程的调度);【notify() 去 对 wait()的线程唤醒,让wait() 这个线程自己再去参与竞争同步资源锁。 】
3、使用的范围是不同的:
wait 必须在同步代码块中;
sleep 可以在任何地方睡;
4、是否需要捕获异常:
wait是不需要捕获异常;
sleep必须要捕获异常;
3、Lock
为什么要有锁? 因为多个线程共享的内存资源,可能会在各自的线程内进行了变化。
每个线程都有自己的、私有的:程序计数器、虚拟机栈、本地方法栈,去进行执行线程。在这其中就会发生内存资源泄露,数据紊乱的问题。 为了保证多线程的安全,
- synchornized、lock 加锁[保证原子性,要么都成功,要么都失败]。 提供互斥访问,同一时刻只能有一个线程对数据进行操作
- synchornized, volatile完成可见性。一个线程对主内存的修改可以及时地被其他线程看到,
- 有序性,禁止指令重排。 volatile 通过内存屏障 去禁止指令重排。
3.1、传统的synchornized
解决多线程并发问题时使用 synchornized,去修饰 (锁住) 代码块或者静态方法、实例方法等。
在其中我们使用 lambda表达式, 即函数式接口【接口中只有一个方法时】。
lombda表达式语法: () -> { }。括号里面写参数, {}里面写代码块。
package com.AL.demo01;
public class SaleTicketDemo01 {
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();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "C").start();
}
}
//资源类 OOP思想, 属性和方法。 和上面的就能解耦
class Ticket {
private int number = 40;
// 卖票的方式
//public void sale() {
// synchronized 本质是队列,锁
public synchronized void sale() { // 设置锁,即 synchronized
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
}
}
结果:
############ 在没有使用 synchornized 锁的时候,会造成数据紊乱,重复
A卖出了第35张票剩余34张票
A卖出了第33张票剩余32张票 // 重复了
A卖出了第32张票剩余31张票
A卖出了第31张票剩余30张票
B卖出了第34张票剩余32张票 //
############ 设置锁,即 synchronized
A卖出了第30张票剩余29张票
A卖出了第29张票剩余28张票
A卖出了第28张票剩余27张票
B卖出了第27张票剩余26张票
B卖出了第26张票剩余25张票
B卖出了第25张票剩余24张票
3.2、Lock
Package java.util.concurrent.locks 中的 lock 锁:
Lock接口。
模块 java.base
软件包 java.util.concurrent.locks
Interface Lock
-
所有已知实现类:
ReentrantLock
,ReentrantReadWriteLock.ReadLock
,ReentrantReadWriteLock.WriteLock
这三种锁分别是 可重入锁[常用的]、 读锁 和 写锁[专门用于读写操作的]。
lock锁的语法: 加锁,解锁:
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
RenntrantLock : 实现了 lock 接口,功能和 synchronized一样, 不过还有扩展。主要有三点:①等待可中断;==②可实现公平锁;==③可实现选择性通知(锁可以绑定多个条件);④ 性能已不是选择标准。
-
ReenTrantLock 提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly( )来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
-
ReenTrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock 默认情况是非公平的,可以通过 ReenTrantLock 类的 ReentrantLock(boolean fair) 构造方法来制定是否是公平的。【公平锁:不能插队】
-
synchronized 关键字与 wait() 和 notify/notifyAll() 方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
查看 ReentrantLock 的源码:
-
公平锁:十分公平: 先来后到
-
非公平锁: 十分不公平: 可以插队。 默认的是非公平锁。
public ReentrantLock() {
sync = new NonfairSync(); // 非公平锁
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) { // 可以设置修改为 公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
例子:售票的例子,采用 lock 锁去解决多线程并发。
lock三步:
- new ReentrantLock, 创建 lock 锁
- lock.lock(); 加锁
- lock.unlock; 解锁
package com.AL.demo01;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketDemo02 {
public static void main(String[] args) {
final Ticket2 ticket = new Ticket2();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "C").start();
}
}
//lock三部曲
//1、 Lock lock=new ReentrantLock();
//2、 lock.lock() 加锁
//3、 finally=> 解锁:lock.unlock();
class Ticket2 {
private int number = 30;
// 创建锁
Lock lock = new ReentrantLock();
//卖票的方式
public synchronized void sale() {
lock.lock(); // 开启锁
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
}finally {
lock.unlock(); // 关闭锁
}
}
}
3.3、Synchronized 和 Lock 的区别
1、Synchronized 内置的Java关键字,Lock是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock可以判断
3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!如果不释放,可能会遇到死锁
4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。
5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁; 在lock参数中加一个 true 就是公平锁。
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
3.4、生产者和消费者问题
使用 Synchronized 版本: 那么会有 wait notify, 等待和唤醒
JUC 版本: lock 锁
进行生产者消费者的问题的时候: 6字口诀: 等待,业务,通知。
3…4.1、Synchronized 版本的生产者和消费者问题:
package com.AL.PC;
/**
* 线程之间的通信问题: 生产者和消费者问题。 等待唤醒,通知唤醒
* 线程交替执行: A B 去操作同一个资源 num
* A num++
* B num--
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
//
// new Thread(() -> {
// for (int i = 0; i < 10; i++) {
// try {
// data.increment();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }, "C").start();
// new Thread(() -> {
// for (int i = 0; i < 10; i++) {
// try {
// data.decrement();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }, "D").start();
}
}
// 业务: 资源类
class Data {
private int num = 0;
// +1
public synchronized void increment() throws InterruptedException {
// 判断等待
if (num != 0) {
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 判断等待
if (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
结果:
#### 当只有 A B线程的时候
B=>0
A=>1
B=>0
A=>1
B=>0
#### 当线程增加 A B C D的时候
D=>1
D=>0
B=>-1
D=>-2
D=>-3
D=>-4
D=>-5
问题存在:A B C D,线程增加的时候。
2)存在问题(虚假唤醒)
问题,如果有四个线程,会出现虚假唤醒
因为我们这里是选择的一个 if 语句进行判断, 所以就会存在 虚假唤醒的问题,
解决方案:
解决方式 ,if 改为while即可,防止虚假唤醒
结论:
就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if。
代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后。
这是因为 if 判断只会去进行一次,然后就去运行了:
修改后的:
package com.AL.PC;
/**
* 线程之间的通信问题: 生产者和消费者问题。 等待唤醒,通知唤醒
* 线程交替执行: A B 去操作同一个资源 num
* A num++
* B num--
* C num++
* D num--
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
// 业务: 资源类
class Data {
private int num = 0;
// +1
public synchronized void increment() throws InterruptedException {
// 判断等待
while (num != 0) {
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 判断等待
while (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
3.4.2、Lock版本的生产者和消费者问题
Lock版本的生产者消费者问题:即 JUC 版本的:
我们在查看 synchronized 完成锁时的 wait 和 object类中 wait方法。那么对应的在锁 lock 类中也会有类似于 wait 方法 这种功能的实现:那么会有 Condition接口:
-
Condition因素出Object监视器方法( wait , notify和[notifyAll)到不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果 Lock个 实现。 如果Lock替换了synchronized方法和语句的使用,则Condition将替换Object监视方法的使用。
条件(也称为条件队列或条件变量 )为一个线程提供暂停执行(“等待”)的手段,直到另一个线程通知某个状态条件现在可能为真。 由于对此共享状态信息的访问发生在不同的线程中,因此必须对其进行保护,因此某种形式的锁定与该条件相关联。 等待条件提供的关键属性是它以原子方式释放关联的锁并挂起当前线程,就像Object.wait一样。
-
Condition实例本质上绑定到锁。 为了获得Condition实例特定Lock实例使用newCondition()方法。 如下所示:
我们新建一个 B.java去进行完成 lock 锁的生产者和消费者 多线程:
对于 lock:采用lock类中常见的 ReentrantLock() 进行加锁 和释放锁。
-
进行生产者消费者的问题的时候: 6字口诀: 等待,业务,通知。
-
解决多线程并发的步骤依旧是: lock.lock(加锁)、等待、业务代码、 唤醒、最后进行释放 即 lock.unlock。
采用 Condition接口中的方法:
condition.await(); //等待 condition.signalAll(); //唤醒全部
package com.AL.PC;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Data2 {
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// +1
public void increment() throws InterruptedException {
lock.lock();
try {
// 判断等待
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
condition.signalAll();
}finally {
lock.unlock();
}
}
// -1
public void decrement() throws InterruptedException {
lock.lock();
try {
// 判断等待
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
condition.signalAll();
}finally {
lock.unlock();
}
}
}
结果: 没有像 A -> B ->C ->D这样我们想要的进行下去。
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
D=>0
问题是: 随机的状态。 我们想让其有序执行。
Condition实现精准通知唤醒。
3.4.3、Condition的优势
精准的通知和唤醒的线程!
如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程~
例子:新建一个 C.java :
我们可以在唤醒的时候, 指定唤醒的目标就行了,即通过监视器 Condition去指定唤醒进程。
package com.AL.PC;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// A 执行完调用B, B执行完调用C,C执行完调用A
public class C {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printA();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printB();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printC();
}
},"C").start();
}
}
class Data3 {
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
private int num = 1; // 1A 2B 3C
// condition.await() 等待
// condition.signalAll() 唤醒全部
// 我们可以再唤醒的时候, 指定唤醒的目标就行了,即通过监视器 Condition去指定唤醒进程。
public void printA() {
lock.lock();
try {
// 业务代码 判断 -> 执行 -> 通知
while (num != 1) {
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "==> AAAA" );
// 唤醒,唤醒指定的人 B
num = 2;
condition2.signal();
}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();
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
// 业务代码 判断 -> 执行 -> 通知
while (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "==> CCCC" );
num = 1;
condition1.signal();
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
结果:
A==> AAAA
B==> BBBB
C==> CCCC
A==> AAAA
B==> BBBB
C==> CCCC
3.5、synchronized 和 ReenTrantLock 的区别
这两者都是为了锁住资源,多线程并发的安全性。保证数据的同步。
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量。且 ReentrantLock 类是实现了java.util.concurrent.locks 包中的Lock接口。
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
ReenTrantLock扩展的地方主要来说主要有三点:①等待可中断;==②可实现公平锁;==③可实现选择性通知(锁可以绑定多个条件);④ 性能已不是选择标准。
-
ReenTrantLock 提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly( )来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
-
ReenTrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock 默认情况是非公平的,可以通过 ReenTrantLock 类的 ReentrantLock(boolean fair) 构造方法来制定是否是公平的。【公平锁:不能插队】
-
synchronized 关键字与 wait() 和 notify/notifyAll() 方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
4、8锁现象
那么什么是锁? 锁住的又是谁呢?
如何判断锁的是谁。 去知道什么是锁,锁到底锁的是谁?
肯定是两个中的一个 对象、class, 一个是new出来的,一个是类。
我们拿synchronized 锁举例,它所修饰的锁的区域:
synchronized 关键字最主要的三种使用方式:
1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method() { //业务代码}
2.修饰静态方法: 作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁;
synchronized void staic method() {
//业务代码
}
3.修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给Class 类上锁。
synchronized(this) {
//业务代码
}
所以:
- 你锁住的是静态方法,即 static。它是你一个类创建的所有实例对象中都会被锁住。
- 修饰实例方法,作用的是这个类创建的 某个具体实例对象 才会有的一把锁。
- 锁住的就是两种: 一种是具体实例对象[new 出来的];一种是类,它的所有具体对象都有这把锁。
锁会锁住:对象、Class
问题1:一个对象,两个同步方法
创建lock8文件: 8锁就是关于锁的8个问题。
问题1:标准情况下,两个线程是先打印 发短信 还是 打电话
package com.AL.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8 锁,关于锁的8个问题
* 1、标准情况下,两个线程是先打印 发短信 还是 打电话
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> { phone.sendMs(); }).start();
// 捕获异常
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
public synchronized void sendMs() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果:不管运行多少次,结果都是 先发啊短信,再打电话。
发短信
打电话
我们给发短信这个锁增加延时 4 s,再次观察结果:
package com.AL.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8 锁,关于锁的8个问题
* 1、标准情况下,两个线程是先打印 发短信 还是 打电话
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> { phone.sendMs(); }).start();
// 捕获异常
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
// synchronized 锁住的对象是方法的调用!
// 对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待
public synchronized void sendMs() {
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果仍然是 先发短信,再打电话。 而且是等了4s延迟的发短信。
原先: 之所以先发短信 后打电话,不是什么顺序执行。
这是因为在这里,锁的对象就一个 即 phone,调用这两个方法的锁是同一个对象,所以这个对象先拿到了哪一把锁,就去先执行哪一个。
-
synchronized 锁住的对象是方法的调用! 对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待。
-
此时的方法可以被称为是 同步方法,synchronized 被锁住的方法。
问题2:一个对象,普通方法和同步方法
package com.AL.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、增加同步方法后,先执行发短信还是 Hello? 普通方法
*/
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone = new Phone2();
new Thread(() -> {
try {
phone.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {phone.hello();}).start();
}
}
class Phone2 {
// synchronized 锁住的对象是方法的调用!
// 对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待
public synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
输出结果为:
hello
发短信
原因:hello是一个普通方法,不受synchronized锁的影响,不用等待锁的释放.
问题4:两个对象,两个同步方法
如果我们使用的是两个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?
package com.AL.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、增加同步方法后,先执行发短信还是 Hello? 普通方法
* 4、 两个对象,两个同步方法,发短信还是 先打电话
*/
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(() -> {
try {
phone1.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone2.call(); }).start();
}
}
class Phone2 {
// synchronized 锁住的对象是方法的调用!
// 对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待
public synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
结果:先打电话 后发短信。
在这里两个对象两把锁[phone1、phone2],不会出现等待的情况,发短信睡了4s,所以先执行打电话
问题5:一个对象,两个静态同步方法
如果我们把synchronized的方法加上static变成静态方法!那么顺序又是怎么样的呢?
(1)我们先来使用一个对象调用两个方法!
package com.AL.lock8;
import java.util.concurrent.TimeUnit;
// 增加两个静态同步方法,是先打电话 还是先发短信?
public class Test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
// Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendMs();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> { phone.call(); }).start();
}
}
class Phone3 {
// synchronized 锁住的对象是方法的调用!
// static 修饰方法,静态方法。类一加载的时候就有了。 Class模板
public static synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
结果: 先发短信,后打电话。
原因:
对于静态方法 static,类一加载的时候就会有,锁住的是Class。即phone3.getClass() 所以仍然是谁先拿到锁,谁就先执行。
(2)如果我们使用两个对象调用两个方法!
问题6:两个对象,两个静态同步方法
package com.AL.lock8;import java.util.concurrent.TimeUnit;// 增加两个静态同步方法,是先打电话 还是先发短信?// 两个对象,两个静态同步方法public class Test3 { public static void main(String[] args) { Phone3 phone1 = new Phone3(); Phone3 phone2 = new Phone3(); new Thread(() -> { try { phone1.sendMs(); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> { phone2.call(); }).start(); }}class Phone3 { // synchronized 锁住的对象是方法的调用! // static 修饰方法,静态方法。类一加载的时候就有了。 Class模板 public static synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public static synchronized void call() { System.out.println("打电话"); }}
结果:还是先发短信,后打电话
原因是什么呢? 为什么加了static就始终前面一个对象先执行呢!为什么后面会等待呢?
原因是:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!
这个就是 锁 锁住的是 Class 类。 不管这个创建了多少个这个类的具体 实例化对象, 所有的对象都会拿着同一把 锁, 类锁。 所以就是谁先拿谁先执行。
问题7:一个对象,静态同步方法和同步方法
如果我们使用一个静态同步方法、一个同步方法、一个对象调用顺序是什么?
package com.AL.lock8;
import java.util.concurrent.TimeUnit;
public class Test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
new Thread(() -> {
try {
phone.sendMs();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> { phone.call(); }).start();
}
}
class Phone4 {
// synchronized 锁住的对象是方法的调用!
// static 修饰方法,静态方法。类一加载的时候就有了。 Class模板
public static synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
// 普通的同步方法,锁住的是方法的调用者,即对象
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
打电话
发短信
-
普通的同步方法,锁住的是锁的调用者,
-
static静态同步方法锁住的是Class 类模板,
这两个锁不一样。所以普通的同步方法 它不用去等待 Class这个锁释放 就可以直接去运行。
问题8:两个对象,静态同步方法和同步方法
package com.AL.lock8;import java.util.concurrent.TimeUnit;public class Test4 { public static void main(String[] args) { Phone4 phone1 = new Phone4(); Phone4 phone2 = new Phone4(); new Thread(() -> { try { phone1.sendMs(); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> { phone2.call(); }).start(); }}class Phone4 { // synchronized 锁住的对象是方法的调用! // static 修饰方法,静态方法。类一加载的时候就有了。 Class模板 public static synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } // 普通的同步方法,锁住的是方法的调用者,即对象 public synchronized void call() { System.out.println("打电话"); }}
结果:
打电话发短信
此时的两把锁,锁住的都不是一个东西。一个static锁是 类,所有的对象都拿着同一把。 而普通的同步方法是这个具体的实例化对象 方法的调用者 才有的一把锁。
小结
new 出来的 this 是具体的一个对象
static Class 是唯一的一个模板。
5、集合不安全
5.1、List不安全
ArrayList在单线程下是安全的, 但是在多线程的时候,就不安全了。
ArrayList 类是一个可以动态修改的数组队列,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。
ArrayList<String> sites = new ArrayList<String>(); // 初始化一个数组队列,且存放的内容为 字符串型
sites.add("Weibo"); // 添加元素。 [Weibo]
sites.get(0); // 访问第一个元素
sites.set(0, "Wiki"); // 修改元素。第一个参数为索引位置,第二个为要修改的值
sites.remove(0); // 删除第一个元素
sites.size(); // 计算ArrayList 中的元素数量
Collections.sort(sites); // ArrayList 字母排序,也可为数字排序
// for 循环迭代元素
for (int i = 0; i < sites.size(); i++) {
System.out.println(sites.get(i));
}
// for-each 迭代元素
for (String i : sites) {
System.out.println(i);
}
indexOf(); //返回 arraylist 中元素的索引值
toArray(); //将 arraylist 转换为数组
toString(); //将 arraylist 转换为字符串
ArrayList 在并发情况下出现 java.util.ConcurrentModificationException 并发修改异常:ArrayList 中的方法并没有被 synchronized关键字修饰,方法中也没 lock 锁。
package com.AL.unsafe;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
// 出现 java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
public static void main(String[] args) {
List<Object> arrayList = new ArrayList<>();
for(int i=1;i<=10;i++){
new Thread(()->{
arrayList.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(arrayList);
},String.valueOf(i)).start();
}
}
}
解决方案:
1.List list = new Vector<>(); // 使用 Vector时 线程安全。Vector也是一个动态数组,不过它不属于集合框架。 字符串类型 String 是不可修改的,被 final修饰,是常量类,线程安全
2.List list = Collections.synchronizedList(new ArrayList<>()); // 加锁, synchronized
3.List list = new CopyOnWriteArrayList<>(); // JUC 中的 读 写分离
package com.AL.unsafe;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
// 出现 java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
public static void main(String[] args) {
/**
List<Object> arrayList = new ArrayList<>();
// 解决方案
1.List<String> list = new Vector<>(); // 使用 Vector时 线程安全。 字符串类型 String 是不可修改的,被 final修饰,是常量类,线程安全的优点
2.List<String> list = Collections.synchronizedList(new ArrayList<>()); // 加锁, synchronized
3.List<String> list = new CopyOnWriteArrayList<>();
List<Object> arrayList = new ArrayList<>();
for(int i=1;i<=10;i++){
new Thread(()->{
arrayList.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(arrayList);
},String.valueOf(i)).start();
}
*/
// CopyOnWrite 写入时复制 COW是计算机程序设计领域中的一种优先策略
// 多个线程调用的时候, 对 List 进行读取的时候 固定的, 在写入时会进行覆盖
// 为了在写入的时候避免覆盖掉,造成数据问题。 那么采用 读写分离
List<String> list = new CopyOnWriteArrayList<>();
System.out.println("#############");
for (int i = 1; i <=10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
解决的原因;
CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略.
核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针 指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建, 因此多个调用者只是 读取操作时可以共享同一份资源。
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;
CopyOnWriteArrayList比Vector厉害在哪里?
Vector底层是使用synchronized关键字来实现的:效率特别低下。里面的方法都用 synchronized 修饰了 锁住了。
CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!
5.2、Set不安全
在集合里面存在的:关于 Collection中的三个接口 , 实现下面这三个的接口,接口实现类即为集合类。
Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;都没有 锁。
解决方案:
- 使用Collections工具类的synchronized包装的Set类, 如Collections.synchronizedSet(new HashSet<>());
- 使用 JUC 中的写入时复制 CopyOnWriteArraySet。 读的时候不用加锁,就算读的过程中 多个线程改变了数据,读到的数据还是原来的旧的数据。 新数据会在副本 上进行创建并修改完成更新, 没有新数据 就不会创建副本。
package com.AL.unsafe;import java.util.HashSet;import java.util.Set;import java.util.UUID;import java.util.concurrent.CopyOnWriteArraySet;public class SetTest { public static void main(String[] args) { /** * 1. Set<String> set = Collections.synchronizedSet(new HashSet<>()); //Collections工具类的synchronized包装的Set类 * 2. Set<String> set = new CopyOnWriteArraySet<>(); // JUC中的读写分离, 写入时复制 */ //Set<String> set = new HashSet<>(); Set<String> set = new CopyOnWriteArraySet<>(); for (int i = 1; i <= 30; i++) { new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(set); },String.valueOf(i)).start(); } }}
HashSet底层是什么? hashSet底层就是一个HashMap;它的源码:
// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); // final 不变的值 /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); }// set add 的本质中,进行map.put(e, PRESENT)==null,即 map key 是无法重复的 public boolean add(E e) { return map.put(e, PRESENT)==null; }
5.3、Map不安全
//map 是这样用的吗? 不是,工作中不使用这个//默认等价什么? new HashMap<>(16,0.75);Map<String, String> map = new HashMap<>();//加载因子、初始化容量
默认加载因子是0.75,默认的初始容量是16
同样的HashMap基础类也存在并发修改异常!java.util.ConcurrentModificationException 异常
下面的代码测试例子: 就出现了并发修改的异常问题: 并发肯定会存在的, 你不可能只有你在调用资源。一定会是多个线程去调用同一个资源。
public class MapTest { public static void main(String[] args) { //map 是这样用的吗? 不是,工作中不使用这个 //默认等价什么? new HashMap<>(16,0.75); Map<String, String> map = new HashMap<>(); //加载因子、初始化容量 for (int i = 1; i < 100; i++) { new Thread(()->{ map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5)); System.out.println(map); },String.valueOf(i)).start(); } }}
解决方案:两种
- 使用Collections工具类的synchronized包装的Set类, 如Collections.synchronizedMap(new HashMap<>());
- 使用 JUC 中的写入时复制 new ConcurrentHashMap<>();
package com.AL.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MapTest {
public static void main(String[] args) {
//map 是这样用的吗? 不是,工作中不使用这个
//默认等价什么? new HashMap<>(16,0.75);
// Map<String, String> map = new HashMap<>();
/**
* 解决方案
* 1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
* Map<String, String> map = new ConcurrentHashMap<>();
*/
Map<String, String> map = new ConcurrentHashMap<>();
//加载因子、初始化容量
for (int i = 1; i < 100; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
6、Callable
public interface Callable
返回结果并可能抛出异常的任务。 实现者定义一个没有参数的单个方法,名为call 。
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类而设计的。 但是, Runnable不会返回结果,也不能抛出已检查的异常。 【Callable 来自于 java.util.concurrent 包中的接口, 而Runnable是来自于java.lang 包的一个接口】
该Executors类包含的实用方法,从其他普通形式转换为Callable类。
Callable接口实现类 创建线程:
1、可以有返回值;
2、可以抛出异常;
3、方法不同,run()/call()
代码测试:
传统的实现 Runnable接口的并发编程例子: 没有返回值。
package com.AL.callable;
public class CallableTest {
public static void main(String[] args) {
new Thread(new MyThread()).start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
}
}
对于Callable接口的**参数 泛型**, 并有返回值:源码:
- Callable . 参数泛型
- V call()
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
我们原来实现 Ruanable接口创建的线程[重写 run 方法后],直接将其 丢入到 Thread中进行 start()调用即可。如:
new Thread(new MyThread()).start();
注意:
Thread()里面只能接收 Runnable接口的参数, 不能接收 Callable接口, 所以我们通过一个介质去进行传递:
成功创建了一个Callable接口的线程代码:
- MyThread1 thread1 = new MyThread1(); // 创建的 Callable接口实现类
- FutureTask futureTask = new FutureTask<>(thread1); //适配类,中间介质
- new Thread(futureTask, “A”).start(); // 通过中间介质去运行
package com.AL.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 {
new Thread(new MyThread()).start();
// 如何启动 Callable
new Thread().start();
MyThread1 thread1 = new MyThread1();
FutureTask<String> futureTask = new FutureTask<>(thread1); //适配类
new Thread(futureTask, "A").start();
String res = futureTask.get(); // 获取 Callable的返回结果
System.out.println(res);
}
}
class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread1 implements Callable<String> { // 此时定义参数泛型为 String,返回值自然也是 String
@Override
public String call() throws Exception {
return "SB";
}
}
对于Callable接口中的多线程:
package com.AL.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 {
/**
new Thread(new MyThread()).start();
// 如何启动 Callable
new Thread().start();
MyThread1 thread1 = new MyThread1();
FutureTask<String> futureTask = new FutureTask<>(thread1); //适配类
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();
String res = futureTask.get(); // 获取 Callable的返回结果
System.out.println("返回值:" + res);
*/
for (int i = 1; i < 10; i++) {
new Thread(new MyThread()).start();
// 如何启动 Callable
new Thread().start();
MyThread1 thread1 = new MyThread1();
FutureTask<String> futureTask = new FutureTask<>(thread1); //适配类
// 放入Thread中使用,结果会被缓存
new Thread(futureTask,String.valueOf(i)).start();
// 这个get方法可能会被阻塞,如果在call方法中是一个耗时的方法,所以一般情况我们会把这个放在最后,
// 或者使用异步通信
String res = futureTask.get();
System.out.println("返回值:" + res);
}
}
}
class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread1 implements Callable<String> {
@Override
public String call() throws Exception {
return "SB";
}
}
细节的地方:
- 有缓存
- 结果可能需要等待,会阻塞。
7、常用的辅助类
7.1、CountDownLatch
public class CountDownLatch
extends Object
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
使用给定计数初始化CountDownLatch
。 所述await
种方法阻塞,直到当前计数达到零由于的调用countDown()
方法,之后所有等待的线程被释放和任何后续调用await
立即返回。 这是一次性现象 - 计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier
。
CountDownLatch的作用就是个倒计数器 就是一定会等到所有的线程执行完毕,然后才会关门,继续下一个执行进程或线程命令。
- CountDownLatch.countDown(); // 数量 - 1
- CountDownLatch.await(); // 等待计数器归零,然后向下去执行
这个CountDownLatch在初始化的时候 会设置线程数量, 每次有线程调用 countDown() 就会 进行 -1 操作,直到计数器为 0 的时候,就会去唤醒CountDownLatch.await(),继续执行。
例子: 假设需要去处理6个用户或者其它的 6 个任务时,然后才能执行其它任务。 我们可以在初始化 CountDownLatch 的时候就去定义/设置 线程的数量, 每次有线程去调用的时候,就 count-1, 直到 conut=0的时候,去调用 await()方法时,去执行后面的逻辑。
package com.AL.add;
import java.util.concurrent.CountDownLatch;
// 倒计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6.必须要执行任务的时候 再使用
CountDownLatch countDownLatch = new CountDownLatch(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");
}
}
注意: 里面执行的线程的顺序我不管, 只是 必须在所有的指定任务的线程执行完毕后 才能去关闭。
优点和缺点:参考链接:微信公众号:码农求职小助手
根据 CountDownLatch 这个倒计数器的性质,我们可以有这种类似的应用场景:
- 在某一个线程开始运行的时候,前面必须要完成 n 个线程。 那么CountDownLatch 倒计时器,在每一个线程执行完时计数器 - 1. 只有当计数器为 0 的时候 await()方法才能去唤醒某个线程
- 实现多个线程开始执行任务的 最大并行性。我们可以设置一个 共享线程CountDownLatch 对象,将其计数器初始化为 1 ,在主线程进行调用的时候,计数器为 0 的时候, 去直接同时唤醒这 多个线程; 强调的是 在某一个时刻,让多个线程同时开始执行。
不足的地方在于: CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
7.2、CyclicBarrier
有减一的操作,那么就有加一的操作。
对于减一的操作,我们可以在 一个文件的资源必须要读取 6 次之后,才能去进行其它线程。 这时候你就可以用到 CountDownLatch 倒计数器, 只有当指定任务的线程执行完毕之后,才能去关闭。
关于 CyclickBarrier的官方文档手册介绍:
-
public class CyclicBarrier extends Object
一种同步辅助工具,允许一组线程全部等待彼此到达公共障碍点。CyclicBarriers在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。屏障称为*循环,*因为它可以在释放等待线程后重新使用。
CyclicBarrier
支持可选的Runnable
命令,该命令在每个障碍点运行一次,在聚会中的最后一个线程到达之后,但在释放任何线程之前。 在任何一方继续之前,此屏障操作对于更新共享状态非常有用。
**示例用法:**以下是在并行分解设计中使用屏障的示例:
class Solver {
final int N;
final float[][] data;
final CyclicBarrier barrier;
class Worker implements Runnable {
int myRow;
Worker(int row) {
myRow = row;
}
public void run() {
while (!done()) {
processRow(myRow);
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
}
public Solver(float[][] matrix) {
data = matrix;
N = matrix.length;
Runnable barrierAction = () -> mergeRows(...);
barrier = new CyclicBarrier(N, barrierAction);
List<Thread> threads = new ArrayList<>(N);
for (int i = 0; i < N; i++) {
Thread thread = new Thread(new Worker(i));
threads.add(thread);
thread.start();
}
// wait until done for (Thread thread : threads) thread.join(); } }
这里,每个工作线程处理一行矩阵,然后在屏障处等待,直到所有行都被处理完毕。处理完所有行后,将执行提供的Runnable
屏障操作并合并行。如果合并确定已找到解决方案,则done()
将返回true
并且每个工作人员将止。
例子:
CyclickBarrier 其实就是加法计数器。
-
在我们的例子中设置达到 8 个的时候才能召唤神龙
-
如果你的线程永远达不到 8 个。此时就会一直等待,不会结束。
package com.AL.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 主线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
System.out.println("召唤神龙");
});
//for (int i = 1; i <= 6; i++) { //在这种情况下,线程会一直执行,召唤神龙失败
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();
}
}
}
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
CountDownLatch 和 CyclicBarrier两者的区别:
CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. (CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;)
CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)
-
对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是 多个线程,在任意一个线程没有完成,所有的线程都必须等待。
-
CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递 减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续 执行。
7.3、Semaphore
Semaphore:信号量
-
public class Semaphore extends Object implements Serializable
计数信号量。**从概念上讲,信号量保持一组许可。如果有必要,每个acquire()
都会阻止,直到有许可证,然后接受。每个release()
都添加了许可证,可能会释放阻止收购者。**但是,没有使用实际的许可对象;Semaphore
只保留可用数量并相应地采取行动。
信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
例子:
public static void main(String[] args) {
// 线程数量,停车位,限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
// acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
}catch (Exception e) {
e.printStackTrace();
}finally {
semaphore.release(); // release() 释放
}
}).start();
}
}
结果:
Thread-0抢到车位
Thread-2抢到车位
Thread-1抢到车位
Thread-0离开车位
Thread-1离开车位
Thread-2离开车位
Thread-3抢到车位
Thread-5抢到车位
Thread-4抢到车位
Thread-4离开车位
Thread-5离开车位
Thread-3离开车位
Thread-6抢到车位
Thread-6离开车位
原理:
- **semaphore.acquire()获得资源,**如果资源已经使用完了,就等待资源释放后再进行使用
- semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!
- 作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
总结:
Semaphore **(信号量)-**允许多个线程同时访问同一资源。synchronized 和 ReentrantLock 都是⼀次只允许⼀个线程访问某个资源, Semaphore (信号量)可以指定多个线程同时访问某个资源。
CountDownLatch (倒计时器): CountDownLatch 是⼀个同步⼯具类,⽤来协调多个线程之间的同步。这个⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结束,再开始执⾏。
CyclicBarrier (循环栅栏): CyclicBarrier 和 CountDownLatch ⾮常类似,它也可以实现线程间的技术等待,但是它的功能⽐ CountDownLatch 更加复杂和强⼤。主要应⽤场景和CountDownLatch 类似。 CyclicBarrier 的字⾯意思是可循环使⽤( Cyclic )的屏障( Barrier )。它要做的事情是,让⼀组线程到达⼀个屏障(也可以叫同步点)时被阻塞,直到最后⼀个线程到达屏障时,屏障才会开⻔,所有被屏障拦截的线程才会继续⼲活。
CyclicBarrier 默认的构造⽅法是 CyclicBarrier(int parties) ,其参数表示屏障拦截的线程数量,每个线程调⽤ await() ⽅法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
8、读写锁
Interface ReadWriteLock
官方文档的介绍:
-
所有已知实现类:
ReentrantReadWriteLock
public interface ReadWriteLock
ReadWriteLock
维护一对关联的locks
,一个用于只读操作,另一个用于写入。只要没有写入器, read lock可以由多个读取器线程同时保持。write lock是独家的。
所有ReadWriteLock
实现必须保证writeLock
操作的内存同步效果(如Lock
接口中所指定)也相对于关联的readLock
。 也就是说,成功获取读锁定的线程将看到在先前释放写锁定时所做的所有更新。
读写锁允许访问共享数据的并发性 高于 互斥锁允许的并发性。 它利用了这样一个事实:虽然一次只有一个线程(一个编写器线程)可以修改共享数据,但在许多情况下,任何数量的线程都可以同时读取数据(因此读取器线程)。 理论上,使用读写锁所允许的并发性的增加将导致相互使用互斥锁的性能提高。 实际上,这种并发性的增加只能在多处理器上完全实现,并且只有在共享数据的访问模式合适时才能实现。
ReadWriteLock 读写锁利用的性质: 读可以被多个线程进行操作,写的时候只能有一个线程去写。 这是因为 我们需要去保持数据的一致性。
例子测试:
1.自定义的缓存:
package com.AL.rw;
import java.util.HashMap;
import java.util.Map;
/** 自定义缓存
* 方法未加锁,导致写的时候被插队
*/
public class MyCache {
private volatile Map<String, String> map = new HashMap<>();
// 存 写数据
public void write(String key, String value) {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入ok");
}
// 取 读
public void read(String key) {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程写读取ok");
}
}
2.读写锁,没有添加 读写锁时候的 多线程,让其进行写数据 和 读数据 线程任务。
在这里,我们使用 Lambda表达式 去实现线程时:
注意:在lambda表达式中,无法获得变量, 所以这里会去让i赋给一个常量, 让lambda中的代码块获取常量:
int finalI = i;
new Thread(() -> {
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
package com.AL.rw;
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
int num = 6;
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
},String.valueOf(i)).start();
}
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
部分结果如下所示: 理想情况下,我们想要的结果是 1线程写入,1线程写入ok;再执行 2 线程写入 且写入ok;执行3线程写入… 这样去保证事务一致性。
但是会发现在 1写入未彻底完成的时候,3进行插入了。
1线程开始写入
3线程开始写入 # 插入了其他线程的写入,可能会导致数据不一致
4线程开始写入
2线程开始写入
2线程写入ok
4线程写入ok
1线程写入ok
3线程写入ok
解决方案:由于如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。
- 我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。
- 但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证。
对于加锁的读写缓存:
package com.AL.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/** 加锁的缓存
* 独占锁(写锁)一次只能被一个线程占用
* 共享锁(读锁)多个线程可以同时占有
* ReadWriteLock
* 读-读 可以共存
* 读-写 不能共存
* 写-写 不能共存
*/
public class MyCacheLock {
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(); // 释放读锁
}
}
}
测试:
package com.AL.rw;
public class ReadWriteLockDemo {
public static void main(String[] args) {
//MyCache myCache = new MyCache();
MyCacheLock myCache = new MyCacheLock();
int num = 6;
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
},String.valueOf(i)).start();
}
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
结果:
1线程开始写入
1线程写入ok
4线程开始写入
4线程写入ok
6线程开始写入
6线程写入ok
3线程开始写入
3线程写入ok
2线程开始写入
2线程写入ok
5线程开始写入
5线程写入ok
3线程开始读取
3线程写读取ok
1线程开始读取
1线程写读取ok
2线程开始读取
2线程写读取ok
4线程开始读取
4线程写读取ok
6线程开始读取
6线程写读取ok
5线程开始读取
5线程写读取ok
9、队列
队列接口是集合 Collection 中的一种。
Java中关于集合 Collection 的介绍:在java集合框架中,主要是两种类型的容器:
- 一种是 Collection 集合, 用来存储一个对象集合
- Collection接口中有三种类型: List、Set、Queue
- 另一种是 Map ,用来存储 键值对。
List用来存放的是有序、可重复的数据; set存放的是 无序、唯一的数据; Queue是队列。
集合框架要完成对基本集合 数组、链表、树、哈希表的实现,即 array、linked、tree、hash,所以这些接口实现类有 ArrayList、LinkedList、TreeSet、HashSet、TreeSet、TreeMap、HashMap。
对于 Collection集合中的接口 和 接口实现类,针对 队列 Queue有:
- BlockingQueue:阻塞队列
- AbstractQueue:非阻塞队列
- Deque:双端队列
Java中关于队列 Queue的说明:
软件包 java.util
Interface Queue
-
-
参数类型
E
- 此队列中保留的元素类型 -
All Superinterfaces:
Collection<E>
,Iterable<E>
-
All Known Subinterfaces:
BlockingDeque<E>
,BlockingQueue<E>
,Deque<E>
,TransferQueue<E>
-
所有已知实现类:
AbstractQueue
,ArrayBlockingQueue
,ArrayDeque
,ConcurrentLinkedDeque
,ConcurrentLinkedQueue
,DelayQueue
,LinkedBlockingDeque
,LinkedBlockingQueue
,LinkedList
,LinkedTransferQueue
,PriorityBlockingQueue
,PriorityQueue
,SynchronousQueue
-
9.1、阻塞队列BlockingQueue
Interface BlockingQueue
-
-
参数类型
E
- 此队列中保留的元素类型 -
All Superinterfaces:
Collection<E>
,Iterable<E>
,Queue<E>
-
All Known Subinterfaces:
BlockingDeque<E>
,TransferQueue<E>
-
所有已知实现类:
ArrayBlockingQueue
,DelayQueue
,LinkedBlockingDeque
,LinkedBlockingQueue
,LinkedTransferQueue
,PriorityBlockingQueue
,SynchronousQueue
-
对于阻塞队列:既然是队列,那么当然遵循 先进先出 FIFO 原则,
BlockingQueue 是 Collection的一个子类。 我们在何时使用 阻塞队列呢?
- 多线程并发处理,线程池的时候使用
BlockingQueue里面的接口 方法,添加、删除和移除队首元素这三种方法: 根据方式有四种类型 api:
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞 等待 | 超时 等待 |
---|---|---|---|---|
添加 | add | offer() | put() | offer( , , ) |
删除 | remove | poll() | take() | poll( , ) |
检测队首元素 | element | peek |
关于添加和移除 Queue队列, BlockingQueue阻塞队列的中的 会抛出异常的一组API:
- 使用 add()方法的时候会抛出异常
- 使用 remove()方法移除元素时,抛出异常
使用 offer() 和 poll() 方法不抛出异常,有问题会返回 false 布尔值或者 null:
package com.AL.bq;
import java.util.Collection;
import java.util.concurrent.ArrayBlockingQueue;
public class Test {
public static void main(String[] args) {
//test1();
test2();
}
//抛出异常
public static void test1(){
// 队列的大小
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// 此时会抛出异常:java.lang.IllegalStateException: Queue full
//System.out.println(blockingQueue.add("d"));
System.out.println("-----------------------");
// 因为是队列, FIFO 原则, 结果输出依次为 a, b, c
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// 下行代码会 抛出异常: java.util.NoSuchElementException
//System.out.println(blockingQueue.remove());
}
// 不抛出异常, 有问题会返回 false 布尔值或者 null
public static void test2(){
// 队列的大小
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.peek()); // 检查队首元素
System.out.println(blockingQueue.offer("d")); // false
System.out.println("-----------------------");
// 因为是队列, FIFO 原则, 结果输出依次为 a, b, c
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll()); // null
}
}
那么对于以上的异常情况:队列阻塞的情况来讲, 我们可以让它执行,产生异常(抛出或者返回布尔值); 也可以让它选择等待 即阻塞。 阻塞也会分为 一直阻塞 或者 等待超时的那种阻塞。
- 第三组API接口: 对于阻塞选择一直等待的, 添加和删除使用 put() take() 这两种方法。
- 第四组API接口: 对于阻塞选择等待超时, 添加和删除使用 offer() 方法 poll()方法, 这个offer方法对于第一组那种来讲,这个方法是进行 重载的方法: 方法重载。
package com.AL.bq;
import java.util.Collection;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) throws InterruptedException {
//test1();
//test2();
//test3();
test4();
}
/**
* 等待 一直阻塞
*/
public static void test3() throws InterruptedException {
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
//一直阻塞 不会返回
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//如果队列已经满了, 再进去一个元素 这种情况会一直等待这个队列 什么时候有了位置再进去,程序不会停止
//blockingQueue.put("d"); // 程序一直在运行,一直阻塞
System.out.println(blockingQueue.take());
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<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
System.out.println("开始等待");
blockingQueue.offer("d",2, TimeUnit.SECONDS); //超时时间2s 等待如果超过2s就结束等待
System.out.println("结束等待");
System.out.println("===========取值==================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println("开始等待");
blockingQueue.poll(2,TimeUnit.SECONDS); //超过两秒 我们就不要等待了
System.out.println("结束等待");
}
}
9.2、同步队列SynchronousQueue
同步队列:
- 同步队列 没有容量,也可以视为容量为1的队列;
- 进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;
put方法 和 take方法:
- SynchronizedQueue 和 其他的BlockingQueue 不一样 它不存储元素;
- put了一个元素,就必须从里面先take出来,否则不能再put进去值!
- 并且SynchronousQueue 的take是使用了lock锁保证线程安全的。
public class sq {
public static void main(String[] args) {
BlockingQueue<String> synchronousQueue = new java.util.concurrent.SynchronousQueue<>();
// 向queue中添加元素
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();
}
}
10、线程池
线程池:三大方式、七大参数、四种拒绝策略。
池化技术:
程序的运行,本质:占用系统的资源!我们需要去优化资源的使用 ===> 池化技术
线程池、JDBC的连接池、内存池、对象池 等等。。。。
资源的创建、销毁十分消耗资源
池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。 每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。
《Java 并发编程的艺术》提到使⽤线程池的好处:
-
降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
-
提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
-
提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。
线程复用、可以控制最大并发数、管理线程;
线程池:三大方法
三大方法:
- ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程
- ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
- ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
package com.AL.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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++) {
//通过线程池创建线程
threadPool2.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
结果:
################ 单个线程
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
######################################## 固定的线程池大小 每次都是 5 个
pool-2-thread-1 ok
pool-2-thread-5 ok
pool-2-thread-4 ok
pool-2-thread-4 ok
pool-2-thread-3 ok
pool-2-thread-2 ok
pool-2-thread-3 ok
############## 可伸缩的,但是不会超过100
pool-3-thread-35 ok
pool-3-thread-36 ok
pool-3-thread-31 ok
pool-3-thread-30 ok
pool-3-thread-42 ok
pool-3-thread-38 ok
pool-3-thread-39 ok
pool-3-thread-40 ok
线程的三大方法:用于去规定线程个数。单个、 指定大小,就这么多、 可伸缩的。
七大参数
源码分析: **三大方法的源码中本质都是 **ThreadPoolExecutor.
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 本质: 都是 ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大的线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
RejectedExecutionHandler handler //拒绝策略
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
阿里巴巴的Java操作手册中明确说明:
对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。
七大参数:
- corePoolSize, //核心线程池大小
- maximumPoolSize, //最大的线程池大小
- long keepAliveTime, //超时了没有人调用就会释放
- TimeUnit unit, //超时单位
- BlockingQueue workQueue, //阻塞队列
- ThreadFactory threadFactory, //线程工厂 创建线程的。 一般不用动
- RejectedExecutionHandler handler //拒绝策略
例子: 银行办理业务窗口
对应的七大参数:
Core就是核心线程池,平常规定的。 而Max表示的是最大线程池。 对应的还有阻塞队列。 当max 最大线程池满了。且阻塞队列也满了的时候,就会产生一个 拒绝策略。 不让别的进来了。
超时: 指的是最大线程池那几个。当候客区阻塞队列没数据, 所有窗口都没数据了。在超过一定的时间,就关闭释放这几个线程池。
四种拒绝策略
- new ThreadPoolExecutor.AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常超出最大承载,就会抛出异常:队列容量大小 + maxPoolSize
- new ThreadPoolExecutor.CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 main线程进行处理
- new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了,丢掉异常,不会抛出异常。
- new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常
对于可伸缩的应⽤程序,建议使⽤ ThreadPoolExecutor.CallerRunsPolicy 。当最⼤池被填满时,此策略为我们提供可伸缩队列。
例子:银行处理业务时,人满了时的拒绝策略。 即 线程池满了的时候,对其它的线程的决绝策略:
package com.AL.pool;
import java.util.concurrent.*;
public class Demo02 {
public static void main(String[] args) {
ExecutorService threadPool= new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()); //银行满了,还有人进来,不处理这个人的,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy()); // 哪来的回哪去? 回到main线程
//线程池用完必须要关闭线程池
try { // 最大承载为: max + Deque
for (int i = 1; i <= 9 ; i++) {
//通过线程池创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
结果:
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-2 ok
// new ThreadPoolExecutor.AbortPolicy()); //银行满了,还有人进来,不处理这个人的,抛出异常
java.util.concurrent.RejectedExecutionException: Task com.AL.pool.Demo02$$Lambda$1/1831932724@7699a589 rejected from java.util.concurrent.ThreadPoolExecutor@58372a00[Running, pool size = 5, active threads = 3, queued tasks = 0, completed tasks = 5]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.AL.pool.Demo02.main(Demo02.java:20)
######### new ThreadPoolExecutor.CallerRunsPolicy()); // 哪来的回哪去? 回到main线程
pool-1-thread-1 ok
main ok
pool-1-thread-3 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-2 ok
如何设置线程池大小
那么池的大小该如何设置呢?
- IO 密集型 > 判断程序中十分消耗 IO 的线程。 然后指定最大的线程池数 为耗IO资源的那个线程
- CPU 密集型。 几核的CPU 就是几,可以保持CPU的效率最高
对于CPU 几核的确立:设备管理器中的处理器,有几个就是几核。
用代码去获取 CPU 的核:
Runtime.getRuntime().availableProcessors(); //获取核
System.out.println(Runtime.getRuntime().availableProcessors()); // 6
例子:
package com.AL.pool;
import java.util.concurrent.*;
public class Demo03 {
public static void main(String[] args) {
// 自定义线程池: 使用 ThreadPoolExecutor
/** 最大线程池的个数如何定义:
*1. **IO 密集型** > 判断程序中十分消耗 IO 的线程。 然后指定最大的线程池数 为耗IO资源的那个线程
* 2. **CPU 密集型**。 几核的CPU 就是几,可以保持CPU的效率最高
* 程序: 15个大型任务 IO十分占用资源
*/
System.out.println(Runtime.getRuntime().availableProcessors()); // 获取CPU的核数
ExecutorService threadPool= new ThreadPoolExecutor(
2,
//5,
Runtime.getRuntime().availableProcessors(),
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()); //银行满了,还有人进来,不处理这个人的,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy()); // 哪来的回哪去? 回到main线程
//线程池用完必须要关闭线程池
try { // 最大承载为: max + Deque
for (int i = 1; i <= 10 ; i++) {
//通过线程池创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
总结:选择最大线程池的 大小:
- CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小
- I/O密集型:在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
第三方开源软件的拒绝策略
参考链接:https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485679&idx=1&sn=57dbca8c9ad49e1f3968ecff04a4f735&chksm=cea24724f9d5ce3212292fac291234a760c99c0960b5430d714269efe33554730b5f71208582&token=1141994790&lang=zh_CN%23rd
dubbo中的线程拒绝策略
publicclass AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
protectedstaticfinal Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
privatefinal String threadName;
privatefinal URL url;
privatestaticvolatilelong lastPrintTime = 0;
privatestatic Semaphore guard = new Semaphore(1);
public AbortPolicyWithReport(String threadName, URL url) {
this.threadName = threadName;
this.url = url;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
dumpJStack();
thrownew RejectedExecutionException(msg);
}
private void dumpJStack() {
//省略实现
}
}
可以看到,当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因
- 输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在
- 输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。
- 继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性
Netty中的线程池拒绝策略
privatestaticfinalclass NewThreadRunsPolicy implements RejectedExecutionHandler {
NewThreadRunsPolicy() {
super();
}
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
final Thread t = new Thread(r, "Temporary task executor");
t.start();
} catch (Throwable e) {
thrownew RejectedExecutionException(
"Failed to start a new thread", e);
}
}
}
Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常
ActiveMq中的线程池拒绝策略
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
try {
executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
thrownew RejectedExecutionException("Interrupted waiting for BrokerService.worker");
}
thrownew RejectedExecutionException("Timed Out while attempting to enqueue Task.");
}
});
ActiveMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常
pinpoint中的线程池拒绝策略
publicclass RejectedExecutionHandlerChain implements RejectedExecutionHandler {
privatefinal RejectedExecutionHandler[] handlerChain;
public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) {
Objects.requireNonNull(chain, "handlerChain must not be null");
RejectedExecutionHandler[] handlerChain = chain.toArray(new RejectedExecutionHandler[0]);
returnnew RejectedExecutionHandlerChain(handlerChain);
}
private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) {
this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null");
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) {
rejectedExecutionHandler.rejectedExecution(r, executor);
}
}
}
pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍。
11、四大函数式接口
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
在java.util.function 中的四大函数式接口分别为:
- Consumer
- Function:函数式接口
- Predicate:断定型接口
- Supplier
11.1、Function 函数式接口
函数式接口:只有一个方法的接口。
Runnable接口就是一个函数式接口.
Function 的源码:
Function 函数型接口,有一个输入参数,有一个输出。
- 传入参数 T
- 返回类型 R
只要是 函数型接口, 就可以用 lambda表达式。
例子:
package com.AL.function;
import java.util.function.Function;
// Function 函数型接口
public class Demo01 {
public static void main(String[] args) {
// 工具类:输入输出的值
// Function function = new Function<String, String>() {
// @Override
// public String apply(String s) {
// return s;
// }
// };
// System.out.println(function.apply("asd"));
// lambda表达式
Function<String, String> function =(str) ->{return str;};
System.out.println(function.apply("科研使我快乐"));
}
}
11.2、Predicate 断定型接口
Predicate 断定型接口: 有一个输入参数, 返回值只能是 布尔值.
package com.AL.function;
import java.util.function.Predicate;
/**
Predicate 断定型接口: 有一个输入参数, 返回值只能是 布尔值
*/
public class Demo02 {
public static void main(String[] args) {
// 判断字符串 是否为空。 重写了此方法
// Predicate<String> predicate = new Predicate<String>() {
// @Override
// public boolean test(String s) {
// return s.isEmpty();
// }
// };
Predicate<String> predicate = (str) -> { return str.isEmpty();};
System.out.println(predicate.test("")); // true
System.out.println(predicate.test("heng")); // false
}
}
11.3、Supplier 供给型接口
Supplier 供给型接口: 没有参数,只有返回值
供给型接口:不要你输入东西,我就返回给你。 供给。
package com.AL.function;
import java.util.function.Supplier;
// Supplier 供给型接口, 没有参数, 只有返回值
public class Demo03 {
public static void main(String[] args) {
// Supplier supplier = new Supplier<Integer>() {
// @Override
// public Integer get() {
// return 1024;
// }
// };
Supplier supplier = () -> { return 1024;};
System.out.println(supplier.get());
}
}
11.4、Consummer 消费型接口
Consummer 消费型接口: 只有输入, 没有返回值
package com.AL.function;
import java.util.function.Consumer;
// Consummer 消费型接口: 只有输入, 没有返回值
public class Demo04 {
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String s) {
// System.out.println(s);
// }
// };
Consumer<String> consumer = (s) -> {
System.out.println(s);
};
consumer.accept("qwer");
}
}
12、Stream流式计算
Stream流式计算:
大数据:存储+计算
集合和MySQL的本质都是用来存储东西的。
计算应该交给 流 来操作。
JDK文档中关于 Stream的介绍:
模块 java.base
软件包 java.util.stream
Interface Stream
-
-
参数类型
T
- 流元素的类型 -
All Superinterfaces:
AutoCloseable
,BaseStream<T,Stream<T>>
public interface Stream<T> extends BaseStream<T,Stream<T>>
支持顺序和并行聚合操作的一系列元素。
-
例子:
创建一个 User类:
package com.AL.stream;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 有参,无参构造方法,get set toString方法
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private int age;
}
需要去完成的一项任务:
package com.AL.stream;
import java.util.Arrays;
import java.util.List;
/**
* 题目要求: 用一行代码实现
* 1. Id 必须是偶数
* 2.年龄必须大于23
* 3. 用户名转为大写
* 4. 用户名倒序
* 5. 只能输出一个用户
*/
public class Test {
public static void main(String[] args) {
User u1 = new User(1, "a", 28);
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);
// lambda、链式编程、函数式接口、流式计算
list.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);
}
}
13、ForkJoin
ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!
大数据中:MapReduce 核心思想->把大任务拆分为小任务! 类似于分治思想
ForkJoin 特点: 工作窃取!
实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!
先执行完毕的线程会把没执行完的线程的任务窃取过来进行执行,提高任务效率。
在这里,线程B的任务还没执行完的时候,线程A已经执行完毕了, 然后线程A就会去窃取线程B的任务去执行,提高任务执行效率。
2)如何使用ForkJoin?
查看帮助文档中的包: java.utils.concurrent, 里面的Executor和ExecutorService。
官方文档的解释:
模块 java.base
Class ForkJoinTask
-
- java.util.concurrent.ForkJoinTask
-
-
实现的所有接口
Serializable
,Future<V>
-
已知直接子类:
CountedCompleter
,RecursiveAction
,RecursiveTask
public abstract class ForkJoinTask<V> extends Object implements Future<V>, Serializable
-
其中的:
- RecursiveAction:递归事件,没有返回值
- RecursiveTask:递归任务,有返回值的
使用ForkJoin的步骤:
- 通过ForkJoinPool来执行
- 计算任务 execute(ForkJoinTask<?> task)
- 计算类要去继承ForkJoinTask;
例子:
ForkJoin的计算类:
package com.AL.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
求和计算的任务:
3000 6000(ForkJoin) 9000(Stream并行流)
如何使用 ForkJoin:
1. forkjoinPool 通过它来执行
2. 计算任务 forkjoinPool.execute(ForkJoinTask task)
3. 计算类要继承 ForkJoinTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private long star;
private long end;
//临界值
private long temp = 1000000L;
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 分而治之 计算
//1 . 计算平均值
long middle = (star + end) / 2;
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(star, middle);
// 拆分任务,把线程压入线程队列
forkJoinDemo1.fork();
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle, end);
forkJoinDemo2.fork();
long taskSum = forkJoinDemo1.join() + forkJoinDemo2.join();
return taskSum;
}
}
}
对其进行测试,查看效率:
package com.AL.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class Test {
private static final long SUM = 20_0000_0000;
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
/**
* 使用普通方法
*/
public static void test1() {
long star = System.currentTimeMillis();
long sum = 0L;
for (long i = 1; i < SUM ; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(sum);
System.out.println("时间:" + (end - star));
System.out.println("----------------------");
}
/**
* 使用ForkJoin 方法
*/
public static void test2() throws ExecutionException, InterruptedException {
long star = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, SUM);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long along = submit.get();
System.out.println(along);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("-----------");
}
/**
* 使用 Stream 流计算
*/
public static void test3() {
long star = System.currentTimeMillis();
long sum = LongStream.range(0L, 20_0000_0000L).parallel().reduce(0, Long::sum);
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("-----------");
}
}
在这里,Stream流式计算的速度最快。
LongStream.range(0L, 20_0000_0000L).parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。
14、异步回调
Future 设计的初衷:对将来的某个事件结果进行建模!
其实就是前端 --> 发送ajax异步请求给后端
保持数据能够完成异步通信完成吗? 不能客户端调用了数据库中的数据,修改的时候,让它一直进行等待把。所以就有了异步通信。 这个是自己关于整个web框架流程中涉及到的一部分的理解。 不是这一节的。
服务端和客户端之间就通过 ajax 完成异步调用的。
JDK官方文档中关于Future中的解释:
模块 java.base
Interface Future
-
-
参数类型
V
- 此Future的get
方法返回的结果类型 -
All Known Subinterfaces:
RunnableFuture<V>
,RunnableScheduledFuture<V>
,ScheduledFuture<V>
-
所有已知实现类:
CompletableFuture
,CountedCompleter
,ForkJoinTask
,FutureTask
,RecursiveAction
,RecursiveTask
,SwingWorker
-
我们一般使用的是 CompletableFuture
-
没有返回值的 runAsync异步回调
异步执行,成功回调,失败回调
package com.AL.future; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** *没有返回值的 **runAsync**异步回调 * 异步执行, * 成功回调, * 失败回调 */ public class Demo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 发起 一个 请求 System.out.println(System.currentTimeMillis()); System.out.println("---------------------"); CompletableFuture<Void> future = CompletableFuture.runAsync(()->{ //发起一个异步任务 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"....."); }); System.out.println(System.currentTimeMillis()); System.out.println("------------------------------"); //输出执行结果 System.out.println(future.get()); //获取执行结果 } }
-
有返回值的异步回调supplyAsync
ajax,成功和失败的回调
返回的是错误的信息
package com.AL.future; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * **有返回值的异步回调supplyAsync** * ajax,成功和失败的回调 * 返回的是错误的信息 */ public class Demo02 { public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<Integer> completableFuture=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(completableFuture.whenComplete((t, u) -> { //success 回调 System.out.println("t=>" + t); //正常的返回结果 System.out.println("u=>" + u); //抛出异常的 错误信息 }).exceptionally((e) -> { //error回调 System.out.println(e.getMessage()); return 404; }).get()); } }
运行结果为;
ForkJoinPool.commonPool-worker-1 t=>null u=>java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero 404
whenComplete: 有两个参数,一个是t 一个是u
T:是代表的 正常返回的结果;
U:是代表的 抛出异常的错误信息;
如果发生了异常,get可以获取到exceptionally返回的值;
15、JMM和volatile
15.1、什么是JMM
JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!
- 作用是 为了解决缓存不一致的问题。 缓存一致性协议,用于定义数据的读写规则。
- JMM抽象地定义了线程和主内存之间的关系:线程之间的共享变量存储在主内存 main memory,每个线程都有自己私有的本地内存。
在线程中,会分为 主内存、工作内存。不同的线程会对主内存中的共享资源进行调用或改变,我们需要保证线程和主内存中的数据同步,一致。所以我们会去 锁住资源,来保证线程安全,数据的一致。
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存;
2、线程加锁前,必须读取主存中的最新值到工作内存中;
3、加锁和解锁是同一把锁;
在操作工作内存变量和主内存变量时有 8 种操作:
- Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
- load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
- Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
- store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
在这其中,read和load是一组操作,不可分割的; Use和assign为一组;write和store为一组;lock和unlock为一组。
JMM对这8种操作给了相应的规定:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量对一个变量进行unlock操作之前,必须把此变量同步回主内存
但是多线程之间会存在着这样的问题:
- 线程B改变了主内存中的资源,线程A不能及时查看,就会发生线程任务和预期不一样的问题。
例子:
package com.AL.tvolatile;
import java.util.concurrent.TimeUnit;
public class JMMDemo01 {
// 如果不加volatile 程序会死循环
// 加了volatile是可以保证可见性的
//private volatile static Integer number = 0;
private static Integer number = 0;
public static void main(String[] args) {
//main线程
//子线程1
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//子线程2
new Thread(()->{
while (number==0){
number=1;
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(number);
}
}
代码中表示的线程运行如下图所示:
对于上述的线程B的操作改变了工作内存中的值,并重新store到工作内存中完成刷新;但是A却没收到刷新后的值。
遇到问题:线程A的程序不知道主存中的值已经被修改过了;导致线程 A 此时一直在运行。
我们如何去解决这种情况?使用 Volatile 关键字。
15.2、volatile
Volatile 是 Java 虚拟机提供 轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
简单介绍下 volatile?
volatile 关键字是用来保证有序性和可见性的。这跟 Java 内存模型有关。
比如:我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU 也会做重排序的,这样的重排序是为了减少流水线的阻塞的,引起流水阻塞,比如数据相关性,提高 CPU 的执行效率。需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知道对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量规则:对一个变量的 写操作 先行发生于后面对这个变量的 读操作;有序性实现的是通过插 入内存屏障来保证的。
可见性:首先 Java 内存模型分为主内存,工作内存。比如线程 A 从主内存把变量从主内存读到了自己的工作内存中,做了加 1 的操作,但是此时没有将 i 的最新值刷新会主内存中,线程 B 此时读到的还是 i 的旧值。加了 volatile 关键字的代码生成的汇编代码发现,会多出一个 lock 前缀指令。 Lock 指令对 Intel 平台的 CPU,早期是锁总线,这样代价太高了,提出了**缓存一致性协议,MESI,来保证了多核之间数据不一致性问题**。
1.保证可见性:
针对上述的线程 A 不知道主内存中变量的变化,使用 volatile关键字修饰即可:
// 如果不加volatile 程序会死循环
// 加了volatile是可以保证可见性的
private volatile static Integer number = 0;
//private static Integer number = 0;
2.不保证原子性:
回顾事务的原则 ACID:
事务原则:ACID原则:原子性,一致性,隔离性,持久性
-
原子性(Atomicity):要么都成功,要么都失败
-
一致性(Consistency):事务前后的数据完整性要保证一致
-
持久性(Durability)— 事务提交。事务一旦提交则不可逆,被持久化到数据库中!
-
隔离性(Isolation):事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被
其他事务的操作数据所干扰,事务之间相互隔离。
原子性:不可分割。
线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。
例子:volatile修饰的变量不保证原子性:
package com.AL.tvolatile;
/**
* volatile修饰的变量不保证原子性。
* 原子性:线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。
*/
public class JMMDemo02 {
private static volatile int number = 0;
public static void add(){
number++;
//++ 不是一个原子性操作,是2个~3个操作
}
public static void main(String[] args) {
//理论上number == 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ //main gc,java线程中默认的至少有两个,main线程和 GC垃圾回收线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
结果:
main,num=18266 // 结果不等于 2万
如果不加lock和synchronized ,怎么样保证原子性?
解决办法:使用 java.util.concurrent.atomic 原子类。
模块 java.base
Package java.util.concurrent.atomic
一个小型工具包,支持对单个变量进行无锁线程安全编程。原子类的实例维护使用其他可用于使用关联原子VarHandle
操作的字段的方法访问和更新的值。
类的实例AtomicBoolean
, AtomicInteger
, AtomicLong
,和AtomicReference
各自提供访问和更新相应的类型的单个变量。 每个类还为该类型提供适当的实用方法。 例如,类AtomicLong
和AtomicInteger
提供原子增量方法。
针对上述的无法保证原子性的 numbers++的问题,使用原子类解决:
我们修改为原子类的 int 类型: 原子类中的 getAndIncrement()方法 不是一个简单的加一操作,而是用的 CAS。CAS可以去完成原子性操作,CAS保证原子性。
package com.AL.tvolatile;
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile修饰的变量不保证原子性。
* 原子性:线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。
*/
public class JMMDemo02 {
// private static volatile int number = 0;
// 原子类的 Integer
private static volatile AtomicInteger number = new AtomicInteger();
public static void add(){
// number++; //++ 不是一个原子性操作,是2个~3个操作
number.getAndIncrement(); // AtomicTnteger + 1 的方法,利用 CAS操作。
}
public static void main(String[] args) {
//理论上number == 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ //main gc,java线程中默认的至少有两个,main线程和 GC垃圾回收线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
3.禁止指令重排:
什么是指令重排?
我们写的程序,计算机并不是按照我们自己写的那样去执行的
源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!
int x=1; //1
int y=2; //2
x=x+5; //3
y=x*x; //4
//我们期望的执行顺序是 1_2_3_4 可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的。 因为它会考虑数据之间的依赖性
1234567
可能在线程A中会出现,先执行b=1,然后再执行x=a;
在B线程中可能会出现,先执行a=2,然后执行y=b;
那么就有可能结果如下:指令重排的诡异结果 x=2; y=1.
volatile可以避免指令重排:
volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。
内存屏障:CPU指令。作用:
1、保证特定的操作的执行顺序;
2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)
总结:
- volatile保证可见性
- volatile不保证原子性。 CAS去保证原子性,atomic原子类的底层就使用了 CAS操作
- volatile通过增加内存屏障,可以保证 避免指令重排的现象产生
16、单例模式
饿汉式、DCL懒汉式
只要是单例模式,则构造器私有。
16.1、饿汉式
会直接将这几个创建的数组大小填满:
package com.AL.single;
/**
* 饿汉式单例
*/
public class Hungry {
/**
* 可能会浪费空间
*/
private byte[] data1=new byte[1024*1024];
private byte[] data2=new byte[1024*1024];
private byte[] data3=new byte[1024*1024];
private byte[] data4=new byte[1024*1024];
private Hungry(){
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
16.2、DCL懒汉式
懒汉式单例在 单线程下没有问题,但是在多线程并发下就出现问题了.
双重检测完成单例模式,且此时的变量必须由 volatile进行修饰。
package com.AL.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
// DCL 懒汉式单例
public class LazyMan {
private static boolean key = false;
private LazyMan(){
synchronized (LazyMan.class){
if (key==false){
key=true;
}
else{
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName()+" ok");
}
private volatile static LazyMan lazyMan;
//双重检测锁模式 简称DCL懒汉式
public static LazyMan getInstance(){
//需要加锁
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();
/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 就有可能出现指令重排问题
* 比如执行的顺序是1 3 2 等
* 我们就可以添加volatile保证指令重排问题
*/
}
}
}
return lazyMan;
}
//单线程下 是ok的
//但是如果是并发的
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
//Java中有反射
// LazyMan instance = LazyMan.getInstance();
Field key = LazyMan.class.getDeclaredField("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);
}
}
双重校验锁实现单例模式:
- 需要注意 lazyMan 采用 volatile 关键字修饰也是很有必要。【采用 volatile保证可见性、禁止指令重排】
- lazyMan=new LazyMan();这段代码其实是分为三步执行:
- 为 lazyMan 分配内存空间
- 初始化 lazyMan
- 将 lazyMan 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
16.3、静态内部类
单例不安全,因为反射。
package com.AL.single;
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static final Holder holder = new Holder();
}
}
16.4、枚举
package com.AL.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
// enum 本身也是一个 Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);//有参的
declaredConstructor.setAccessible(true);
//java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.<init>()
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
使用枚举,我们就可以防止反射破坏了。
利用 jad.exe 工具包进行反编译,去获得了下面的源码:
枚举类型的最终反编译源码:
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/ogj/single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i);
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
17、CAS
17.1、什么是CAS
CAS(Compare And Swap) 比较和替换。
CAS操作大概有如下几步: 【需要保证原子性,即要么都成功,要么都失败】
- 读取旧值为一个临时变量
- 对旧值的临时变量进行操作或者依赖旧值临时变量进行一些操作
- 判断旧值临时变量是不是等于旧值,等于则没被修改,那么新值写入.不等于则被修改,此时放弃或者从步骤1重试.
boolean compareAndSet(int expect, int update):
- 期望值、更新值
- 如果实际值 和 我的期望值相同,那么就更新; 如果实际值 和 我的期望值不同,那么就不更新
例子如下:
package com.AL.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
//CAS : compareAndSet 比较并交换
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//boolean compareAndSet(int expect, int update)
//期望值、更新值
//如果实际值 和 我的期望值相同,那么就更新
//如果实际值 和 我的期望值不同,那么就不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
//因为期望值是2020 实际值却变成了2021 所以会修改失败
//CAS 是CPU的并发原语
atomicInteger.getAndIncrement(); //++操作
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
结果:
true
2021 // 因为期望值 2020 和实际值 2020相等,所以就进行更新为 2021
false
2022
对于原子类中的 Unsafe类:
native表示本地方法,指去调用 c++ 底层代码
其实CAS算法就是一种自旋锁。
17.2、CAS的ABA问题
CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。
缺点:
- 循环会耗时;
- 一次性只能保证一个共享变量的原子性; 所以会把多个共享变量封装成一个类去解决。
- 它会存在ABA问题
CAS 的ABA问题:狸猫换太子:
如下图所示:线程A和线程B想要完成的任务是:
线程1:期望值是1,要变成2;
线程2:两个操作:
- 1、期望值是1,变成3
- 2、期望是3,变成1
所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;
对上面的线程改进,造出一个 ABA的现场:
package com.AL.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo02 {
//CAS : compareAndSet 比较并交换
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//boolean compareAndSet(int expect, int update)
//期望值、更新值
// 如果我期望的值达到了,那么就更新,否则,就不去更新。 CAS
// =================== 捣蛋的线程 =======================
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// =================== 期望的线程 =======================
System.out.println(atomicInteger.compareAndSet(2020, 8888));
System.out.println(atomicInteger.get());
}
}
结果:
true
2021
true
2020
true
8888
17.3、带版本号的原子操作
针对 CAS中出现的 ABA问题,解决方案是: 使用版本号机制,去查看它中间状态时变量到底更新了没有。
带版本号的 原子操作!
Integer 使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf会使用缓存,而new一定会创建新的对象分配新的内存空间。【这是 Integer的性质引起的。对于Integer来讲,使用 new创建,那么直接会在堆中分配内存,肯定不会相等。 常量池这种,会有缓存。】
〖 强 制 〗 所 有 的 相 同 类 型 的 包 装 类 对 象 之 间 值 的 比 较 , 全 部 使 用 equals 方 法 比 较 。 说 明 : 对 于 Integer var : ? 在 · 128 至 127 之 间 的 赋 值 , Integer 对 象 是 在 IntegerCache.cache产生,会复用已有对象,即重复使用已经存在的对象[缓存机制]。这 个 区 间 内 的 Integer 值 可 以 直 接 使 用 == 进 行 判 断 , 但 曰 这 个 区 间 之 外 的 所 有 数 据 , 都 会 在 堆 上 产 生 并 不 会 复 用 己 有 对 象 , 这 是 一 个 大 坑, 推荐 使 用 equals方 法 进 行 判 断 。
Integer的内存默认 -128 127。
例子: 这里的版本号原子操作,也是属于 原子类中的 atomic里面的:
package com.AL.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo03 {
/**
* AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
* 正常在业务操作,这里面比较的都是一个个对象
*/
static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1, 1);
// CAS compareAndSet : 比较并交换!
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);
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();
}
}
18、各种锁
18.1、公平锁,非公平锁
- 公平锁:非常公平。哪个线程先来先执行。FIFO原则。 不能够插队,必须先来后到。
- 非公平锁:非常不公平,可以插队。默认的都是非公平。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
// 非公平锁
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
18.2、可重入锁
可重入锁(递归锁):
“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
释放锁之前,A线程自己是可以重复获取此锁的(state 会累加),这就是==可重入==的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
synchronized 和 lock 就是可重入锁。
-
synchronized锁:
package com.AL.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{ public synchronized void sms(){ System.out.println(Thread.currentThread().getName()+"=> sms"); call();//这里也有一把锁 } public synchronized void call(){ System.out.println(Thread.currentThread().getName()+"=> call"); } }
结果:
A=> sms A=> call B=> sms B=> call
-
lock锁
package com.AL.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; //lock public class Demo02 { public static void main(String[] args) { Phone2 phone = new Phone2(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone2{ Lock lock=new ReentrantLock(); public void sms(){ lock.lock(); //细节:这个是两把锁,两个钥匙 //lock锁必须配对,否则就会死锁在里面 try { System.out.println(Thread.currentThread().getName()+"=> sms"); call();//这里也有一把锁 } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void call(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> call"); }catch (Exception e){ e.printStackTrace(); } finally { lock.unlock(); } } }
需要注意的是:
- lock锁必须配对,相当于lock和 unlock 必须数量相同;
- 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁
18.3、自旋锁
自旋锁:不断地去循环,遍历迭代,直到成功。
自旋锁: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;
}
自我设计自旋锁:
package com.AL.lock;
import java.util.concurrent.atomic.AtomicReference;
public class SpinlockDemo {
// 默认
// int 0
//thread null
AtomicReference<Thread> atomicReference=new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"===> mylock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){
System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
}
}
//解锁
public void myUnlock(){
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"===> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
进行测试:
package com.AL.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
//使用CAS实现自旋锁
SpinlockDemo spinlockDemo=new SpinlockDemo();
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t2").start();
}
}
结果:
==> 自旋中~
t2 ==> 自旋中~
t2 ==> 自旋中~
t2 ==> 自旋中~
t2 ==> 自旋中~
...
t2 ==> 自旋中~
t2 ==> 自旋中~
t1===> myUnlock
t2 ==> 自旋中~
t2===> myUnlock
t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。
当然,自旋锁你也不能让他一直自旋下去,线程阻塞。 可以进行设置自旋锁的事件,比如 适应性自旋锁就可以做到。