文章目录
1、什么是JUC
JUC就是java.util.concurrent下面的类包,专门用于
多线程的开发
。
2、进程和线程
进程
是操作系统中的应用程序、是资源分配的基本单位。
线程
是用来执行具体的任务和功能,是CPU调度和分派的最小单位一个进程往往可以包含多个线程,至少包含一个
Java默认有几个线程?2个线程!
main线程
、GC线程
1)深入理解进程和线程
做个简单的比喻:进程=火车,线程=车厢
线程在进程下行进(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢)
不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-
"互斥锁"
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-
“信号量”
2)扩展:java可以开启线程吗?
不能
对于Java而言:Thread、Runable、Callable进行开启线程的。
查看
Thread.start()
源码可得知:Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。
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();
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 */
}
}
}
// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();
3)并发与并行
并发:
同一时间段
,多个任务都在执行 (单位时间内不一定同时执行
);
- 并发编程的本质:充分利用CPU的资源!
并行:
单位时间内
,多个任务同时执行
。
获取cpu的核数
public class Test1 {
public static void main(String[] args) {
// 获取cpu的核数
// CPU 密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
//输出为8为八核处理器
}
}
4)线程的状态
查看
Thread.State
枚举类源码可得知:
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死死地等
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
5)wait和sleep
共同点:
- 两者都可以暂停线程的执行。
差异:
关于锁的释放:
wait() 方法释放了锁;
wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。
sleep() 方法没有释放锁;
(简单来说,就是抱着🔒睡觉)使用的范围:
wait() 需要在同步代码块中使用;否则或抛出
IllegalMonitorStateException
异常sleep 可以在任何地方睡;
作用对象:
wait()定义在
Object
类中,作用于对象本身sleep()定义在
Thread
类中,作用当前线程。方法属性:
wait()是实例方法
sleep()是静态方法 有
static
唤醒条件
其他线程调用对象的
notify()
或者notifyAll()
方法超时或者调用
interrupt()
方法体
一般情况企业中多使用:
import java.util.concurrent.TimeUnit;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
TimeUnit.DAYS.sleep(1); //休眠1天
TimeUnit.SECONDS.sleep(1); //休眠1s
}
}
3、Lock锁
1)开发中错误开启线程的方法
真正的多线程开发,公司中的开发,降低耦合性
线程就是一个单独的资源类,没有任何附属的操作!
对于资源类只有: 属性、方法
耦合度高,违背了oop(面向对象)思想
public class Test1 {
public static void main(String[] args){
new Thread(new MyThread()).start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("执行起来");
}
}
2)传统的 synchronized
public class Test1 {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类, 把资源类丢入线程
final Ticket ticket = new Ticket();
// @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
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 = 30;
//卖票的方式
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
}
}
A卖出了第30张票剩余29张票
A卖出了第29张票剩余28张票
...
A卖出了第17张票剩余16张票
A卖出了第16张票剩余15张票
B卖出了第15张票剩余14张票
B卖出了第14张票剩余13张票
...
B卖出了第3张票剩余2张票
B卖出了第2张票剩余1张票
C卖出了第1张票剩余0张票
3)Lock锁
- java.util.concurrent.locks 包下
- 接口
- 实现类
- ReentrantLock
可重入锁
- ReentrantReadWriteLock.ReadLock
可重入读锁
- ReentrantReadWriteLock.WriteLock
可重入写锁
- 常用上锁语句
Lock l = ...;
l.lock(); //加锁
try {
// access the resource protected by this lock
}
finally {
l.unlock(); //解锁
}
ReentrantLock 可重入锁
new
ReentrantLock()
源码发现
发现:默认是非公平锁
公平锁与非公平锁
公平锁: 非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认)
// Lock 三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加锁
// 3、 finally=> lock.unlock(); // 解锁
public class Test2 {
public static void main(String[] args) {
Ticket2 ticket = new Ticket2();
//lambda表达式终极写法
//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();
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 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 + "张票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
A卖出了第30张票剩余29张票
A卖出了第29张票剩余28张票
A卖出了第28张票剩余27张票
A卖出了第27张票剩余26张票
A卖出了第26张票剩余25张票
B卖出了第25张票剩余24张票
B卖出了第24张票剩余23张票
...
C卖出了第3张票剩余2张票
C卖出了第2张票剩余1张票
C卖出了第1张票剩余0张票
4)Synchronized 和 Lock
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入不可中断非公平 | 可重入可判断可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
4、生产者和消费者
案例:对一个数字不停进行 +1 -1操作,加完了减,减完了加
1)synchronized版本
public class Test3 {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data{
//属性
private int number = 0;
//+1方法
public synchronized void increment() throws InterruptedException {
if (number != 0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"-->"+number);
//加完了通知其他线程
this.notifyAll();
}
//-1方法
public synchronized void decrement() throws InterruptedException {
if (number == 0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"-->"+number);
//减完了通知其他线程
this.notifyAll();
}
}
此时输出:
A-->1
B-->0
A-->1
B-->0
A-->1
B-->0
A-->1
B-->0
A-->1
B-->0
交替执行
2)存在问题(虚假唤醒)
如果有四个线程
public class Test3 {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data{
//属性
private int number = 0;
//+1方法
public synchronized void increment() throws InterruptedException {
if (number != 0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"-->"+number);
//加完了通知其他线程
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (number == 0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"-->"+number);
//减完了通知其他线程
this.notifyAll();
}
}
输出
原因如下:
用if判断只执行了一次判断,而wait()方法会导致🔒的释放
具体说明:以两个加法线程A、C举例:
比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁。
那么线程C 获得锁并且也会执行wait()方法,且释放锁,同时两个加线程一起进入等待状态,等待被唤醒。
此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程(notifyAll),那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后C
再执行。如果是if的话,那么A修改完num后,C不会再去判断num的值,直接会给num+1,如果是while的话,A执行完之后,C还会去判断num的值,因此就不会执行。
上述情况称为:虚假唤醒
此时解决方法:将 if 改为
while
public class Test3 {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data{
//属性
private int number = 0;
//+1方法
public synchronized void increment() throws InterruptedException {
while (number != 0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"-->"+number);
//加完了通知其他线程
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number == 0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"-->"+number);
//减完了通知其他线程
this.notifyAll();
}
}
...
A-->1
B-->0
C-->1
D-->0
C-->1
D-->0
C-->1
D-->0
C-->1
D-->0
C-->1
D-->0
3)Lock版本(JUC版本)
Lock
newCondition()
方法返回一个新Condition绑定到该实例。阅读源码:
发现
Lock
替换synchronized
方法和语句的使用
await
替换wait
signal
替换notify
signalAll
替换notifyAll
实例:
public class Test2 {
public static void main(String[] args) {
Data1 data = new Data1();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data1 {
//属性
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//condition.await(); 等待
//condition.signalAll(); 唤醒全部
//+1方法
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//加完了通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//减完了通知其他线程
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
打印结果
A-->1
B-->0
...
A-->1
D-->0
A-->1
B-->0
A-->1
B-->0
A-->1
B-->0
思考: 实现结果与synchronized效果一致,仅仅只是为了覆盖原来的技术?
4)Condition的优势
精准的通知和唤醒的线程!
如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程~
public class Test3 {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.printB();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
data.printC();
}
},"C").start();
}
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data3{
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1;
public void printA(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number != 1) {
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "=>AAAAAA"+"-----number为->"+number);
// 唤醒,唤醒指定的人,B
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number != 2) {
// 等待
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "=>BBBBBB"+"-----number为->"+number);
// 唤醒,唤醒指定的人C
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number != 3) {
// 等待
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "=>CCCCCC"+"-----number为->"+number);
// 唤醒,唤醒指定的人A
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
打印结果
A=>AAAAAA-----number为->1
B=>BBBBBB-----number为->2
C=>CCCCCC-----number为->3
A=>AAAAAA-----number为->1
B=>BBBBBB-----number为->2
C=>CCCCCC-----number为->3
A=>AAAAAA-----number为->1
B=>BBBBBB-----number为->2
C=>CCCCCC-----number为->3
A=>AAAAAA-----number为->1
B=>BBBBBB-----number为->2
C=>CCCCCC-----number为->3
A=>AAAAAA-----number为->1
B=>BBBBBB-----number为->2
C=>CCCCCC-----number为->3
在搜集资料的过程中发现用wait和notify 也实现了精准唤醒
5)使用wait()与notify()实现精准唤醒
public class Test4 {
public static void main(String[] args) {
Data4 data = new Data4();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.printC();
}
},"C").start();
}
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data4{
private int number = 1;
public synchronized void printA() {
// 业务,判断-> 执行-> 通知
while (number != 1) {
// 等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "=>AAAAAA->"+number);
// 唤醒,唤醒指定的人,B
number = 2;
this.notifyAll();
}
public synchronized void printB(){
// 业务,判断-> 执行-> 通知
while (number != 2) {
// 等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "=>BBBBBB->"+number);
// 唤醒,唤醒指定的人,B
number = 3;
this.notifyAll();
}
public synchronized void printC(){
// 业务,判断-> 执行-> 通知
while (number != 3) {
// 等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "=>CCCCCC->"+number);
// 唤醒,唤醒指定的人,B
number = 1;
this.notifyAll();
}
}
结果同样实现了精准唤醒
A=>AAAAAA->1
B=>BBBBBB->2
C=>CCCCCC->3
A=>AAAAAA->1
B=>BBBBBB->2
C=>CCCCCC->3
A=>AAAAAA->1
B=>BBBBBB->2
C=>CCCCCC->3
A=>AAAAAA->1
B=>BBBBBB->2
C=>CCCCCC->3
A=>AAAAAA->1
B=>BBBBBB->2
C=>CCCCCC->3
5、8锁的现象
- 如何判断锁的是谁!锁到底锁的是谁?
- 锁会锁住:对象、Class
- 深刻理解我们的锁
1)标准情况
1、标准情况下,两个线程先打印什么?
public class Test1 {
public static void main(String[] args) throws InterruptedException {
phone1 phone = new phone1();
new Thread(()->{
phone.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
},"A").start();
}
}
class phone1{
public synchronized void send(){
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
输出结果:
发短信
打电话
2、标准情况下,我们让发短信 延迟4s,两个线程先打印什么?
public class Test2 {
public static void main(String[] args) throws InterruptedException {
phone2 phone = new phone2();
new Thread(()->{
phone.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
},"A").start();
}
}
class phone2{
public synchronized void send(){
//休息4秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
输出结果:
发短信
打电话
总结:
synchronized 锁的对象是
方法的调用者
,也就是实例化后的对象。两个方法用的是
同一个锁
,谁先拿到谁执行!
2)增加普通方法
3、增加一个普通方法后,两个线程先打印什么?
public class Test3 {
public static void main(String[] args) throws InterruptedException {
phone3 phone = new phone3();
new Thread(()->{
phone.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.hello();
},"B").start();
}
}
class phone3{
public synchronized void send(){
//休息4秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public void hello(){
System.out.println("hello");
}
}
hello
发短信
4、两个对象,两个同步方法,两个线程先打印什么?
public class Test3 {
public static void main(String[] args) throws InterruptedException {
phone3 phone1 = new phone3();
phone3 phone2 = new phone3();
new Thread(()->{
phone1.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
},"B").start();
}
}
class phone3{
public synchronized void send(){
//休息4秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
输出结果:
打电话
发短信
总结:
synchronized 锁的对象是
方法的调用者
,也就是实例化后的对象。普通方法不会上锁
3)静态方法
5、增加两个静态的同步方法,只有一个对象,两个线程先打印什么?
public class Test5 {
public static void main(String[] args) throws InterruptedException {
phone5 phone = new phone5();
new Thread(()->{
phone.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
},"B").start();
}
}
class phone5{
public static synchronized void send(){
//休息4秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
输出结果:
发短信
打电话
6、增加两个静态的同步方法,两个对象,两个线程先打印什么?
public class Test5 {
public static void main(String[] args) throws InterruptedException {
phone5 phone1 = new phone5();
phone5 phone2 = new phone5();
new Thread(()->{
phone1.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
},"B").start();
}
}
class phone5{
public static synchronized void send(){
//休息4秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
输出结果:
发短信
打电话
总结:
- 对于
static静态方法
来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象
!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!
4)静态与普通方法
7、1个静态的同步方法,1个普通的同步方法 ,一个对象
public class Test7 {
public static void main(String[] args) throws InterruptedException {
phone7 phone = new phone7();
new Thread(()->{
phone.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
},"B").start();
}
}
class phone7{
//静态的同步方法
public static synchronized void send(){
//休息4秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//普通的同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
输出结果:
打电话
发短信
8、1个静态的同步方法,1个普通的同步方法 ,两个对象
public class Test7 {
public static void main(String[] args) throws InterruptedException {
phone7 phone1 = new phone7();
phone7 phone2 = new phone7();
new Thread(()->{
phone1.send();
},"A").start();
//休息1秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
},"B").start();
}
}
class phone7{
//静态的同步方法
public static synchronized void send(){
//休息4秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//普通的同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
输出结果:
打电话
发短信
总结:
- 因为一个锁的是Class类的模板,一个锁的是对象的调用者。所以不存在等待,直接运行。
总结:
new
出来的 this 锁的是具体的一个对象
static
Class 锁的是唯一的一个模板
6、集合类不安全
1)List不安全
public class TestList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
执行程序后会抛出:
java.util.ConcurrentModificationException
并发修改异常,也就是不同线程同时操作了同一list索引元素抛出的异常。
解决方法:
//1.集合自带的线程安全的list
List<String> list = new Vector<>();
//2.Collections工具类强行上锁
List<Object> list = Collections.synchronizedList(new ArrayList<>());
//3.用JUC包下的读写数组CopyOnWriteArrayList,读写分离
CopyOnWriteArrayList<Object> list = new CopyOnWriteArrayList<>();
public class Test1 {
public static void main(String[] args) {
/**
* 1.集合自带的线程安全的list
* List<String> list = new Vector<>();
* 2.Collections工具类强行上锁
* List<Object> list = Collections.synchronizedList(new ArrayList<>());
* 3.用JUC包下的读写数组CopyOnWriteArrayList,读写分离
* CopyOnWriteArrayList<Object> list = new CopyOnWriteArrayList<>();
*/
CopyOnWriteArrayList<Object> list = new CopyOnWriteArrayList<>();
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。
CopyOnWriteArrayList和Vector
CopyOnWriteArrayList写操作底层源码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
Vector底层是使用synchronized关键字来实现的:效率特别低下。
Vector写操作底层源码
public void add(E e) {
int i = cursor;
synchronized (Vector.this) {
checkForComodification();
Vector.this.add(i, e);
expectedModCount = modCount;
}
cursor = i + 1;
lastRet = -1;
}
CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!
2)Set不安全
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 1; i <= 100; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
执行程序后会抛出:
java.util.ConcurrentModificationException
并发修改异常
1.Collections工具类强行上锁
Set<String> set = Collections.synchronizedSet(new HashSet<>());
2.用JUC包下的读写数组CopyOnWriteArrayList,读写分离
Set<String> set = new CopyOnWriteArraySet<>();
HashSet的本质
public HashSet() {
map = new HashMap<>();
}
// add set 本质就是 map key是无法重复的!
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 不变的值,做value
private static final Object PRESENT = new Object();
3)Map不安全
//map 是这样用的吗? 不是,工作中不使用这个
//默认等价什么? new HashMap<>(16,0.75);
Map<String, String> map = new HashMap<>();
//加载因子、初始化容量
默认
加载因子是0.75
,默认的初始容量是16
同样的HashMap基础类也存在并发修改异常!
public class MapTest {
public static void main(String[] args) {
//map 是这样用的吗? 不是,工作中不使用这个
//默认等价什么? new HashMap<>(16,0.75);
/**
* 解决方案
* 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();
}
}
}
7、Callable
1、可以有返回值;
2、可以抛出异常;
3、方法不同,run()/call()
public class TestCallAble {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);//适配类
// 放入Thread中使用,结果会被缓存
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();
// 这个get方法可能会被阻塞,如果在call方法中是一个耗时的方法,所以一般情况我们会把这个放在最后,或者使用异步通信
int a = futureTask.get();
System.out.println("返回值:" + a);
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("call()");
return 1024;
}
}
8、CountDownLatch、CyclicBarrier、Semaphore
1)CountDownLatch
倒计时锁存器
- 不管你线程中间执行的情况,结果若是线程执行完了,那就再执行最后语句,如果没达到条件就一直等
public class CountDownLatchTest {
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()+" == get out");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("外面真凉");
}
}
输出结果
1 == get out
4 == get out
3 == get out
2 == get out
6 == get out
5 == get out
外面真凉
主要方法:
newCountDownLatch(int i); 创建线程总数为i
countDown 减一操作;
await 等待计数器归零,就唤醒,再继续向下运行
如果创建了7条任务线程,但只countDown了6次,那么将会一直阻塞线程
2)CyclicBarrier
加法计时器
:
- CyclicBarrier通常称为循环屏障。它和CountDownLatch很相似,都可以使线程先等待然后再执行。不过CountDownLatch是使一批线程等待另一批线程执行完后再执行;而CyclicBarrier只是使等待的线程达到一定数目后再让它们继续执行。
public class CyclicBarrierTest {
public static void main(String[] args) throws InterruptedException {
/**
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠的线程:7条线程
// 创建成功后执行runnable接口打印“召唤七颗龙珠成功”
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤成功");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集到第"+temp+"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
1收集到第1颗龙珠
4收集到第4颗龙珠
3收集到第3颗龙珠
2收集到第2颗龙珠
7收集到第7颗龙珠
6收集到第6颗龙珠
5收集到第5颗龙珠
召唤成功
创建线程Barrier后开启线程执行:
CyclicBarrier(int parties, Runnable barrierAction)
创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。
并在调用await()方法时自动+1:
如果执行的线程数到达设定值,则会执行 创建时设定的屏障动作,
如果无法到达则线程会处在阻塞状态
3)Semaphore
信号量,可近似看作资源池。
举例子(抢车位):
public class SemaphoreTest {
public static void main(String[] args) {
// 线程数量:停车位! 限流!
//创建只有3个停车位的停车场
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
// acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了车位");
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName()+"离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//停车结束,离开车位
// release() 释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
1抢到了车位
2抢到了车位
3抢到了车位
2离开了车位
3离开了车位
4抢到了车位
1离开了车位
5抢到了车位
6抢到了车位
6离开了车位
4离开了车位
5离开了车位
9、读写锁
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 1; i < 5; i++) {
int temp = i;
new Thread(()->{
cache.write(temp +"", temp +"");
}).start();
}
for (int i = 1; i < 5; i++) {
int temp = i;
new Thread(()->{
cache.read(temp +"");
}).start();
}
}
}
/**
* 方法未加锁,导致写的时候被插队
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
//写数据
public void write(String key,Object obj){
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,obj);
System.out.println(Thread.currentThread().getName()+"写入ok"+key);
}
//读数据
public void read(String key){
System.out.println(Thread.currentThread().getName()+"读取"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取ok"+key);
}
}
1写入1
4写入4
4写入ok4
3写入3
2写入2
3写入ok3
1写入ok1
2写入ok2
1读取1
1读取ok1
3读取3
3读取ok3
4读取4
2读取2
4读取ok4
2读取ok2
写入线程会被 读取线程中断,造成脏读,对数据不安全
我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。
但是这次我们采用更细粒度的锁:
ReadWriteLock
读写锁来保证
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCacheWithLock cache = new MyCacheWithLock();
for (int i = 1; i < 5; i++) {
int temp = i;
new Thread(()->{
cache.write(temp +"", temp +"");
},String.valueOf(i)).start();
}
for (int i = 1; i < 5; i++) {
int temp = i;
new Thread(()->{
cache.read(temp +"");
},String.valueOf(i)).start();
}
}
}
class MyCacheWithLock {
private volatile Map<String, Object> map = new HashMap<>();
//读写锁:对数据更精准控制
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//写数据 只希望有一个线程在执行
public void write(String key,Object obj){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,obj);
System.out.println(Thread.currentThread().getName()+"写入ok"+key);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//读数据:可一条或者多条同时执行
public void read(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取ok"+key);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
/**
* 存入数据过程上锁,安全
*/
}
1写入1
1写入ok1
2写入2
2写入ok2
4写入4
4写入ok4
3写入3
3写入ok3
2读取2
2读取ok2
1读取1
4读取4
4读取ok4
3读取3
1读取ok1
3读取ok3
存入数据过程上锁,安全
10、阻塞队列
BlockingQueue
是数据结构中队列Queue的延展使用。
1) BlockingQueue
是Collection的一个子类
多线程并发处理、线程池 情况下我们会使用阻塞队列
BlockingQueue
有四组API
方式 | 抛出异常 | 不会抛出异常,有返回值 | 阻塞,等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(timenum.timeUnit) |
移出 | remove | poll | take | poll(timenum,timeUnit) |
判断队首元素 | element | peek | – | – |
抛出异常
/**
* 抛出异常
*/
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(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//弹出 如果没有元素 会抛异常:java.util.NoSuchElementException
System.out.println(blockingQueue.remove());
//队首元素 如果没有元素 会报NoSuchElementException异常
System.out.println(blockingQueue.element());
}
不抛出异常,有返回值
/**
* 不抛出异常,有返回值
*/
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"));
//添加 一个不能添加的元素 使用offer只会返回false 不会抛出异常
System.out.println(blockingQueue.offer("d"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//弹出 如果没有元素 只会返回null 不会抛出异常
System.out.println(blockingQueue.poll());
//队首元素 如果没有元素 只会返回null 不会抛出异常
System.out.println(blockingQueue.peek());
}
等待 一直阻塞
/**
* 等待 一直阻塞
*/
public static void test3() throws InterruptedException {
ArrayBlockingQueue<Object> 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<Object> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
//添加 如果队列没有位置了,超时时间2s 等待如果超过2s就结束等待 返回false
System.out.println(blockingQueue.offer("d", 2, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//弹出 超过两秒 我们就不等待了 返回null
System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
}
2) SynchronousQueue
同步队列 :
没有容量,也可以视为容量为1的队列;
- 进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;
put方法 和 take方法;
Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;
put
了一个元素,就必须从里面先take
出来,否则不能再put
进去值!并且SynchronousQueue 的take是使用了lock锁保证线程安全的。
public class SynchronousQueueTest {
public static void main(String[] args) {
SynchronousQueue<String> sq = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" => put a");
sq.put("a");
System.out.println(Thread.currentThread().getName()+" => put b");
sq.put("b");
System.out.println(Thread.currentThread().getName()+" => put c");
sq.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " => take " + sq.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " => take " + sq.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " => take " + sq.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
A => put a
B => take a
A => put b
B => take b
A => put c
B => take c
11、线程池
线程池:三大方式、七大参数、四种拒绝策略
1)池化技术
在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。
简单点来说,就是提前保存大量的资源,以备不时之需,池化技术就是通过复用来提升性能。
常见池:
对象池
- 通过复用对象来减少创建对象、垃圾回收的开销;
连接池
- (数据库连接池、Redis连接池和HTTP连接池等)通过复用TCP连接来减少创建和释放连接的时间
线程池通过复用线程提升性能
使用内存池的优点
- 降低资源消耗。这个优点可以从创建内存池的过程中看出,当我们在创建内存池的时候,分配的都是一块块比较规整的内存块,减少内存碎片的产生。
- . 提高相应速度。这个可以从分配内存和释放内存的过程中看出。每次的分配和释放并不是去调用系统提供的函数或操作符去操作实际的内存,而是在复用内存池中的内存。
- . 方便管理。
使用内存池的缺点
- 缺点就是很可能会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到。
2)三大方法
Executors 工具类中3大方法
//创建一个线程池,根据需要创建新的线程,但在可用时将重用先前构建的线程。
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//创建一个线程池,使用固定数量的线程操作了共享无界队列
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//创建一个线程池,根据需要创建新的线程,但在可用时将重用先前构建的线程。
ExecutorService threadPool = Executors.newCachedThreadPool();
//工具类 Executors 三大方法;
public class ThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
ExecutorService threadPool = Executors.newFixedThreadPool(5); //创建一个固定大小的线程池
ExecutorService threadPool = 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();
}
}
}
3)七大参数
其本质都是调用本质**
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;
}
银行办理业务举例:
有5个柜台,每次办理一个人。
当天值班柜员只有2人。
剩余3个临时工等待超过一定时间会离开柜台。
候客区有3个座位,可容纳3人等待。
public class Test1 {
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
ExecutorService threadPool = new ThreadPoolExecutor(
2, //当天值班员工(核心线程池大小)
5, //柜台总数:最大核心线程池大小
3, //临时工的超时等待:超时了没有人调用就会释放
TimeUnit.SECONDS, //超时等待单位
new LinkedBlockingDeque<>(3),//候客区:阻塞队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //满了后告诉客人办不了业务了:抛异常RejectedExecutionException
try {
// 最大承载:队列:Deque + 最大核心线程池:max 此处为:5+3=8 柜台总数+候客区人数
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (
Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
4)拒绝策略
new ThreadPoolExecutor.AbortPolicy():
- 该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常
- 超出最大承载,就会抛出异常:队列容量大小+maxPoolSize
new ThreadPoolExecutor.CallerRunsPolicy():
- 该拒绝策略为:哪来的去哪里 main线程进行处理
new ThreadPoolExecutor.DiscardPolicy():
- 该拒绝策略为:队列满了,丢掉异常,不会抛出异常。
new ThreadPoolExecutor.DiscardOldestPolicy():
- 该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常
5)线程池调优
CPU密集型
这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
电脑的核数是几核就选择几;选择maximunPoolSize的大小
// 获取CPU的核数
int cpuNum = Runtime.getRuntime().availableProcessors();
System.out.println(cpuNum);
IO密集型
- 这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
- 例如:在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
12、四大函数式接口
Java 1.8之前的程序员:泛型、枚举、反射
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口:只有一个方法的接口(截至目前用的最多的是Runnable接口)
作用:简化编程模型
,在新版本的框架底层大量应用!
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
四大原生函数式接口
java.util.function
包下;
Function: 函数型接口
Predicate: 断定型接口
Suppier: 供给型接口
Consummer: 消费型接口
1)Function 函数型接口
Function 函数型接口, 有一个输入参数,有一个输出
/**
* Function 函数型接口, 有一个输入参数,有一个输出
*/
public class Test {
public static void main(String[] args) {
Function<String,String> function = new Function<String,String>() {
@Override
public String apply(String str) {
return str;
}
};
//lambda表达式简化
Function<String, String> function = str -> str;
System.out.println(function.apply("This is Function"));
}
}
2)Predicate 断定型接口
断定型接口:有一个输入参数,返回值只能是 布尔值
/**
* 断定型接口:有一个输入参数,返回值只能是 布尔值!
*/
public class Test {
public static void main(String[] args) {
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String str) {
return str.isEmpty();
}
};
//lambda表达式简化
Predicate<String> predicate = String::isEmpty;
System.out.println(predicate.test(""));//true
System.out.println(predicate.test("Test"));//false
}
}
3)Suppier 供给型接口
Supplier 供给型接口 没有参数,只有返回值
/**
* Supplier 供给型接口 没有参数,只有返回值
*/
public class Test {
public static void main(String[] args) {
Supplier supplier = new Supplier<Integer>() {
@Override
public Integer get() {
return 1024;
}
};
//lambda表达式简化
Supplier<String> supplier = () -> String.valueOf(1024);
System.out.println(supplier.get());
}
}
4)Consummer 消费型接口
Consumer 消费型接口: 只有输入,没有返回值
/**
* Consumer 消费型接口: 只有输入,没有返回值
*/
public class Test {
public static void main(String[] args) {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
};
//lambda表达式简化
Consumer<String> consumer = System.out::println;
consumer.accept("这是消费型接口");
}
}
13、Stream流式计算
大数据:存储 + 计算
集合、MySQL 本质就是存储东西的;
计算都应该交给流来操作!
流的简单介绍
Java 8 中,引入了流(Stream)的概念,利用提供的Stream API,我们可以方便的操作集合数据,这种方式很类似于使用SQL对数据库的操作。
如何生成流
利用Stream API
1、所有继承自Collection的接口都可以直接转化为流:
List<Integer> l1 = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = l1.stream();
Map<String,Student> s1 = new HashMap<>();
Stream<Map.Entry<String, Student>> stream1 = s1.entrySet().stream();
2、利用Arrays类中的stream()方法:
Integer[] i1 = new Integer[]{1,2,3,4,5};
Stream<Integer> stream = Arrays.stream(i1);
3、使用Stream类中的静态方法:
Stream<Integer> stream = Stream.of(1,2,3,4,5);
4、利用BufferedReader读取文本中的内容转化为流:
BufferedReader reader = new BufferedReader(new FileReader("D:\\stream.txt"));
Stream<String> stream = reader.lines();
我们经常使用的还是方式1,将集合转化为流。
题目要求:一分钟内完成此题,只能用一行代码实现!
现在有5个用户!筛选:
1、ID 必须是偶数
2、年龄必须大于23岁
3、用户名转为大写字母
4、用户名字母倒着排序
5、只输出一个用户!
/**
* 题目要求:一分钟内完成此题,只能用一行代码实现!
* 现在有5个用户!筛选:
* 1、ID 必须是偶数
* 2、年龄必须大于23岁
* 3、用户名转为大写字母
* 4、用户名字母倒着排序
* 5、只输出一个用户!
*/
public class Test {
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(5, "e", 25);
User u6 = new User(6, "f", 26);
// 集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5, u6);
list.stream()
.filter(u -> u.getId() % 2 == 0)
.filter(u -> u.getAge() > 23)
.map(u -> new User(u.getId(), u.getName().toUpperCase(), u.getAge()))
.sorted((uu1, uu2) -> uu2.getAge().compareTo(uu1.getAge()))
.limit(1)
.forEach(System.out::println);
}
}
class User {
private Integer id;
private String name;
private Integer age;
//get,set,toString...
}
输出结果:
User{id=6, name='F', age=26}
涉及方法说明:
- filter()
Stream<T> filter(Predicate<? super T> predicate)
//用于对数据进行过滤,筛选出符合条件的数据;
//接收一个Predicate的函数接口,用于进行条件的过滤;
//返回符合条件的数据组成的一个新的流;
- map()
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
//对数据进行处理,将T类型的对象转化为R类型的对象,
//简单来说就是对流中的数据进行同一项操作;
//接收一个Function的函数接口,用于对数据处理;
//返回R类型的数据组成的一个新的流;
- sorted()
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
//将流中的数据进行排序,然后排序后的数据组合成一个新的流;
//无参的方法,默认按照升序升序进行排列;
//有参的方法,需要传入Comparator接口的一个实现类,按照该实现进行排序操作;
- limit()
Stream<T> limit(long maxSize)
//返回由该流的元素组成的流,截断长度不能超过maxSize 。
- foreach()
void forEach(Consumer<? super T> action)
//对此流的每个元素执行操作。
//最后的System.out::println打印的是传入的参数,
//也就是Customer的参数泛型T,这个T就是从list的流对象获取的T,
//最终都是List对象创建时指定的泛型
14、ForkJoin分支合并
从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。
这种思想和大数据:
MapReduce
很像(input --> split --> map --> reduce --> output)
举个例子
明天就要开学了,暑假作业还没做完,翻开一看还有半本没有写,于是你把寒假作业拆开分成几份,找来小伙伴承诺报酬5毛钱,分别写完之后重新装订好
这就是Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。
简单来说就是种分而治之的思想
1)ForkJoin 特点: 工作窃取!
实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。
但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。
而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列
,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。工作窃取算法:
优点是充分利用线程进行并行计算,并减少了线程间的竞争,
缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
2)如何使用ForkJoin?
1、通过ForkJoinPool来执行
2、计算任务 ForkJoinPool.execute(ForkJoinTask<?> task)
3、计算类要去继承ForkJoinTask;
举例:对比
遍历
、Fork/Join
、和流计算
的运算速度
计算类
/**
* 求和计算的任务!
* // 如何使用 forkjoin
* // 1、forkjoinPool 通过它来执行
* // 2、计算任务 forkjoinPool.execute(ForkJoinTask task)
* // 3. 计算类要继承 ForkJoinTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private long start;
private long end;
//临界值
private long temp = 1000000L;
public ForkJoinDemo(long start, long end) {
this.start = start;
this.end = end;
}
/**
* 计算方法
*/
@Override
protected Long compute() {
if ((end - start) < temp) {
long sum = 0L;
for (long i = start; i < end; i++) {
sum += i;
}
return sum;
} else {
// 使用ForkJoin 分而治之 计算
//1 . 计算平均值
long middle = (start + end) / 2;
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(start, middle);
// 拆分任务,把线程压入线程队列
forkJoinDemo1.fork();
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle, end);
// 拆分任务,把线程压入线程队列
forkJoinDemo2.fork();
return forkJoinDemo1.join() + forkJoinDemo2.join();
}
}
}
测试类
public class ForkJoinTest {
private static final long SUM = 20_0000_0000;
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
/**
* 使用普通方法
*/
public static void test1() {
long start = 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 - start));
System.out.println("----------------------");
}
/**
* 使用ForkJoin 方法
*/
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo task = new ForkJoinDemo(0, SUM);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
System.out.println(submit.get());
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - start));
System.out.println("----------------------");
}
/**
* 使用普通方法
*/
public static void test3() {
long start = System.currentTimeMillis();
long sum = LongStream.range(0, SUM).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println(sum);
System.out.println("时间:" + (end - start));
System.out.println("----------------------");
}
}
运行结果
1999999999000000000
时间:1081
----------------------
1999999999000000000
时间:688
----------------------
1999999999000000000
时间:498
----------------------
.parallel().reduce(0, Long::sum)
使用一个并行流去计算整个计算,提高效率。
小结:
Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。
使用Fork/Join模式可以进行并行计算以提高效率。
15、异步回调
Java内部的ajax方法:Future 设计=>对将来的某个事件的结果进行建模
平时使用CompletableFuture
较多
(1)没有返回值的runAsync异步回调
public class FutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 没有返回值的 runAsync 异步回调
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"runAsync=>Void");
});
System.out.println("1111");
completableFuture.get(); // 获取阻塞执行结果
}
}
(2)有返回值的异步回调supplyAsync
public class FutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"supplyAsync=>Integer");
int i = 10/0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=>" + t); // 正常的返回结果
System.out.println("u=>" + u); // 错误信息:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 233; // 可以获取到错误的返回结果
}).get());
}
}
whenComplete: 有两个参数,一个是t 一个是u
T:是代表的 正常返回的结果;
U:是代表的 抛出异常的错误信息;
如果发生了异常,get可以获取到exceptionally
返回的值;
16、JMM
JAVA内存模型,是一个概念,是不存在的东西,是一种约定
关于JMM的同步的约定:
线程解锁的时候必须把共享变量立刻刷新回主内存(因为线程会把主存中的变量拷贝一份,线程操作的都是主存变量的拷贝,所以需要把做的更改刷新回主存)
线程加锁前,必须读取主存中变量的最新值到线程的工作内存中
加锁和解锁是同一把锁
线程分为: 工作内存 主内存
- 工作内存相当于虚拟机栈帧中的局部变量表 主存相当于堆 所有的线程都要进栈
8种操作:(官方)
Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
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操作之前,必须把此变量同步回主内存
遇到问题:程序不知道主存中的值已经被修改过了!
17、 volatile
Volatile 是 Java 虚拟机提供 轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
1)保证可见性
下面这个程序无法停止,因为线程无法知道主线程中的变量已经改变了,所以需要volatile
public class VolatileTest {
// 不加 volatile 程序就会死循环!
// 加 volatile 可以保证可见性
// private volatile static int num = 0;
private static int num = 0;
public static void main(String[] args) { // main
new Thread(() -> { // 线程 1 对主内存的变化不知道的
while (num == 0) {
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
但是如果在while循环里面添加了打印i的语句
new Thread(() -> { // 线程 1 对主内存的变化不知道的
while (num == 0) {
System.out.println(num);
}
}).start();
如下图,程序运行一会就停止了
这是因为子线程强制读了一遍i的值
在循环体中打印输出语句时,println方法中有synchronized同步代码块,而synchronized也能保证对变量的修改可见性,对变量num进行操作以后,num的值也会立即刷回主存
在子线程加了输出语句会导致run方法里面的循环每输出一次进行一次判断,当内存数据修改,等缓存一致性协议生效 再判断 就会退出循环
下面是println的源码
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
也可以给变量加
volatile
加了volatile会
强制保证可见性
,不用考虑cpu有没有时间
private volatile static int num=0;
volatile是如何保证可见性?
有volatile修饰的共享变量进行写操作的时候多出一条带lock前缀的指令,如下所示
我们相信volatile保证线程可见性肯定与这条lock前缀的指令有关,经过查证知道,lock前缀的指令在多核处理器下会引发两件事情
1. 将当前处理器缓存行的数据写回到系统内存
。
2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但是操作完了不知道什么时候写回内存。而对声明了volatile关键字的变量进行写操作,JVM会向处理器发送一条lock前缀的指令,将这个变量所在的缓存行立即写回系统内存。并且为了保证各个处理器的缓存是一致的,实现了缓存一致性协议
,各个处理通过嗅探
在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态
,那么下次对这个数据进行操作,就会重新从系统内存中获取最新的值。对应JMM来说就是:
1. Lock前缀的指令让线程工作内存中的值写回主内存中;
2. 通过缓存一致性协议,其他线程如果工作内存中存了该共享变量的值,就会失效;
3. 其他线程会重新从主内存中获取最新的值;
2)不保证原子性
什么叫原子性: 不可分割
线程A在执行任务的时候,是不能被打扰的,要么同时成功,要么同时失败
public class VolatileTest2 {
private static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
//因为程序里面始终是有2个线程的,main线程和gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
//Thread.yield()是在主线程中执行的,意思是还有出路GC和main之外的其他线程在跑,主线程就让出cpu不往下执行,让出然后重新竞争cpu的执行权
}
//理论上打印出来应该是20000
System.out.println(Thread.currentThread().getName() + " " + num);//肯定不能直接
}
}
Thread.yield()
是在主线程中执行的,意思是还有出路GC和main之外的其他线程在跑,主线程就让出cpu不往下执行,是让出然后重新竞争cpu的执行权,有可能还是main抢到,不过这里是循环,抢到继续让出,直到只有2个线程
输出结果:理论上打印出来应该是20000,但是实际上每次都不一样
main 18649
解决方法
- 我们可以加
synchronized
public synchronized static void add(){
num++;
}
输出结果:
main 20000
如果只是给变量加上volatile
private volatile static int num=0;
输出结果:
main 18684
总结:volatile 不保证原子操作
我们来看看jvm底层是怎么操作的
我们来分析一下字节码操作
第一步:获得 num的值
第二步: num的值加1
第三步: 写回num
在多线程操作的过程中,可能加1后的值还没来得及更新,就被另一个线程读到进行加1,也就是说,有一些线程用的是以前的值,所以整体加起来会小于20000
之所以小于理论值,是因为线程回写数据到主内存的时候,覆盖了已经被其他更快的线程执行的结果
假如num是0,就可能线程1加了1,还没写回,线程2就拿到了,此时num还是0,所以两个线程都操作了num,但是num的值是1,相当于只加了一次
对线程来讲只保证每次加之前,从主存取一下当前值
加的次数是固定的,有的线程取的是旧值,结果肯定小了
如果不加lock和synchronized ,怎么样保证原子性?
使用原子类解决原子性问题
public class VolatileTest2 {
private static AtomicInteger num = new AtomicInteger();
public static void add() {
num.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
//因为程序里面始终是有2个线程的,main线程和gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
//Thread.yield()是在主线程中执行的,意思是还有出路GC和main之外的其他线程在跑,主线程就让出cpu不往下执行,让出然后重新竞争cpu的执行权
}
//理论上打印出来应该是20000
System.out.println(Thread.currentThread().getName() + " " + num);//肯定不能直接
}
}
输出结果
main 20000
因为原子类底层初始化的时候,将赋值给了一个volatiel属性
原子类的底层都直接和操作系统挂钩,在内存中修改值,Unsafe类是很特殊的存在
源码
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//valueOffset
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//objectFieldOffset native 直接和操作系统挂钩!是在内存中修改值
public native long objectFieldOffset(Field f);
3)禁止指令重排
什么是指令重排?
我们写的程序,计算机并不是按照我们自己写的那样去执行的
源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!
int x=1; //1
int y=2; //2
x=x+5; //3
y=x*x; //4
//我们期望的执行顺序是 1_2_3_4 可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的
可能造成的影响结果:前提:a b x y这四个值 默认都是0
线程A | 线程B |
---|---|
x=a | y=b |
b=1 | a=2 |
正常的结果: x = 0; y =0;
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
可能在线程A中会出现,先执行b=1,然后再执行x=a;
在B线程中可能会出现,先执行a=2,然后执行y=b;
那么就有可能结果如下:x=2; y=1.
volatile可以避免指令重排:
volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。
内存屏障:CPU指令。作用:
保证特定的操作的执行顺序;
可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)
volatile标注后,编译器生成代码会不优化(避免指令重排)
volatile写前加SS屏障写后加SL屏障,读前加LL屏障,读后加LS屏障
4)总结
volatile可以保证可见性;
不能保证原子性
由于内存屏障,可以保证避免指令重排的现象产生
18、深入单例模式
说到volatile的防止指令重排,那么volatile的内存屏障在哪里使用的最多,就是单例模式了
1)饿汉式单例
主要特点:
有构造函数私有,避免在外部被创建对象
提前创建好对象
提供公有的接口,返回在类内部提前创建好的对象
静态变量随着类的加载就已经实例化了,跟是否调用静态方法没有关系
饿汉式加载时就会初始化,懒汉式只有在获取单例的时候才会初始化
类加载时,成员变量会被初始化,局部变量不会
存在问题:
- 饿汉式一上来就会把所有的东西加载到内存,对象就已经存在了,对象没有使用的话,
可能会浪费内存
/**
* 饿汉式单例
*/
public class Hungry {
private Hungry() {}//私有构造方法
private final static Hungry hunger = new Hungry();//饿汉式
public static Hungry getInstance() {
return hunger;
}
}
2)懒汉式单例
针对饿汉式单例的浪费内存的问题,提出了懒汉式单例,要用的时候再创建对象
/**
* 懒汉式单例
*/
public class LazyMan {
private LazyMan() {}//私有构造方法
private static LazyMan lazyman;//还没有创建对象,只是声明,没有new
public static LazyMan getInstance() {
if (lazyman == null) {
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
return lazyman;
}
}
在多个线程的情况下,懒汉式单例可能会出现安全问题
/**
* 懒汉式单例
*/
public class LazyMan {
private LazyMan() {//私有构造方法
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyman;//还没有创建对象,只是声明,没有new
public static LazyMan getInstance() {
if (lazyman == null) {
lazyman = new LazyMan();//如果这个对象为空,就实例化这个对象
}
return lazyman;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(LazyMan::getInstance).start();
}
}
}
可以看到,有5个线程调用了构造函数,这说明程序中现在有5个Lazyman对象,就不是单例了,所以不安全
3)双重锁机制(DCL懒汉式)
/**
* 双重锁机制(DCL懒汉式)
*/
public class LazyMan {
private LazyMan() {//私有构造方法
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyman;//还没有创建对象,只是声明,没有new
public static LazyMan getInstance() {
//双重检测锁模式 DCL懒汉式
if (lazyman==null){
//如果为空,先上一层锁,锁LazyMan当前对象
synchronized (LazyMan.class){//静态方法是类锁
//如果synchronized直接写在方法上,所有线程都要抢锁,效率低,这个只有为空时才会抢锁
if (lazyman == null) {//在锁里面再判断一次
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(LazyMan::getInstance).start();
}
}
}
可以看到只创建了一个对象
解读:
lazyman = new LazyMan();
创建对象的过程在极端情况下肯定是会出现问题的,因为不是原子性操作,会经历3个步骤 :
分配内存空间,
执行构造方法(初始化对象)
把对象指向分配的空间
存在问题
可能会发生指令重排,可能会按1 3 2的顺序执行,就是先分配内存空间,然后用空对象先占用内存空间,占用之后再执行构造方法
很有可能A执行了1 3还没执行2,但是现在lazyman已经不是null了,如果现在进来一个B线程,外层判断不为空,那么B线程会直接返回lazyman,但lazyman实际上还没有完成构造,所以不安全(new只是把应用加上了,但是堆还没有创建完,return就会有问题)
所以要用volatile修饰防止指令重排(防止第二个线程抢先执行,抢先返回一个尚未初始化完成的引用)
所以这里是同步代码块保证了操作的原子性,volatile禁止了指令重排
指令重排的原理是为了提升CPU多段流水的效率,但并不是指令任意重排,处理器必须能正确处理指令依赖关系保障程序得出正确的执行结果
防止指令重排的双重锁机制(DCL懒汉式)
/**
* 使用volatile修饰防止指令重排的双重锁机制(DCL懒汉式)
*/
public class LazyMan {
private LazyMan() {//私有构造方法
System.out.println(Thread.currentThread().getName()+"ok");
}
//还没有创建对象,只是声明,没有new
private volatile static LazyMan lazyman;
public static LazyMan getInstance() {
//双重检测锁模式 DCL懒汉式
if (lazyman==null){
//如果为空,先上一层锁,锁LazyMan当前对象
synchronized (LazyMan.class){//静态方法是类锁
//如果synchronized直接写在方法上,所有线程都要抢锁,效率低,这个只有为空时才会抢锁
if (lazyman == null) {//在锁里面再判断一次
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(LazyMan::getInstance).start();
}
}
}
总结:
synchronized保证的是if判断和new一个对象能同时成功或同时失败,但是new一个对象不是原子操作,执行1 3后,第二个线程认为已经new对象成功了,最上面的if判断不等于null,所以要用volatile修饰防止指令重排
4)静态内部类
在一个类里面写一个静态的类
首先只要单例一定要先构造器私有
加载外部类时,不会加载静态内部类
线程安全且懒加载
但是静态内部类单例也是不安全的,因为反射可以破坏单例
/**
* 静态内部类
*/
public class Holder {
private Holder() {}
public static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass {
private static final Holder HOLDER = new Holder();
}
}
测试
/**
* 静态内部类
*/
public class Holder {
private Holder() {
System.out.println(Thread.currentThread().getName()+"ok");
}
public static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass {
private static final Holder HOLDER = new Holder();
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
Holder.getInstance();
}).start();
}
}
}
可以看到,内存中只有一个实例,就是只有一个线程进入了构造函数,我猜是因为静态类只加载一次
存在安全问题
只要有反射,任何私有的都是纸老虎,我们以DCL的单例为例,来试试反射
public class LazyMan {
private LazyMan() {}
//还没有创建对象,只是声明,没有new
private volatile static LazyMan lazyman;
public static LazyMan getInstance() {
//双重检测锁模式 DCL懒汉式
if (lazyman==null){
synchronized (LazyMan.class){
if (lazyman == null) {
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
LazyMan instance1 = LazyMan.getInstance();
//获得空参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//无视私有构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
LazyMan instance2 = declaredConstructor.newInstance();
//测试两个对象是否一样
System.out.println(instance1);
System.out.println(instance2);
}
}
可以看到,两个对象不一样
在上面的基础上,对构造函数做如下修改
private LazyMan() {
synchronized (LazyMan.class){
if (lazyman!=null){
throw new RuntimeException("请不要试图通过反射破坏单例");
}
}
}
看完整代码
相当于在DCL的基础上又在构造函数里面加了一重检测
public class LazyMan {
private LazyMan() {
synchronized (LazyMan.class){
if (lazyman!=null){
throw new RuntimeException("请不要试图通过反射破坏单例");
}
}
}
//还没有创建对象,只是声明,没有new
private volatile static LazyMan lazyman;
public static LazyMan getInstance() {
//双重检测锁模式 DCL懒汉式
if (lazyman==null){
//如果为空,先上一层锁,锁LazyMan当前对象
synchronized (LazyMan.class){//静态方法是类锁
//如果synchronized直接写在方法上,所有线程都要抢锁,效率低,这个只有为空时才会抢锁
if (lazyman == null) {//在锁里面再判断一次
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
LazyMan instance1 = LazyMan.getInstance();
//获得空参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//无视私有构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
LazyMan instance2 = declaredConstructor.newInstance();
//测试两个对象是否一样
System.out.println(instance1);
System.out.println(instance2);
}
}
现在我们不用geyInstance()去获取对象,而是直接通过反射创建两个对象
public class LazyMan {
private LazyMan() {
synchronized (LazyMan.class){
if (lazyman!=null){
throw new RuntimeException("请不要试图通过反射破坏单例");
}
}
}
//还没有创建对象,只是声明,没有new
private volatile static LazyMan lazyman;
public static LazyMan getInstance() {
//双重检测锁模式 DCL懒汉式
if (lazyman==null){
//如果为空,先上一层锁,锁LazyMan当前对象
synchronized (LazyMan.class){//静态方法是类锁
//如果synchronized直接写在方法上,所有线程都要抢锁,效率低,这个只有为空时才会抢锁
if (lazyman == null) {//在锁里面再判断一次
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//LazyMan instance1 = LazyMan.getInstance();
//获得空参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//无视私有构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();
//测试两个对象是否一样
System.out.println(instance1);
System.out.println(instance2);
}
}
可以发现,我们用反射new的对象跟类里面的lazyman对象肯定是不一样的啊,没有调用getInstance(),类里面的lazyman就一直为空,所以单例又被破坏了,因为构造函数里面判断的是
if (lazyman == null) {//在锁里面再判断一次
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
解决方法,用个标志位
public class LazyMan {
private static boolean flag = false;
private LazyMan() {
synchronized (LazyMan.class){
if (!flag){
flag=true;
}else {
throw new RuntimeException("请不要试图通过反射破坏单例");
}
}
}
//还没有创建对象,只是声明,没有new
private volatile static LazyMan lazyman;
public static LazyMan getInstance() {
//双重检测锁模式 DCL懒汉式
if (lazyman==null){
//如果为空,先上一层锁,锁LazyMan当前对象
synchronized (LazyMan.class){//静态方法是类锁
//如果synchronized直接写在方法上,所有线程都要抢锁,效率低,这个只有为空时才会抢锁
if (lazyman == null) {//在锁里面再判断一次
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//LazyMan instance1 = LazyMan.getInstance();
//获得空参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//无视私有构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();
//测试两个对象是否一样
System.out.println(instance1);
System.out.println(instance2);
}
}
来我们继续破坏单例,我们把这个flag字段给它破坏了
public class LazyMan {
private static boolean flag = false;
private LazyMan() {
synchronized (LazyMan.class){
if (!flag){
flag=true;
}else {
throw new RuntimeException("请不要试图通过反射破坏单例");
}
}
}
//还没有创建对象,只是声明,没有new
private volatile static LazyMan lazyman;
public static LazyMan getInstance() {
//双重检测锁模式 DCL懒汉式
if (lazyman==null){
//如果为空,先上一层锁,锁LazyMan当前对象
synchronized (LazyMan.class){//静态方法是类锁
//如果synchronized直接写在方法上,所有线程都要抢锁,效率低,这个只有为空时才会抢锁
if (lazyman == null) {//在锁里面再判断一次
//如果这个对象为空,就实例化这个对象
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//LazyMan instance1 = LazyMan.getInstance();
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
//获得空参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//无视私有构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
LazyMan instance1 = declaredConstructor.newInstance();
flag.set(instance1,false);//把第一个对象的flag重新改成false
LazyMan instance2 = declaredConstructor.newInstance();
//测试两个对象是否一样
System.out.println(instance1);
System.out.println(instance2);
}
}
单例又被破坏了。。。。
那怎么解决呢?我们点进去反射的newInstance()看看呢
我们可以看到,如果类是一个枚举类型的话,就会告诉你不能使用反射破坏枚举,枚举是jdk 1.5 开始出现的,自带单例模式
接下来我们用枚举实现单例
5)枚举单例
枚举本身也是一个类
package juc.single;
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) {
EnumSingle instance1 = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.getInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
可以看到,我们获取的始终都是一个相同的对象
我们来试试用反射破坏枚举单例
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
上面的错误提示是枚举类没有空参的构造方法
也就是
EnumSingle.class.getDeclaredConstructor(null);
这句话出错了而正常破坏单例是应该报错不能使用反射破坏枚举 通过反编译我们可以看到,这个枚举本身也是一个class,它继承了一个枚举类
然而构造器还是无参的啊,说明我们还是被IDEA骗了
现在我们用jad.exe反编译试试
我们把class字节码生成java文件看看
jad -sjava EnumSingle.class
我们看看生成的java文件
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingle.java
package juc.single;
public final class EnumSingle extends Enum {
public static EnumSingle[] values() {
return (EnumSingle[]) $VALUES.clone();
}
public static EnumSingle valueOf(String name) {
return (EnumSingle) Enum.valueOf(juc / single / EnumSingle, name);
}
private EnumSingle(String s, int i) {
super(s, i);
}
public static EnumSingle getInstance() {
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static {
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[]{
INSTANCE
});
}
}
可以看到,不是无参构造器哦,而是有参构造器,有一个String,一个Int
现在我们修改反射代码运行
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
得到我们想要的结果了,抛出反射不能破坏枚举的单例异常
19、深入理解CAS
1)什么是CAS?
CAS :
compareAndSet
比较并交换大厂必须深入研究底层!!!!修内功!操作系统、计算机网络原理、组成原理、数据结构
public class CasTest {
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());
//CAS 是CPU的并发原语
//因为期望值是2020 实际值却变成了2021 所以会修改失败
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
Java无法操作内存,但是C++可以操作内存,Java可以通过native方法调用c++从而操作内存
思考怎么实现的呢?看AtomicInteger
源码
同时,Java可以通过unsafe类操作内存,从下图可以看到,unsafe类全是native方法
2)总结
CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。
优点:
- 是不用切换线程状态,因为切换线程状态性能消耗比较大
缺点:
由于底层是自旋锁,循环会浪费时间
因为是底层的cpu操作,一次只能保证一个共享变量的原子性
ABA问题
CAS:ABA问题?(狸猫换太子)
先执行线程1再执行线程2
线程1:两个操作:
1、期望值是1,变成3
2、期望是3,变成1
线程2:期望值是1,要变成2;所以对于线程2来说,A的值还是1,所以就出现了问题,骗过了线程1;
public class CasTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//模拟线程1
//期望值是2020,变成2021
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
//期望值是2021,变成2020
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
//模拟线程2
//期望值是2020,变成2021
System.out.println(atomicInteger.compareAndSet(2020, 2022));
System.out.println(atomicInteger.get());
}
}
20、原子引用
解决ABA问题,对应的思想:就是使用了乐观锁~
public class CasTest {
/**
* 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(2);
} 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();
}
}
注意
Integer使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间。
21、各种锁的理解
1)公平锁,非公平锁
synchronized和lock默认都是非公平锁
公平锁:非常公平,不能插队,必须先来后到
/**
* 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();
}
2)可重入锁
什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class ReentrantLock {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
演示可重入锁是什么意思
public class ReentrantLock2 {
public static void main(String[] args) {
Lock lock = (Lock) new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
lock.unlock();
}
}
} finally {
lock.unlock();
}
}
}).start();
}
}
可以发现没发生死锁,可以多次获取相同的锁
可重入锁有
- synchronized
- ReentrantLock
使用ReentrantLock的注意点
ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁
,并且加锁次数和释放次数要一样
以下代码演示,加锁和释放次数不一样导致的死锁
public class ReentrantLock3 {
public static void main(String[] args) {
Lock lock = (Lock) new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
//lock.unlock();// 这里故意注释,实现加锁次数和释放次数不一样
}
}
} finally {
lock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 20; i++) {
System.out.println("threadName:" + Thread.currentThread().getName());
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}).start();
}
}
由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
让加锁和释放次数一样,就没问题了
public class ReentrantLock3 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
lock.unlock();// 这里故意注释,实现加锁次数和释放次数不一样
}
}
} finally {
lock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 20; i++) {
System.out.println("threadName:" + Thread.currentThread().getName());
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}).start();
}
}
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;
}
- 自我设计自旋锁
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);
}
}
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进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。。。
4)死锁
public class DeadLock {
public static void main(String[] args) {
String lockA= "lockA";
String lockB= "lockB";
new Thread(new MyThread(lockA,lockB),"t1").start();
new Thread(new MyThread(lockB,lockA),"t2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
}
}
}
}
可以看到出现了死锁
1、使用jps定位进程号,jdk的bin目录下: 有一个jps
命令:jps -l
2、使用jstack 进程进程号 找到死锁信息
一般情况信息在最后:
视频学习地址:https: //www.bilibili.com/video/BV1B7411L7tE?spm_id_from=333.999.0.0