个人博客欢迎访问
作者总结不易,点赞,关注支持一下。
Doug Lea是一个无私的人,他深知分享知识和分享苹果是不一样的,苹果会越分越少,而自己的知识并不会因为给了别人就减少了,知识的分享更能激荡出不一样的火花。
程序(program)是为完成特定任务,用某种编程语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process)一个在内存中运行的应用程序。每个进程都有自己的独立的一块内存空间,一个进程可以有多个线程,比如Windows系统中,一个运行的*.exe就是一个进程。是一个动态的过程:有它自身的产生、存在和消亡的过程 -----生命周期
线程(thread)进程中的一个执行单元(控制单元),负责当前进程中程序的执行,是一个程序内部的一条执行路径。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程之间共享数据
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单元,每个线程拥有独立的运行栈和程序计数器(PC),线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一个堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效。但是多个线程操作共享的系统资源可能就会带来安全隐患。
单核CPU和多核CPU的理解
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重新进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入线程的操作系统中,通常一个进程有若干个线程,至少包含一个线程。
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 进程是操作系统资源分配的基本单元 | 线程是处理器任务调度和执行的基本单元 |
资源开销 | 每个进程都有独立的代码和数据空间(程序上下文),程序之间切换会有较大的开销 | 线程可以看做轻量级的进程,同一线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小 |
包含关系 | 如果一个进程中有多个线程,则执行过程不是一条线的,而是多条线共同完成的 | 线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程 |
内存分配 | 一个进程崩溃后,在保护模式下不会对其他进程产生影响 | 一个线程崩溃整个进程都会死掉,所以多进程比多线程健壮 |
执行过程 | 每个独立的进程有程序运行的入口、顺序执行序列和程序出口 | 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 |
多线程编程中一般线程的个数大于CPU的核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程能得到有效的执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于依次上下文交换。
概括来说就是:当前任务执行完CPU时间片切换到另一个任务之前会保存自己的状态,以便下次在切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换
上下文切换通常是计算密集型的,也就是说,他需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
并行:时间单位内,多个CPU同时执行多个任务,真正意义上的同时进行,比如多个人呢同时做不同的事情。
并发:一个CPU(采用时间片)按细分的时间片轮流交替执行,同时执行多个任务,从逻辑上看来这些任务是同时执行的。
串行:有n个任务,由一个线程按顺序执行,由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需要多线程呢?
多线程程序的优点:
并发编程的缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如**:内存泄漏、上下文切换、线程安全、死锁**等问题
出现线程安全问题的原因:
解决办法
构造器
JDK1.5之前创建新执行的线程有两种方式:
步骤
/**
* @author :zsy
* @date :Created 2021/4/12 10:11
* @description:继承Thread类创建线程
*/
//线程开启不一定立即执行,由CPU调度执行
public class TestThread1 extends Thread {
@Override
public void run() {
//run方法线程体
super.run();
for (int i = 0; i < 20; i++) {
System.out.println("testThread1---->" + i);
}
}
public static void main(String[] args) {
//main线程,主线程
//创建一个线程对象
TestThread1 thread1 = new TestThread1();
//调用start()方法开启线程
thread1.start();
//调用run()方法开启线程
//thread1.run();
for (int i = 0; i < 20; i++) {
System.out.println("main---->" + i);
}
}
}
实现多线程同步下载图片
/**
* @author :zsy
* @date :Created 2021/4/12 17:27
* @description:练习Thread,实现多线程同步下载图片
*/
public class TestThread2 extends Thread{
private String url;//网图地址
private String name;//保存文件名
public TestThread2(String url, String name) {
this.name = name;
this.url = url;
}
@Override
public void run() {
super.run();
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载的图片名称--->" + name);
}
public static void main(String[] args) {
//创建线程对象
TestThread2 t1 = new TestThread2("https://picsum.photos/id/237/300/200", "237.jpg");
TestThread2 t2 = new TestThread2("https://picsum.photos/id/337/300/200", "337.jpg");
TestThread2 t3 = new TestThread2("https://picsum.photos/id/437/300/200", "437.jpg");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
//下载器
class WebDownloader {
//下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现异常");
}
}
}
执行结果
下载的图片名称--->237.jpg
下载的图片名称--->437.jpg
下载的图片名称--->337.jpg
步骤
推荐使用Runnable对象,因为Java单继承的局限性
/**
* @author :zsy
* @date :Created 2021/4/12 18:05
* @description:实现Runnable接口创建多线程
*/
public class TestThread3 implements Runnable{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("testThread1---->" + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实例对象
TestThread3 thread3 = new TestThread3();
/*
//创建线程对象,通过线程对象来开启我们的线程,代理
Thread thread = new Thread(thread3);
//调用start()方法开启线程
thread.start();
*/
new Thread(thread3).start();
//调用run()方法开启线程
//thread1.run();
for (int i = 0; i < 20; i++) {
System.out.println("main---->" + i);
}
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:45
* @description:卖优盘接口
*/
public interface UsbSell {
float sell();
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:47
* @description:金士顿厂商
*/
public class UsbKingFactory implements UsbSell {
//定义厂家出厂价格
@Override
public float sell() {
return 85.0f;
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:48
* @description:淘宝代理商
*/
public class Taobao implements UsbSell {
//明确代理的目标对象
private UsbKingFactory factory = new UsbKingFactory();
@Override
public float sell() {
float price = factory.sell();
//增加价格
price += 15f;
return price;
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:50
* @description:用户类
*/
public class User {
//通过淘宝购买金士顿U盘
public float buyUsb() {
Object factory = new UsbKingFactory();
InvocationHandler handler = new MyHandler(factory);
UsbSell proxy = (UsbSell) Proxy.newProxyInstance(factory.getClass().getClassLoader(),
factory.getClass().getInterfaces(), handler);
return proxy.sell();
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:51
* @description:测试
*/
public class Test {
public static void main(String[] args) {
//创建用户
User user = new User();
//购买U盘
System.out.println(user.buyUsb());
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:45
* @description:卖优盘接口
*/
public interface UsbSell {
float sell();
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:47
* @description:金士顿厂商
*/
public class UsbKingFactory implements UsbSell {
//定义厂家出厂价格
@Override
public float sell() {
return 85.0f;
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:53
* @description:动态代理类
*/
public class MyHandler implements InvocationHandler {
//代理类(不确定)
private Object target = null;
public MyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res = method.invoke(target, args);
//获取到金士顿厂家的U盘价格
if (res != null) {
float price = (float) res;
System.out.println("从厂家拿到的U盘价格" + price);
price += 15f;
res = price;
}
return res;
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:50
* @description:用户类
*/
public class User {
//通过淘宝购买金士顿U盘
public float buyUsb() {
Object factory = new UsbKingFactory();
InvocationHandler handler = new MyHandler(factory);
UsbSell proxy = (UsbSell) Proxy.newProxyInstance(factory.getClass().getClassLoader(),
factory.getClass().getInterfaces(), handler);
return proxy.sell();
}
}
/**
* @author :zsy
* @date :Created 2021/4/12 21:57
* @description:测试动态代理
*/
public class Test {
public static void main(String[] args) {
//创建用户
User user = new User();
System.out.println("用户最终购买U盘的价格" + user.buyUsb());
}
}
步骤
实现多线程同步下载图片
/**
* @author :zsy
* @date :Created 2021/4/12 21:01
* @description:实现Callable接口
*/
public class TestCallable implements Callable<Boolean> {
private String url;//网图地址
private String name;//保存文件名
public TestCallable(String url, String name) {
this.name = name;
this.url = url;
}
@Override
public Boolean call() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载的图片名称--->" + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程对象
TestCallable t1 = new TestCallable("https://upload-images.jianshu.io/upload_images/20068213-872f9137d7826e87.png?imageMogr2/auto-orient/strip|imageView2/1/w/360/h/240", "1.jpg");
TestCallable t2 = new TestCallable("https://profile.csdnimg.cn/C/2/6/2_qq_45796208", "2.jpg");
TestCallable t3 = new TestCallable("https://img-home.csdnimg.cn/images/20210412010711.png", "3.jpg");
//创建执行服务
ExecutorService service = Executors.newFixedThreadPool(3);
//执行提交
Future<Boolean> r1 = service.submit(t1);
Future<Boolean> r2 = service.submit(t2);
Future<Boolean> r3 = service.submit(t3);
//获取结果
boolean re1 = r1.get();
boolean re2 = r2.get();
boolean re3 = r3.get();
//关闭服务
service.shutdownNow();
}
}
//下载器
class WebDownloader {
//下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现异常");
}
}
}
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
因此可以通过Runnable的方式创建实现了Callable接口的线程
创建线程实现Callable接口
/**
* @author :zsy
* @date :Created 2021/4/19 22:03
* @description:通过FutureTast创建Callable线程
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
//使用FutureTask对Callable对象进行包装
FutureTask task = new FutureTask(myThread);
new Thread(task, "A").start();
new Thread(task, "B").start();
boolean res = (boolean) task.get();
System.out.println(res);
}
}
class MyThread implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
System.out.println("call");
return true;
}
}
call
true
启动两个线程,存在缓存,会产生阻塞,结果可能需要等待
Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞
/**
* @author :zsy
* @date :Created 2021/4/12 20:10
* @description:多线程操作同一个对象,模拟买火车票的例子
*/
//发现问题:多个线程操作同一个对象情况下,线程不安全
public class TestThread4 implements Runnable{
private int ticketNums = 10;
@Override
public void run() {
while(true) {
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketNums <= 0) break;
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票");
}
}
public static void main(String[] args) {
TestThread4 ticket = new TestThread4();
//统一个对象被多个线程使用
new Thread(ticket, "小明").start();
new Thread(ticket, "小华").start();
new Thread(ticket, "黄牛党").start();
}
}
小华拿到了第10张票
小明拿到了第9张票
黄牛党拿到了第10张票
黄牛党拿到了第8张票
小明拿到了第7张票
小华拿到了第7张票
黄牛党拿到了第6张票
小华拿到了第6张票
小明拿到了第6张票
黄牛党拿到了第5张票
小明拿到了第5张票
小华拿到了第5张票
小华拿到了第4张票
黄牛党拿到了第3张票
小明拿到了第4张票
黄牛党拿到了第2张票
小明拿到了第1张票
发现问题:多个线程操作同一个对象情况下,线程不安全,数据紊乱
/**
* @author :zsy
* @date :Created 2021/4/12 20:32
* @description:模拟龟兔赛跑
*/
public class Race implements Runnable{
//胜利者
private static String winner;
@Override
public void run() {
//模拟跑步
for (int i = 0; i <= 100; i++) {
//模拟兔子休息
//每走10步休息1ms
if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判断是否已经有了胜利者
if (hasWinner(i)) break;
System.out.println(Thread.currentThread().getName() + "------->跑了" + i + "步");
}
}
//判断是否有胜利者
public boolean hasWinner(int step) {
if (winner != null) {
return true;
}
if (step >= 100) {
winner = Thread.currentThread().getName();
System.out.println("比赛结束,胜利者是" + winner);
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race, "兔子").start();
new Thread(race, "乌龟").start();
}
}
方法 | 说明 |
---|---|
setPriority( int newPriority ) | 更改线程优先级 |
static void sleep( long millis ) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt | 中断线程,不用此方法 |
boolean isAlive() | 测试线程是否处于活动状态 |
/**
* @author :zsy
* @date :Created 2021/4/13 14:31
* @description:测试停止线程
*/
public class TestStop implements Runnable{
//定义标志
private boolean flag = true;
@Override
public void run() {
int i = 0;
while(flag) {
System.out.println(Thread.currentThread().getName() + "run....." + i++);
}
if (!flag){
System.out.println(Thread.currentThread().getName() + "线程停止");
}
}
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop,"T1").start();
for (int i = 0; i < 300; i++) {
System.out.println("main------>" + i);
if (i == 200) {
testStop.stop();
}
}
}
}
/**
* @author :zsy
* @date :Created 2021/4/13 14:55
* @description:模拟倒计时
*/
public class TestSleep1 {
//打印当前系统时间
public static void main(String[] args) {
/*try {
tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}*/
Date nowTime = new Date();
while(true) {
nowTime = new Date();
System.out.println(new SimpleDateFormat("HH:mm:ss SSS").format(nowTime));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//模拟倒计时
public static void tenDown() throws InterruptedException {
int num = 10;
while(true) {
if (num <= 0) break;
Thread.sleep(1000);
System.out.println(num--);
}
}
}
/**
* @author :zsy
* @date :Created 2021/4/13 15:09
* @description:yiled方法
*/
public class TestYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-----> Start");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "------>End");
}
public static void main(String[] args) {
TestYield testYield = new TestYield();
new Thread(testYield, "A").start();
new Thread(testYield, "B").start();
}
}
B-----> Start
A-----> Start
B------>End
A------>End
/**
* @author :zsy
* @date :Created 2021/4/13 15:21
* @description:join
*/
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("线程vip------>" + i);
}
}
public static void main(String[] args) {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
for (int i = 0; i < 300; i++) {
if(i == 0) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main-------->" + i);
}
}
}
NEW
尚未启动的线程处于此状态
RUNNABLE
在Java虚拟机中执行的线程处于此状态
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
WAITING
正在等待另一个线程执行特定动作的线程处于此状态。
TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
TERMINATED
已退出的线程处于此状态
/**
* @author :zsy
* @date :Created 2021/4/13 15:40
* @description:测试线程状态
*/
public class TestState {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("END");
});
Thread.State state = thread.getState();
System.out.println(state);//NEW
thread.start();
state = thread.getState();
System.out.println(state);//RUNNABLE
while(thread.getState() != Thread.State.TERMINATED) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
state = thread.getState();
System.out.println(state);
}
}
}
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
END
TERMINATED
线程分为守护线程和用户线程
thread.setDaemon(true); //默认是false 表示是用户线程,正常都是用户线程。。
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
如:后台记录操作日志,监控内存,垃圾回收等…
守护线程和用户线程
比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。
注意事项:
/**
* @author :zsy
* @date :Created 2021/4/13 16:51
* @description:模拟线程不安全案例
*/
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account("慈善基金", 100.0f);
Drawing ming = new Drawing(account, 50.0f, "小明");
Drawing hua = new Drawing(account, 100.0f, "小华");
ming.start();
hua.start();
}
}
//账户类
class Account {
public String name;
public float balance;
public Account(String name, float balance) {
this.name = name;
this.balance = balance;
}
}
//模拟取款
class Drawing extends Thread{
private Account account;
private float drawingMoney;
public Drawing(Account account, float drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
super.run();
if (account.balance < drawingMoney) {
System.out.println("余额不足");
return;
}
//模拟网络延时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.account.balance = this.account.balance - drawingMoney;
System.out.println(this.getName() + "取出了" + drawingMoney);
System.out.println(this.account.name + "余额" + this.account.balance);
}
}
小明取出了50.0
慈善基金余额-50.0
小华取出了100.0
慈善基金余额-50.0
同步方法: public synchronized void method(int args){
}
缺陷: 若将一个大的方法申明为 synchronized 将会影响效率
// synchronized 同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
//判断是否有票
if(ticketNums<=0){
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);
}
//模拟取款
class Drawing extends Thread{
private Account account;
private float drawingMoney;
public Drawing(Account account, float drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
super.run();
synchronized (account) {
//锁的是用户
if (account.balance < drawingMoney) {
System.out.println("余额不足");
return;
}
//模拟网络延时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.account.balance = this.account.balance - drawingMoney;
System.out.println(this.getName() + "取出了" + drawingMoney);
System.out.println(this.account.name + "余额" + this.account.balance);
}
}
}
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止运行的情形,某一个同步块同时拥有“两个以上对象的锁”时,可能会发生死锁问题
/**
* @author :zsy
* @date :Created 2021/4/13 19:54
* @description:死锁
*/
public class DeadLock {
public static void main(String[] args) {
Makeup makeup1 = new Makeup(0, "灰姑娘");
Makeup makeup2 = new Makeup(1, "白雪公主");
makeup1.start();
makeup2.start();
}
}
class Lipstick {
}
class Mirror {
}
class Makeup extends Thread {
//只有一份使用static关键字
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String name;
public Makeup (int choice, String name){
this.choice = choice;
this.name = name;
}
@Override
public void run() {
super.run();
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆
public void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {
System.out.println(this.getName() + "拿到了口红");
Thread.sleep(1000);
synchronized (mirror) {
System.out.println(this.getName() + "拿到了镜子");
}
}
} else {
synchronized (mirror) {
System.out.println(this.getName() + "拿到了镜子");
Thread.sleep(2000);
synchronized (lipstick) {
System.out.println(this.getName() + "拿到了口红");
}
}
}
}
}
分析:造成原因:某一个同步块同时拿到了两个对象的锁
解决方法:同一个同步块中只允许拿到一个对象的锁
public void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {
System.out.println(this.getName() + "拿到了口红");
Thread.sleep(1000);
}
synchronized (mirror) {
System.out.println(this.getName() + "拿到了镜子");
}
} else {
synchronized (mirror) {
System.out.println(this.getName() + "拿到了镜子");
Thread.sleep(2000);
}
synchronized (lipstick) {
System.out.println(this.getName() + "拿到了口红");
}
}
}
上面四个产生死锁的必要条件,只要想办法破其中的任意一个或者多个条件就可以避免死锁的发生。
jps : JavaVirtual Machine Process Status Tool
jps -l 定位进程号
jps –v :输出jvm参数
jps –q :仅仅显示java进程号
jstack 进程号:查看死锁信息
/**
* @author :zsy
* @date :Created 2021/4/13 20:23
* @description:锁
*/
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
int ticketNum = 10;
private final Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
try {
while (true) {
if (ticketNum <= 0) break;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNum--);
}
}finally {
lock.unlock();
}
}
}
(AbstartQueueSynchronizer)抽象队列同步器,它的定位是为Java中几乎所有的锁和同步器提供一个基础的框架,AQS是基于FIFO的队列实现的,并且内部维护了一个状态变量state,通过原子更新这个状态变量state既可以实现加锁解锁操作。
volatile int waitStatus;
Node节点另外一个重要的成员是waitStatus,它表示节点等待在队列中的状态:
在AbstractQueuedSynchronizer内部,有一个队列,我们把它叫做同步等待队列。它的作用是保存等待在这个锁上的线程(由于lock()操作引起的等待)。此外,为了维护等待在条件变量上的等待线程,AbstractQueuedSynchronizer又需要再维护一个条件变量等待队列,也就是那些由Condition.await()引起阻塞的线程。
下面的类图展示了代码层面的具体实现:
可以看到,无论是同步等待队列,还是条件变量等待队列,都使用同一个Node类作为链表的节点。对于同步等待队列,Node中包括链表的上一个元素prev,下一个元素next和线程对象thread。对于条件变量等待队列,还使用nextWaiter表示下一个等待在条件变量队列中的节点。
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词。
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
// Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) {
// Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
// Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
由于一个重入锁可以生成多个条件变量对象,因此,一个重入锁就可能有多个条件变量等待队列。实际上,每个条件变量对象内部都维护了一个等待列表。其逻辑结构如下所示:
参考:Java 并发高频面试题:聊聊你对 AQS 的理解?
Condition对象的signal()通知
signal()通知的时候,是在条件等待队列中,按照FIFO进行,首先从第一个节点下手:
从上面内容我们注意到,线程的阻塞和唤醒都使用到了LockSupport的方法,LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport定义了一组以park开头的方法用来阻塞线程,以及unpark来唤醒一个被阻塞的线程。
LockSupport提供的方法:
内部都是通过Unsafe实现的
参考:Java并发面试问题,谈谈你对AQS的理解
具体的加锁过程
Tip:简述一下lock的过程,默认为非公平锁(排他锁)每次只获取1,首先尝试修改state修改成功,获取当前执行资格,开始执行。修改失败,acquire(1)去获取资源,首先尝试获取。tryAcquire(),被四个不同的类重写,尝试获取失败,将当前线程封装成节点,放入同步等待队列,并且自旋的去尝试获取锁(acquireQueued),这里可以说是整个加锁的核心了,因为第一个节点是正在运行的线程,所以从第二个节点才能去尝试获取锁,获取锁的过程中,难免一些线程会出现异常中断,自旋的过程中,删除这些已经取消等待的节点,并且将其余节点的waitStatus置为SINGAL等待唤醒,如果循环过程中,第二个节点获取到了资源,则跳出循环。
final void lock() {
//直接CAS交换state和自己的acquires
//交换成功,那到当前运行时间片
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//排他锁获取资源
acquire(1);
}
请求锁
public final void acquire(int arg) {
//尝试获得许可, arg为许可的个数。对于重入锁来说,每次请求1个。
if (!tryAcquire(arg) &&
// 如果tryAcquire 失败,则先使用addWaiter()将当前线程加入同步等待队列
// 然后继续尝试获得锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
进入一步看一下tryAcquire()函数。该函数的作用是尝试获得一个许可。对于AbstractQueuedSynchronizer来说,这是一个未实现的抽象函数。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
具体实现在子类中。在重入锁,读写锁,信号量等实现中, 都有各自的实现。
如果tryAcquire()成功,则acquire()直接返回成功。如果失败,就用addWaiter()将当前线程加入同步等待队列。
private Node addWaiter(Node mode) {
//Node中维护当前线程对象
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//将节点加到队列尾端,这是一个快速的办法,可能失败
//复杂的尝试,获取性能
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果快速加入失败,将node加到队列末尾
enq(node);
return node;
}
接着, 对已经在队列中的线程请求锁,使用acquireQueued()函数,从函数名字上可以看到,其参数node,必须是一个已经在队列中等待的节点。它的功能就是为已经在队列中的Node请求一个许可。
这个函数大家要好好看看,因为无论是普通的lock()方法,还是条件变量的await()都会使用这个方法。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功拿到资源
try {
boolean interrupted = false;//标记等待过程中是否被中断过
//又是一个“自旋”!
for (;;) {
final Node p = node.predecessor();//拿到前驱
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
if (p == head && tryAcquire(arg)) {
setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false; // 成功获取资源
return interrupted;//返回等待过程中是否被中断过
}
//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
什么是自旋
当一个线程拿不到锁的时候,可以不放弃CPU,空转,不断重试,也就是所谓的自旋,对于多CPU或者多核,自旋就很有用了,因为没有线程切换的开销。
AtomicInteger类的getAndIncrement()的源代码:
public final int getAndIncrement() {
for (;;) {
int current = get(); // 取得AtomicInteger里存储的数值
int next = current + 1; // 加1
if (compareAndSet(current, next)) // 调用compareAndSet执行原子更新操作
return current;
}
}
compareAndSwapInt 基于的是CPU 的 CAS指令来实现的。所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好。
他所利用的是基于冲突检测的乐观并发策略。 可以想象,这种乐观在线程数目非常多的情况下,失败的概率会指数型增加。
predecessor拿到队列的头节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
此方法主要用于检查状态,看看自己是否真的可以去休息了,万一队列前边的线程都放弃了,自己直接可以执行
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
private final boolean parkAndCheckInterrupt() {
//线程会停在这里
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
此方法是独占模式下线程释放共享资源的顶层入口。他会释放指定量的资源,如果彻底释放了(state = 0),他会唤醒等待队列里的其他线程来获取资源,这也正是unlock()的语义
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;//找到头结点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒等待队列里的下一个线程
return true;
}
return false;
}
逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!
此方法尝试去释放指定量的资源。如果已经彻底释放资源(state=0),要返回true,否则返回false。下面是tryRelease()的源码:
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {
//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
用unpark()唤醒等待队列中最前面的那个未放弃的线程再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了
如果获取锁的线程在release时异常了,没有unpark队列中的其他结点,这时队列中的其他结点会怎么办?是不是没法再被唤醒了?
答案是YES这时,队列中等待锁的线程将永远处于park状态,无法再被唤醒!!!但是我们再回头想想,获取锁的线程在什么情形下会release抛出异常呢??
ReentrantLock.ReadLock、Semaohore、CountDownLatch
与排他锁相比,共享锁的实现略微复杂一点。这也很好理解。因为排他锁的场景很简单,单进单出,而共享锁就不一样了。可能是N进M出,处理起来要麻烦一些。但是,他们的核心思想还是一致的。共享锁的几个典型应用有:信号量,读写锁中的写锁。
public final void acquireShared(int arg) {
//tryAcquireShared需要在子类中实现
//他表示尝试获取arg个共享许可,如果tryAcquireShared返回负数,表示失败
//返回0表示成功,但是没有多余的许可
if (tryAcquireShared(arg) < 0)
//申请失败,进入通同步等待队列
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
//加入同步等待队列,指定为SHARED类型
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//只有第二个节点有资格申请,因为是一个FIFO的队列,而第一个节点已经获得了许可
//因此第二个节点就是第一个需要申请而的节点
if (p == head) {
//尝试申请许可
int r = tryAcquireShared(arg);
if (r >= 0) {
//申请成功,把自己设置为头部
//根据条件,判断是否需要唤醒后续线程
//如果条件允许,就会尝试传播这个唤醒到后续节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//没有获取成功,只能park等待
//将来如果被唤醒,从这里开始执行,又会回到上面的tryAcquireShared
//和setHeadAndPropagate,去尝试共享许可,并且传播
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
其实流程根acquireQueued并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外,其实都一样。
跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。
那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。(保证公平,但降低了并发)。
//设置标志位,唤醒后续节点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//这里取得了许可成功,将当前节点放在头部
setHead(node);
//propagate>0表示后续节点的许可申请释放可以成功
//由propagate和waitStatus来判断能否唤醒后续线程,如果只有propagate来判断
//在并发环境中,可能出现线程不能被唤醒的情况
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
//唤醒下一个线程,设置传播状态
//被唤醒的线程 又会尝试tryAcquireShared 和 setHeadAndPropagate
doReleaseShared();
}
}
此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!
acquiredShared()流程
此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。
public final boolean releaseShared(int arg) {
//尝试释放许可,需要在子类中实现
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。
例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。
A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。
而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
//唤醒下一个线程,或者设置传播状态
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果需要唤醒后续线程,那么就唤醒,同时设置状态为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
//设置PROPAGATE状态,保证线程唤醒可以传播下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果被打扰,重新再来一次
//否则直接退出
if (h == head) // loop if head changed
break;
}
}
Mutex是一个不可重入的互斥锁实现。锁资源(AQS里的state)只有两种状态:0表示未锁定,1表示锁定。下边是Mutex的核心源码:
class Mutex implements Lock, java.io.Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否锁定状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
if (compareAndSetState(0, 1)) {
//state为0才设置为1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。两者语文一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。当然,接口的实现要直接依赖sync,它们在语义上也存在某种对应关系!!而sync只用实现资源state的获取-释放方式tryAcquire-tryRelelase,至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了,我们不用关心。
除了Mutex,ReentrantLock/CountDownLatch/Semphore这些同步类的实现方式都差不多,不同的地方就在获取-释放资源的方式tryAcquire-tryRelelase。掌握了这点,AQS的核心便被攻破了!
abstract static class Sync extends AbstractQueuedSynchronizer
Sync呢又分别有两个子类:FairSync和NofairSync
A线程准备进去获取锁,首先判断了一下state状态,发现是0,所以可以CAS成功,并且修改了当前持有锁的线程为自己。
这个时候B线程也过来了,也是一上来先去判断了一下state状态,发现是1,那就CAS失败了,真晦气,只能乖乖去等待队列,等着唤醒了,先去睡一觉吧。
A持有久了,也有点腻了,准备释放掉锁,给别的仔一个机会,所以改了state状态,抹掉了持有锁线程的痕迹,准备去叫醒B。
这个时候有个带绿帽子的仔C过来了,发现state怎么是0啊,果断CAS修改为1,还修改了当前持有锁的线程为自己。
B线程被A叫醒准备去获取锁,发现state居然是1,CAS就失败了,只能失落的继续回去等待队列,路线还不忘骂A渣男,怎么骗自己,欺骗我的感情。
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前的状态
int c = getState();
//0代表空闲
if (c == 0) {
//直接CAS交换state和自己的acquires
if (compareAndSetState(0, acquires)) {
//设置当前资源的拥有者为当前线程(排它锁)
setExclusiveOwnerThread(current);
return true;
}
}
//不为0代表当前有线程正在执行
//而且正在执行的线程是当前线程
else if (current == getExclusiveOwnerThread()) {
//重新设置state的值,不能为负数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded")
setState(nextc);
return true;
}
return false;
}
state:当前线程的状态
/**
* The synchronization state.
*/
private volatile int state;
protected final boolean tryRelease(int releases) {
//修改state状态
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//设置当前资源拥有者为空
setExclusiveOwnerThread(null);
}
//修改state
setState(c);
return free;
}
线A现在想要获得锁,先去判断下state,发现也是0,去看了看队列,自己居然是第一位,果断修改了持有线程为自己。
线程b过来了,去判断一下state,嗯哼?居然是state=1,那cas就失败了呀,所以只能乖乖去排队了。
线程A暖男来了,持有没多久就释放了,改掉了所有的状态就去唤醒线程B了,这个时候线程C进来了,但是他先判断了下state发现是0,以为有戏,然后去看了看队列,发现前面有人了,作为新时代的良好市民,果断排队去了。
线程B得到A的召唤,去判断state了,发现值为0,自己也是队列的第一位,那很香呀,可以得到了。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//判断当前等待队列是否为空
//如果为空并且交换state和acquires成功
//占有当前资源
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
代码的大概意思也是判断当前的线程是不是位于同步队列的首位,是就是返回true,否就返回false。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
区别 | 公平锁 | 非公平锁 |
---|---|---|
概念 | 多个线程按照申请锁额顺序去获取锁,线程会直接进入队列,永远都是队列的第一位才能获得锁 | 多个线程去获取锁的时候,会直接去获取锁,获取不到,在进入等待队列,如果能获取,就直接获取了 |
优点 | 所有线程都能得到资源,不会饿死在队列中 | 可以减少CPU去唤醒线程的开销,整体的吞吐量会高点,CPU不必唤醒所有线程,会减少唤起线程数量 |
缺点 | 吞吐量会下降很多,队列除了第一个线程,其他的线程都会阻塞,等待CPU唤醒,CPU唤醒阻塞线程的开销会很大 | 这样可能导致队列中间的线程一直获取不到锁,或者长时间获取不到锁,导致饿死 |
CAS(Compare And Swap 比较并且替换)是乐观锁的一种体现,是一种轻量级锁,JUC工具类的实现就是基于CAS
CAS怎么实现线程安全
线程在读取数据时不进行加锁,在准备写会数据时,先查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程
比较 + 更新 整体是一个原子操作,当然这个流程还是有问题的
ABA问题
看到问题所在没,我说一下顺序:
线程1读取了数据A
线程2读取了数据A
线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
线程3读取了数据B
线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值
懂了么,我尽可能的幼儿园化了,在这个过程中任何线程都没做错什么,但是值被改变了,线程1却没有办法发现,其实这样的情况出现对结果本身是没有什么影响的,但是我们还是要防范。
加标志、时间戳,值可能相同,但是版本号一定不一样
循环时间长开销大的问题:
是因为CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。
只能保证一个共享变量的原子操作
CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不行了,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。
那我就拿AtomicInteger举例,他的自增函数incrementAndGet()就是这样实现的,其中就有大量循环判断的过程,直到符合条件才成功。
synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
在JVM中对象在内存中分为三块区域:对象头(Header)、实例数据(InstanceDate)和对齐填充(Padding)。
对象头
以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Kass Pointer(类型指针)
你可以看到在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。
当 Monitor 被某个线程持有后,就会处于锁定状态,如图中的 Owner 部分,会指向持有 Monitor 对象的线程。
另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入及等待获取锁的线程。
如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。
在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。
每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。
同步方法和同步代码块底层都是通过monitor来实现同步的。
两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。
我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放。
广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁
不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,代码如下
import java.util.concurrent.atomic.AtomicReference;
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
//这句是很经典的“自旋”语法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
/**
* @author :zsy
* @date :Created 2021/4/24 23:45
* @description:测试锁
*/
public class Test {
private static final UnReentrantLock lock = new UnReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到锁");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "释放锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到锁");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "释放锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
}
}
A获取到锁
A释放锁
B获取到锁
B释放锁
使用原子引用来存放线程,同一线程两次调用lock()方法,如果不执行unlock()释放锁的话,第二次调用自旋的时候就会产生死锁,这个锁就不是可重入的,而实际上同一个线程不必每次都去释放锁再来获取锁,这样的调度切换是很耗资源的。
import java.util.concurrent.atomic.AtomicReference;
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
private int state = 0;
public void lock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
state++;
return;
}
//这句是很经典的“自旋”式语法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
if (state != 0) {
state--;
} else {
owner.compareAndSet(current, null);
}
}
}
}
在执行每次操作之前,判断当前锁持有者是否是当前对象,采用state计数,不用每次去释放锁。
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
并发协作模型“生产者/消费者模式” ——>管程法
【生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据】
/**
* @author :zsy
* @date :Created 2021/4/13 22:02
* @description:生产者消费者模式
*/
public class TestPc {
public static void main(String[] args) {
SynContainer container = new SynContainer();
Thread p = new Productor(container);
Thread c = new Consumer(container);
p.start();
c.start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer synContainer) {
this.container = synContainer;
}
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
container.push(new Product(i));
System.out.println("生产了------>" + i + "件产品");
}
}
}
//消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer synContainer) {
this.container = synContainer;
}
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
System.out.println("消费了" + container.pop().id + "件产品");
}
}
}
//产品
class Product {
public int id;
public Product(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer {
Product[] products = new Product[10];
int count = 0;
public synchronized void push(Product product) {
if (count == products.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products[count] = product;
count++;
//通知消费者可以消费了
this.notifyAll();
}
public synchronized Product pop() {
if (count == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
Product product = products[count];
//通知生产者开始生产
this.notifyAll();
return product;
}
}
并发协作模型“生产者/消费者模式” ——>信号灯法
/**
* @author :zsy
* @date :Created 2021/4/13 23:37
* @description:信号灯法
*/
public class TestPc2 {
public static void main(String[] args) {
Tv tv = new Tv();
Player player = new Player(tv);
Watcher watcher = new Watcher(tv);
player.start();
watcher.start();
}
}
//演员类
class Player extends Thread {
Tv tv;
public Player(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
tv.play("王牌对王牌");
} else {
tv.play("青春环游记");
}
}
}
}
//观众类
class Watcher extends Thread{
Tv tv;
public Watcher(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//节目
class Tv {
String program;
/**
* 演员表演节目 T
* 观众观看节目 F
*/
boolean flag = true;
public synchronized void play(String program) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.program = program;
System.out.println("演员表演了----->" + program + "节目");
this.flag = ! this.flag;
this.notifyAll();
}
public synchronized void watch() {
if(flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了" + program + "节目");
this.flag = !this.flag;
this.notifyAll();
}
}
/**
* @author :zsy
* @date :Created 2021/4/14 15:35
* @description:创建线程池
*/
public class TestPool {
public static void main(String[] args) {
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//执行线程
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭线程池
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待,一直等待
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
/**
* @author :zsy
* @date :Created 2021/4/14 17:24
* @description:买票
*/
public class SaleTicketDemo01 {
/*
* 真正的多线程开发,公司中的开发,降低耦合性
* 线程就是一个单独的资源类,没有任何附属的操作!
*/
public static void main(String[] args) {
//创建窗口对象
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}, "B").start();
}
}
//买票窗口
class Ticket {
//票数
private int tickNum = 50;
//买票
public synchronized void saleTicket() {
if (tickNum > 0) {
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买了第" + tickNum-- + "张票,剩余" + tickNum + "张票");
}
}
}
//买票
public void saleTicket() {
if (tickNum > 0) {
lock.lock();
try {
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买了第" + tickNum-- + "张票,剩余" + tickNum + "张票");
}finally {
lock.unlock();
}
}
}
Lock锁
公平锁 :先来后到,十分公平
非公平锁 :可以插队,不公平
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
tryLock()和lock()的区别
重要步骤:等待,业务,唤醒
/**
* @author :zsy
* @date :Created 2021/4/14 20:07
* @description:生产者消费者模式,线程通信
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 20; i++) data.increment(); }, "A").start();
new Thread(() -> {
for (int i = 0; i < 20; i++) data.decrement(); }, "B").start();
}
}
class Data {
int num = 0;
public synchronized void increment() {
if (num != 0) {
//等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//业务
System.out.println(Thread.currentThread().getName() + "--->" + ++num);
//唤醒
this.notifyAll();
}
public synchronized void decrement() {
if(num == 0) {
//等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//业务
System.out.println(Thread.currentThread().getName() + "--->" + --num);
//唤醒
this.notifyAll();
}
}
在两个线程的时候可以正常运行,当线程的数量加到四个的时候,出现了虚假唤醒的问题
new Thread(() -> {
for (int i = 0; i < 20; i++) data.increment(); }, "C").start();
new Thread(() -> {
for (int i = 0; i < 20; i++) data.decrement(); }, "D").start();
while (num != 0) {
/**
* @author :zsy
* @date :Created 2021/4/14 20:45
* @description:Lock锁生产者消费者
*/
public class B {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 20; i++) data.increment(); }, "A").start();
new Thread(() -> {
for (int i = 0; i < 20; i++) data.decrement(); }, "B").start();
new Thread(() -> {
for (int i = 0; i < 20; i++) data.increment(); }, "C").start();
new Thread(() -> {
for (int i = 0; i < 20; i++) data.decrement(); }, "D").start();
}
static class Data {
int num = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try {
while (num != 0) {
//等待
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//业务
num++;
System.out.println(Thread.currentThread().getName() + "--->" + num);
//唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
while(num == 0) {
//等待
condition.await();
}
//业务
num--;
System.out.println(Thread.currentThread().getName() + "--->" + num);
//唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
/**
* @author :zsy
* @date :Created 2021/4/14 21:12
* @description:多个线程按顺序打印
*/
public class C {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) data.printA();}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) data.printB();}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) data.printC();}, "C").start();
}
static class Data {
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void printA() {
lock.lock();
try {
if (num != 1) {
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "---->" + num);
num = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
if (num != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "---->" + num);
num = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
if (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "---->" + num);
num = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
在编译的字节码中加入了两条指令来进行代码的同步
monitorenter
每个对象有一个监视器锁(monitor)。当monitor被占用时,就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
monitorexit
指令执行时,monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
锁升级
在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。
但是,随着 Java SE 1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁()优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。
锁只能升级,不能降级。
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在JVM层面 | 是Java类 |
锁的释放 | 1、获取锁的线程执行完毕,释放锁2、线程执行出现异常,JVM会让线程释放锁 | finally必须释放锁,不然容易造成死锁 |
锁获取方式 | 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去 | Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了 lock.tryLock(); |
锁类型 | 可重入、不可中断、非公平 | 可重入、可中断、可公平(两者皆可) |
使用区别 | synchronized锁适合代码少量的同步问题 | Lock锁适合大量同步的代码的同步问题 |
能否判断获取锁的状态 | 否 | Lock可以判断是否获取到锁 |
锁的对象 | 代码块、方法、类 | 代码块 |
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中markword。
/**
* @author :zsy
* @date :Created 2021/4/15 15:03
* @description:CopyOnWriteList
*/
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 4));
System.out.println(Thread.currentThread().getName() + "---->" + list);
}, String.valueOf(i)).start();
}
}
}
报异常:ConcurrentModificationException(并发修改异常)
List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
所以我们可以看出,如果是读操作十分频繁的话,那么多线程下使用CopyOnWriteArrayList的性能基本上跟ArrayList差不多了。但如果是写操作十分频繁的话,建议还是不要使用CopyOnWriteArrayList了,因为它会造成数组的不断扩容及复制,十分耗性能。这其实就跟我们数据库读写分离的原理是一样的,如果写操作很多的话,那么主从库就会不断的执行复制操作,消耗性能。但如果是读操作多的话,由于该库只用于读,所以不会发生数据库事务锁,效率就会比一般的单库查询快很多。
/**
* @author :zsy
* @date :Created 2021/4/15 17:02
* @description:Set集合
*/
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
set.add(UUID.randomUUID().toString().substring(0, 4));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
ConcurrentModificationException
Set<String> set = Collections.synchronizedList(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
CopyOnWriteArraySet底层是一个CopyOnWriteArrayList
/**
* @author :zsy
* @date :Created 2021/4/15 17:50
* @description:HashMap
*/
public class MapTest {
public static void main(String[] args) {
Map<String, String>map = new HashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 4));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
ConcurrentModificationException
Map<String, String>map = Collections.synchronizedMap(new HashMap<>());
Map<String, String>map = new ConcurrentHashMap<>();
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();//这里如果key == null 会抛出异常
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
rehash 扩容
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
Hashtable扩容,原容量 * 2 +1 ,插入采用头插法
因为hashSeed ^ k.hashCode(); 的值可能是负数,如果直接index = hash % tab.length; 这样就会导致index可能为负数
int index = (hash & 0x7FFFFFFF) % tab.length;
以下内容引自《Java并发编程实战第五章》
同步容器类包括Vector和Hashtable,二者是早期JDK的一部分,此外还包括在JDK1.2中添加的一些功能相似的类,这些同步的封装器类由Collections.synchronizedXxx工厂方法创建,这些类实现线程安全的方式是:将他们封装起来,并对每个共有方法都有同步,每次只有一个线程能访问容器状态。
同步容器类都是线程安全的,但是在某些情况下可能需要额外的客户端加锁来保护复合操作。容器上常见的符合操作包括:迭代(反复访问元素,直到遍历完容器的所有元素)、跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算,例如:若没有则添加(在Map中是否存在键值K,如果没有,则加入二元组(K,V))。在同步容器类中,这些复和操作在没有客户端加锁的情况下仍是线程安全的,但当其他线程并发地修改容器时,他们可能会表现出意料之外的行为。
/**
* @author :zsy
* @date :Created 2021/4/24 16:48
* @description:测试Vector的线程安全性
*/
public class TestVector {
public static void main(String[] args) {
Vector vector = new Vector();
vector.add(1);
vector.add(2);
new Thread(() -> System.out.println(getLast(vector)),
new Thread(() -> deleteLast(vector), "B").start();
}
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return list.get(lastIndex);
}
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
运行上面程序会抛出Exception in thread “A” java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1异常
分析抛出异常的原因
如果线程A在包含2个元素的Vector调用getLast,同时线程B在同一个Vector上调用deleteLast,这些操作的交替执行如图,getLast会抛出ArrayIndexOutOfBoundsException异常。
由于并发容器类要遵守同步策略,即支持客户端加锁,因此可能创建一些新的操作,只要我们知道应该使用哪一个锁,那么新操作就与容器的其他操作一样都是原子操作。同步容器类会保护它的每个方法。
public static Object getLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
同理所有对共享容器进行迭代的地方都需要加锁。实际情况要更加复杂,因为在某些情况,迭代器会隐藏起来,操作不放会抛出ConcurrentModificationException
同步容器将所有对容器的访问都串行化,以实现他们的线程安全性,这种方法的代价是严重降低并发性,当多个线程竞争访问容器的锁时,吞吐量将会严重减低。
针对多个线程设计的,用并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。 如ConcurrentHashMap,CopyOnWriteArrayList等。并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助
A CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。 这是一个一次性的现象 - 计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier
/**
* @author :zsy
* @date :Created 2021/4/20 19:50
* @description:减法计数器
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "---->" + "GO");
countDownLatch.countDown(); //计数器 - 1
}, String.valueOf(i)).start();
}
countDownLatch.await();//等待countDownLatch归零,执行下面的操作
System.out.println("Close door");
}
}
原理:每次调用countDown()数量-1,假设计数器为零,countDownLatch.await()就会被唤醒,继续执行。
A CountDownLatch是一种通用的同步工具,可用于多种用途。 一个CountDownLatch为一个计数的CountDownLatch用作一个简单的开/关锁存器,或者门:所有线程调用await在门口等待,直到被调用countDown()的线程打开。 一个CountDownLatch初始化N可以用来做一个线程等待,直到N个线程完成某项操作,或某些动作已经完成N次。
CountDownLatch一个有用的属性是,它不要求调用countDown线程等待计数到达零之前继续,它只是阻止任何线程通过await ,直到所有线程可以通过。
用法
允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
/**
* @author :zsy
* @date :Created 2021/4/20 20:04
* @description:循环等待屏障
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, new Thread(() -> {
System.out.println("召唤神龙");
}));
for (int i = 0; i < 7; i++) {
final int tmp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "获得第" + tmp + "颗龙珠");
try {
cyclicBarrier.await(); //当等待的类数量达到时,才会执行,否则一直等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
作用:控制当前线程数量达到一定数量后,一起执行某项操作
CyclicBarrier对失败的同步尝试使用all-or-none断裂模型:如果线程由于中断,故障或超时而过早离开障碍点,那么在该障碍点等待的所有其他线程也将通过BrokenBarrierException (或InterruptedException)异常离开。
区别 | CountDownLatch | CyclicBarrier |
---|---|---|
作用 | 一般用于某个线程A等待若干个其他线程完成任务后,他才执行,强调多个线程完成某件事情 | 一般用于一组线程相互等待至某个状态,然后一组线程同时执行,强调多个线程互等,等大家都完成,再携手共进 |
对当前线程的影响 | 调用CountDownLatch的countDown方法会不导致当前线程阻塞,会继续执行下去 | 调用CyclicBarrier的await方法后,会阻塞当前线程(里面使用了ReentrantLock锁),直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行 |
原理 | 利用继承AQS的共享锁来进行线程的通知 | 利用ReentrantLock的Condition来阻塞和通知线程 |
难易程度 | CountDownLatch方法比较少,操作比较简单 | CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能 |
是否可以复用 | 否 | 是 |
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。
信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
/**
* @author :zsy
* @date :Created 2021/4/20 20:32
* @description:信号量
*/
public class SemaphoreDemo {
public static void main(String[] args) {
//指定只有3个信号量
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire();//获取当前的信号量
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获得");
//doSomething()
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "离开");
semaphore.release();//释放当前信号量
}, String.valueOf(i)).start();
}
}
}
作用
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。
Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
此类的构造函数可选择接受公平参数。 当设置为false时,此类不会保证线程获取许可的顺序。 特别是, 闯入是允许的,也就是说,一个线程调用acquire()可以提前已经等待线程分配的许可证-在等待线程队列的头部逻辑新的线程将自己。 当公平设置为真时,信号量保证调用acquire方法的线程被选择以按照它们调用这些方法的顺序获得许可(先进先出; FIFO)。 请注意,FIFO排序必须适用于这些方法中的特定内部执行点。 因此,一个线程可以在另一个线程之前调用acquire ,但是在另一个线程之后到达排序点,并且类似地从方法返回。 另请注意, 未定义的tryAcquire方法不符合公平性设置,但将采取任何可用的许可证。
A ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。 read lock可以由多个阅读器线程同时进行,只要没有作者。 write lock是独家的。
/**
* @author :zsy
* @date :Created 2021/4/20 21:33
* @description:读写锁
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 5; i++) {
final String tmp = String.valueOf(i);
new Thread(() -> {
myCache.put(tmp, "a");
}, String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final String tmp = String.valueOf(i);
new Thread(() -> {
myCache.get(tmp);
}, String.valueOf(i)).start();
}
}
}
class MyCache {
private Map<String, String > map = new HashMap<>();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void put(String key, String value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始写");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public String get(String key) {
String val = null;
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始读");
val = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return val;
}
}
1开始写
1写完成
0开始写
0写完成
2开始写
2写完成
3开始写
3写完成
4开始写
4写完成
0开始读
0读完成
2开始读
3开始读
3读完成
1开始读
4开始读
4读完成
1读完成
2读完成
首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。
ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
方式 | 抛出异常 | 不抛出异常,有返回值 | 阻塞一直等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(“c”,2, TimeUnit.SECONDS) |
删除 | remove | poll | take | poll(2,TimeUnit.SECONDS) |
判断对列首元素 | element | peek | - | - |
/**
* @author :zsy
* @date :Created 2021/4/21 15:41
* @description:阻塞队列
*/
public class bq {
public static void main(String[] args) throws InterruptedException {
test04();
}
/**
* 抛出异常
*/
public static void test01() {
BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
blockingQueue.add("a");
blockingQueue.add("b");
//抛出异常IllegalStateException
//blockingQueue.add("c");
System.out.println(blockingQueue.element());
blockingQueue.remove();
blockingQueue.remove();
//抛出异常NoSuchElementException
//blockingQueue.remove();
}
/**
* 不抛出异常,有返回值
*/
public static void test02() {
BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
blockingQueue.peek();
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//blockingQueue.poll();
}
/**
* 阻塞一直等待
*/
public static void test03() throws InterruptedException {
BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
blockingQueue.take();
blockingQueue.take();
//blockingQueue.remove();
}
/**
* 超时等待
* @throws InterruptedException
*/
public static void test04() throws InterruptedException {
BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
blockingQueue.offer("c",2, TimeUnit.SECONDS);
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
blockingQueue.poll(2,TimeUnit.SECONDS);
}
}
同步队列没有任何内部容量,甚至没有容量,每个插入操作必须等待另一个线程响应的删除操作
/**
* @author :zsy
* @date :Created 2021/4/21 16:37
* @description:同步队列
*/
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "添加元素" + i);
blockingQueue.put("a" + i);
System.out.println(Thread.currentThread().getName() + "添加成功" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "删除元素");
System.out.println(blockingQueue.poll());
System.out.println(Thread.currentThread().getName() + "删除成功");
}
}, "B").start();
}
}
A添加元素0
B删除元素
a0
B删除成功
A添加成功0
A添加元素1
B删除元素
a1
B删除成功
A添加成功1
A添加元素2
B删除元素
a2
B删除成功
A添加成功2
池化技术
程序运行的本质:占用系统的资源,优化资源的使用 => 池化技术
线程池的好处
/**
* @author :zsy
* @date :Created 2021/4/21 17:05
* @description:线程池的三大方法
*/
public class Demo1 {
public static void main(String[] args) {
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//创建一个线程
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建指定数量的线程
ExecutorService threadPool = Executors.newCachedThreadPool();//创建足够多的线程
try {
for (int i = 0; i < 10; i++) {
final int tmp = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "----->" + tmp);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
newScheduledThreadPool 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
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;
}
创建的线程池最大包含请求数量 = maximumPoolSize + workQueue的长度。超过最大请求数量,就会采取拒绝策略
/**
* @author :zsy
* @date :Created 2021/4/21 17:50
* @description:自定义线程池
*/
public class Demo2 {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
new ThreadPoolExecutor.AbortPolicy());
//线程池最大容量为5 + 3 = 8
try {
for (int i = 0; i < 8; i++) {
final int tmp = i;
threadPool.execute(new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "---->" + tmp);
}));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
最大线程池数量如何定义
1)当提交一个新任务到线程池时,线程池判断corePoolSize线程池是否都在执行任务,如果有空闲线程,则从核心线程池中取一个线程来执行任务,直到当前线程数等于corePoolSize;
2)如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
3)如果阻塞队列满了,那就创建新的线程执行当前任务,直到线程池中的线程数达到maxPoolSize,这时再有任务来,由饱和策略来处理提交的任务
只有一个方法的接口
输入T类型返回R类型
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
实例
/**
* @author :zsy
* @date :Created 2021/4/21 19:26
* @description:函数式接口
*/
public class Demo1 {
public static void main(String[] args) {
Function<String, String> function = str -> {
return str;};
System.out.println(function.apply("zhangsna"));
}
}
输入T类型返回boolean类型
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
实例
/**
* @author :zsy
* @date :Created 2021/4/21 19:29
* @description:断定性接口
*/
public class Demo2 {
public static void main(String[] args) {
Predicate<String> predicate = str -> {
return str.isEmpty();};
predicate.test("zhangsan");
}
}
只要输入类型,没有返回类型
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
只有返回类型,没有输入类型
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
实例
/**
* @author :zsy
* @date :Created 2021/4/21 19:37
* @description:消费型接口和供给型接口
*/
public class Demo3 {
public static void main(String[] args) {
Consumer<String> consumer = o -> System.out.println(o);
Supplier<Integer> supplier = () -> 1024;
}
}
大数据 : 存储 + 计算
存储交给集合、数据库
计算交给流
/**
* @author :zsy
* @date :Created 2021/4/21 20:05
* @description:流操作
*/
public class Demo1 {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
userList.add(new User("a", 10,30.0));
userList.add(new User("b", 11,25.0));
userList.add(new User("c", 12,40.0));
userList.add(new User("d", 14,31.0));
userList.add(new User("e", 16,35.0));
//找出年龄大于11成绩大于33的用户倒序的第一位名字的大写
userList.stream()
.filter(u -> {
return u.getAge() > 11;})
.filter(u -> {
return u.getPoint() > 33;})
.sorted((u1, u2) -> {
return u2.getUsername().compareTo(u1.getUsername());})
.limit(1)
.map(user -> {
return user.getUsername().toUpperCase();})
.forEach(System.out :: println);
}
}
参考:JAVA Future类详解
Future模式是多线程开发中常见的设计模式,它的核心思想就是异步调用,他无法立即返回你需要的数据,但是他会返回一个契约,将来你可以凭借这个契约去获取你需要的信息。例如:点外卖,外卖订单相当于契约,你可以通过外面订单获取到你的外卖,而制作外卖是一个很长的过程,无法立刻获得外卖。
同步方法
客户端发出获取数据的请求,尽管请求需要很长一段时间才能返回,但是同步模式规定客户端一直等到到数据返回后才进行后续任务
首先,JDK内部有一个Future接口,这就是类似前面提到的订单,当然了,作为一个完整的商业化产品,这里的Future的功能更加丰富了,除了get()方法来获得真实数据以外,还提供一组辅助方法,比如:
FutureTask表示一个异步运算任务,FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果尚未完成get方法将阻塞,一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
什么是ForkJoin
并发执行任务,提高效率,大数据量
/**
* @author :zsy
* @date :Created 2021/4/22 17:10
* @description:ForkJoin
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
private final Long tmp = 1000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start > tmp) {
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
Long mid = start + end >> 1;
ForkJoinDemo f1 = new ForkJoinDemo(start, mid);
ForkJoinDemo f2 = new ForkJoinDemo(mid + 1, end);
return f1.join() + f2.join();
}
}
public static void main(String[] args) {
test1();
}
public static void test1() {
long start = System.currentTimeMillis();
Long sum = (0 + 10_0000_0000) * 10_0000_0000L / 2;
long end = System.currentTimeMillis();
System.out.println(end - start + "ms : " + sum);
}
public static void test2() {
long start = System.currentTimeMillis();
Long sum = 0L;
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask submit = forkJoinPool.submit(forkJoinDemo);
try {
sum = (Long) submit.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end - start + "ms : " + sum);
}
public static void test3() {
long start = System.currentTimeMillis();
Long sum = 0L;
sum = LongStream.rangeClosed(0,10_0000_0000L).parallel().reduce(0, Long :: sum);
long end = System.currentTimeMillis();
System.out.println(end - start + "ms : " + sum);
}
}
Future模式虽然好用,但也有一个问题,那就是将任务提交给线程后,调用线程并不知道这个任务什么时候执行完,如果执行调用get()方法或者isDone()方法判断,可能会进行不必要的等待,那么系统的吞吐量很难提高。
为了解决这个问题,JDK对Future模式又进行了加强,创建了一个CompletableFuture,它可以理解为Future模式的升级版本,它最大的作用是提供了一个回调机制,可以在任务完成后,自动回调一些后续的处理,这样,整个程序可以把“结果等待”完全给移除了。
开辟一个新的线程去执行异步任务,如果需要获取返回值,那么主线程将进入阻塞状态
异步调用线程
/**
* @author :zsy
* @date :Created 2021/4/24 11:56
* @description:
*/
public class CompletableDemo {
public static void main(String[] args) {
//创建异步任务
CompletableFuture.supplyAsync(CompletableDemo::getPrice)
//当getPrice()执行完后会回调当前函数
.thenAccept(result -> {
System.out.println("price = " + result);
})
//出现异常后,会回调exceptionally
.exceptionally(e -> {
System.out.println(e.getMessage());
return null;
});
System.out.println(1111);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2222);
}
public static Double getPrice() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (Math.random() < 0.3) {
throw new RuntimeException("Error");
}
return Math.random() * 20;
}
}
在这个例子中,首先以getPrice()为基础创建一个异步调用,接着,使用thenAccept()方法,设置了一个后续的操作,也就是当getPrice()执行完成后的后续处理。
不难看到,CompletableFuture比一般的Future更具有实用性,因为它可以在Future执行成功后,自动回调进行下一步的操作,因此整个程序不会有任何阻塞的地方(也就是说你不用去到处等待Future的执行,而是让Future执行成功后,自动来告诉你)。
以上面的代码为例,CompletableFuture之所有会有那么神奇的功能,完全得益于AsyncSupply类(由上述代码中的supplyAsync()方法创建)。
AsyncSupply在执行时,如下所示:
public void run() {
CompletableFuture<T> d; Supplier<T> f;
if ((d = dep) != null && (f = fn) != null) {
dep = null; fn = null;
if (d.result == null) {
try {
//这里就是你要执行的异步方法
//结果会被保存下来,放到d.result字段中
d.completeValue(f.get());
} catch (Throwable ex) {
d.completeThrowable(ex);
}
}
//执行成功了,进行后续处理,在这个后续处理中,就会调用thenAccept()中的消费者
//这里就相当于Future完成后的通知
d.postComplete();
}
}
继续看d.postComplete(),这里会调用后续一系列操作
final void postComplete() {
//省略部分代码,重点在tryFire()里
//在tryFire()里,真正触发了后续的调用,也就是thenAccept()中的部分
f = (d = h.tryFire(NESTED)) == null ? this : d;
}
}
}
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
(1)不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
(2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
(4)一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
(5)一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
(7)如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
其中一个重要的复杂性来源是绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是很难消除的(无法仅靠寄存器来完成所有运算任务)。早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
缓存一致性问题
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他硬件和编译器优化
JMM的核心是找到一个平衡性,在保证内存可见性的前提下,尽量放松对编译器和处理器的重排序限制
JVM中,Java内存模型将内存分为了两部分:线程栈区和堆区,JVM中运行的每个线程都有自己的线程栈,线程栈包含了当前线程执行的方法调用和相关信息,我们把它称作调用栈,调用栈不断变化。
重排序类型
不管如何重排序,都必须保证代码在单线程下运行正确,连单线程下都无法正确运行,更不用考虑多线程并发情况,所以提出了as-if-serial概念
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程的执行结果不能被改变。所以编译器不会对存在数据依赖关系的操作做重排序,因为重排序会改变执行结果(这里所说的数据依赖仅仅针对单个处理器中执行的执行序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不能被编译器和处理器考虑)但是,如果操作之间不存在数据依赖关系,这些操作依然可能被编译器和处理器重排序。
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字。通常来说,使用volatile必须具备以下2个条件:
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
用happens-before的概念来阐述操作之间的内存可见性。如果一个操作的结果需要对另一个操作可见,那么两个操作之间必须要存在happens-before关系
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second) 。
单例模式的特点
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,天生是线程安全的。
/**
* @author :zsy
* @date :Created 2021/4/24 19:49
* @description:饿汉式
*/
public class Hungry {
//可能会浪费空间
private Hungry() {
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance() {
return HUNGRY;
}
}
/**
* @author :zsy
* @date :Created 2021/4/24 19:51
* @description:懒汉式
*/
public class LazyMan {
private LazyMan() {
}
private static LazyMan LAZY_MAN = null;
public static LazyMan getInstance() {
if (LAZY_MAN == null) {
LAZY_MAN = new LazyMan();
}
return LAZY_MAN;
}
}
/**
* @author :zsy
* @date :Created 2021/4/24 19:51
* @description:懒汉式
*/
public class LazyMan {
private LazyMan() {
}
private static LazyMan LAZY_MAN = null;
public static synchronized LazyMan getInstance() {
if (LAZY_MAN == null) {
LAZY_MAN = new LazyMan();
}
return LAZY_MAN;
}
}
这种写法是对getInstance()加了锁的处理,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,为了改进这种写法,就有了下面的双重检查懒汉式。
存在问题多线程下调用getInstance(),会产生错误
//双重检测锁模式的懒汉式单例 DCL懒汉式
public static LazyMan getInstance() {
if (LAZY_MAN == null) {
synchronized (LazyMan.class) {
if (LAZY_MAN == null) {
LAZY_MAN = new LazyMan();
}
}
}
return LAZY_MAN;
}
这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第三种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。
分析为什么需要volatile关键字
通过双重检测锁模式的懒汉式单例模式多线程下仍然存在问题,因为 LAZY_MAN = new LazyMan();不是一个原子操作,而是分为三个步骤:
下图由于指令重拍,而发生了线程B拿到了LAZY_MAN对象,但是LAZY_MAN为null所以需要将LAZY_MAN设置为volatile
private volatile static LazyMan LAZY_MAN = new LazyMan();
存在问题:通过反射机制,打破封装仍然可以创建对象
public static void main(String[] args) throws Exception {
LazyMan instance1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
改进措施
private LazyMan() {
if (LAZY_MAN != null) {
throw new RuntimeException("使用反射破坏异常");
}
}
存在问题:可以通过两次反射来创建对象
这次在构造方法加一个信号变量,只要创建了一次实例,就让信号变量置为true,以后再来调用直接抛出异常
private static boolean isBuild = false;
private LazyMan() {
if (!isBuild) {
isBuild = true;
} else {
throw new RuntimeException("使用反射破坏异常");
}
}
当然这需要对信号变量进行加密,要不然仍然可以通过反射拿到信号变量。
public static void main(String[] args) throws Exception {
//LazyMan instance1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
//通过反射拿到信号变量
Field isBuild = LazyMan.class.getDeclaredField("isBuild");
isBuild.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
//修改信号变量
isBuild.set(instance1,false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
完整代码
public class LazyMan {
private static boolean isBuild = false;
private volatile static LazyMan INSTANCE;
private LazyMan() {
if (isBuild == false) {
isBuild = true;
} else {
throw new RuntimeException("使用反射破坏异常");
}
}
private volatile static LazyMan LAZY_MAN;
//双重检测锁模式的懒汉式单例 DCL懒汉式
public static LazyMan getInstance() {
if (LAZY_MAN == null) {
synchronized (LazyMan.class) {
if (LAZY_MAN == null) {
LAZY_MAN = new LazyMan();
isBuild = true;
}
}
}
return LAZY_MAN;
}
}
/**
* @author :zsy
* @date :Created 2021/4/25 8:33
* @description:静态内部类
*/
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。
不能使用反射破坏枚举
/**
* @author :zsy
* @date :Created 2021/4/24 20:24
* @description:枚举单例
*/
//enum本身也是一个Class
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.getInstance();
EnumSingle instance2 = EnumSingle.getInstance();
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingle enumSingle = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(enumSingle);
}
}
以上代码是通过反射创建枚举的实例抛出异常Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
饿汉式:在类加载的时候就完成了初始化,所以类加载比较慢,但是获取对象的速度快
懒汉式:在类加载的时候不初始化,等到第一次使用时才初始化
优点:
缺点:单例模式一般没有接口,扩展的话只能修改代码
CAS全称compareAndSwapObject,即比较替换,是实现并发应用到的一种技术。compareAndSwapObject 方法其实比较的就是两个 Java Object 的地址,如果相等则将新的地址(Java Object)赋给该字段。
CAS操作是一个原子操作,由一条CPU指令完成(CPU的并发原语),使用了三个基本操作数,内存地址V,旧的预期值A,要修改的新值B
Java Unsafe包中的compareAndSwapObject()方法
比较并且交换Java Object
/***
* Compares the value of the object field at the specified offset
* in the supplied object with the given expected value, and updates
* it if they match. The operation of this method should be atomic,
* thus providing an uninterruptible way of updating an object field.
*
* @param obj the object containing the field to modify.
* @param offset the offset of the object field within obj
.
* @param expect the expected value of the field.
* @param update the new value of the field if it equals expect
.
* @return true if the field was changed.
*/
public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);
CAS该方法的作用是原子操作比较并交换两个值,运用时底层硬件所提供的CAS支持,在Java API中该方法的四个参数
/**
* @author :zsy
* @date :Created 2021/4/18 9:16
* @description:CAS测试
*/
public class UnsafeTest {
private static Unsafe UNSAFE = null;
private static long I_OFFSET;
static {
//Unsafe unsafe = Unsafe.getUnsafe();
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
I_OFFSET = UNSAFE.objectFieldOffset(Person.class.getDeclaredField("i"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Person person = new Person();
new Thread(() -> {
while (true) {
//person.i++;
boolean b = UNSAFE.compareAndSwapInt(person, I_OFFSET, person.i, person.i + 1);
if (b) {
System.out.println(Thread.currentThread().getName() + UNSAFE.getIntVolatile(person, I_OFFSET));
//System.out.println(Thread.currentThread().getName() + person.i);
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
while (true) {
//person.i++;
boolean b = UNSAFE.compareAndSwapInt(person, I_OFFSET, person.i, person.i + 1);
if (b) {
System.out.println(Thread.currentThread().getName() + UNSAFE.getIntVolatile(person, I_OFFSET));
//System.out.println(Thread.currentThread().getName() + person.i);
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
class Person {
public int i;
}
乐观锁中详细介绍了ABA问题
/**
* @author :zsy
* @date :Created 2021/4/24 21:45
* @description:ABA问题
*/
public class ABADemo {
private static AtomicInteger num = new AtomicInteger(1);
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "---->" + num.get());
}, "A").start();
new Thread(() -> {
num.compareAndSet(1, 21);
System.out.println(Thread.currentThread().getName() + "修改了num:" + num.get());
}, "B").start();
new Thread(() -> {
num.compareAndSet(21, 1);
System.out.println(Thread.currentThread().getName() + "修改了num:" + num.get());
}, "C").start();
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---->" + num.get());
}, "A").start();
}
}
A---->1
B修改了num:21
C修改了num:1
A---->1
引入原子运用
/**
* @author :zsy
* @date :Created 2021/4/24 21:45
* @description:ABA问题
*/
public class ABADemo {
static AtomicStampedReference<Integer> num = new AtomicStampedReference<Integer>(1,1);
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "版本号" + num.getStamp() + "---->" + num.getReference());
}, "A").start();
new Thread(() -> {
try {
Thread.sleep(11);
} catch (InterruptedException e) {
e.printStackTrace();
}
num.compareAndSet(1, 21, num.getStamp(), num.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "版本号" + num.getStamp() + "修改了num:" + num.getReference());
}, "B").start();
new Thread(() -> {
try {
Thread.sleep(31);
} catch (InterruptedException e) {
e.printStackTrace();
}
num.compareAndSet(21, 1, num.getStamp(), num.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "版本号" + num.getStamp() + "修改了num:" + num.getReference());
}, "C").start();
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "版本号" + num.getStamp() + "---->" + num.getReference());
}, "A").start();
}
}
输出
A版本号1---->1
B版本号2修改了num:21
C版本号3修改了num:1
A版本号3---->1