多线池与高并发篇章一
多线池与高并发篇章一前言:上天:入地:1 进程,线程,协程,纤程1.1 怎么创建,启动一个线程?1.2 守护线程1.3 sleep方法和yield方法和join方法1.4 提问:有三个线程1,2,3,如何保证线程的执行完毕的顺序是3,2,1?
2 synchronized锁2.1 练习一:volatile和synchronized分别会对输入语句有什么影响呢?二:同步和非同步的方法是否允许同时调用?三:模拟银行业务 添加数据加锁 读数据不加锁。四:synchronized是可重入锁五:程序在执行过程中 如果出现异常,在默认情况下会释放锁
2.2. synchronized底层实现:2.2.1 分析:自旋锁和重量级锁的使用场景?
3 线程状态3.1 线程状态介绍3.2 sleep() 和 wait() 有什么区别?3.3 线程停止3.4 线程优先级3.4.1 优先级priority3.4.2 join()方法3.4.3 yield方法
4 多线程并发的3个特性4.1 原子性4.2 可见性4.3 有序性
5 Java内存可见性5.1 JVM内存结构5.2 Java对象模型5.3 java内存模型
多线池与高并发篇章一
前言:
上天:
高并发,缓存,大流量,大数据量
入地:
JVM,OS,算法,线程,IO
1 进程,线程,协程,纤程
在你的电脑上面有一个程序是QQ.exe,当你双击启动时,这个时候你可以看做一个进程启动了,而当你在发送消息时,接到了对方的消息,这个时候你可以理解成多个线程,进程里面最小的执行单元是线程。
1.1 怎么创建,启动一个线程?
第一种是定义一个类,继承Thread,重写run方法。Thread最终还是的实现Runnable接口
第二种是定义一个类,实现Runnable,重写run方法。启动线程得先new Thread(new 对象).start。
.start()方法【调用start方法是main线程执行,第二个线程也执行】。
第三种写法通过Lambda表达式。
第四种是创建Callable接口的实现类,实现call方法,使用FutureTask类包装Callable实现类的对象。
实现Runnable接口比继承Thread类所具有的优势:
1. 适合多个相同的程序代码的线程去共享同一个资源。
2. 可以避免java中的单继承的局限性。
3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数据独立。
4. 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
public class Test02 {
public static void main(String
[] args
) throws ExecutionException
, InterruptedException
{
FutureTask
<Object> oneTask
= new FutureTask<>(new Tickes<>());
new Thread(oneTask
,"线程1").start();
System
.out
.println(oneTask
.get());
}
}
class Tickes<String> implements Callable<Object> {
@Override
public Object
call() throws Exception
{
for (int i
= 0; i
< 10; i
++) {
System
.out
.println(i
);
}
return Thread
.currentThread().getName() + "线程执行完毕";
}
}
1.2 守护线程
Java中有两种线程,一种是用户线程,另一种是守护线程。
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。
守护线程当进程不存在或主线程停止,守护线程也会被停止。
// 设置线程为守护线程
thread.setDaemon(true);
1.3 sleep方法和yield方法和join方法
sleep方法会让当前线程睡眠一段时间,并让 其他线程去执行
yield方法会让当前线程离开 ,回到等待队列中,有可能会再次执行(回到就绪状态)相当于抢夺CPU
join方法 等到另外一个线程结束,举例:有线程A,线程B。当线程A在执行的过程中调用B.join方法时,
会把线程A停止,同时让线程B运行起来,直到线程B执行完毕,才会接着执行线程A
1.4 提问:有三个线程1,2,3,如何保证线程的执行完毕的顺序是3,2,1?
略
2 synchronized锁
多个线程访问同一个资源时,需要一把锁来保证资源的内存可见性。
一个资源被上锁时,锁的是什么?
千万被说锁的是资源。我们可以理解为synchronized锁,锁的是任意东西,
举例:synchronized锁里有个对象,理解为对象锁,当线程拿到这个对象锁,才可以去访问资源。
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个线程执行synchronized声明的代码块。
还可以保证共享变量的内存可见性。同一时刻只有一个线程执行,这部分代码块的重排序也不会影响其执行结果。
也就是说使用了synchronized可以保证并发的原子性,可见性,有序性。
synchronized实现可见性的过程
1. 获得互斥锁(同步获取锁)
2. 清空本地内存
3. 从主内存拷贝变量的最新副本到本地内存
4. 执行代码
5. 将更改后的共享变量的值刷新到主内存
6. 释放互斥锁
2.1 练习
一:volatile和synchronized分别会对输入语句有什么影响呢?
public class Demo02 implements Runnable {
private int count
= 100;
@Override
public void run() {
count
--;
System
.out
.println(Thread
.currentThread().getName()+"执行了,count=:"+count
);
}
public static void main(String
[] args
) {
Demo02 demo02
= new Demo02();
for (int i
= 0; i
< 100; i
++) {
new Thread(demo02
,"线程"+i
).start();
}
}
}
二:同步和非同步的方法是否允许同时调用?
public class Demo03 {
public synchronized void m1() {
System
.out
.println(Thread
.currentThread().getName() + ":start...");
try {
Thread
.sleep(10000);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
System
.out
.println(Thread
.currentThread().getName() + ":end...");
}
public void m2() {
try {
Thread
.sleep(5000);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
System
.out
.println(Thread
.currentThread().getName() + ":start");
}
public static void main(String
[] args
) {
Demo03 demo03
= new Demo03();
new Thread(demo03
::m1
,"m1").start();
new Thread(demo03
::m2
,"m2").start();
}
}
答案:允许
三:模拟银行业务 添加数据加锁 读数据不加锁。
public class Account {
String name
;
double money
;
public synchronized void set(String name
, double money
) {
this.name
= name
;
try {
Thread
.sleep(2000);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
this.money
= money
;
}
public double getMoney(String name
) {
return this.money
;
}
public static void main(String
[] args
) {
Account account
= new Account();
new Thread(()->account
.set("zhangsan",100.0)).start();
try {
TimeUnit
.SECONDS
.sleep(1);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
System
.out
.println(account
.getMoney("zhangsan"));
try {
TimeUnit
.SECONDS
.sleep(2);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
System
.out
.println(account
.getMoney("zhangsan"));
}
}
四:synchronized是可重入锁
/**
* @description: 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象锁,再次申请时还会得到该对象锁
* @author: 大佬味的小男孩
* @date: 2020-08-08 11:07
**/
public class Demo05 {
public synchronized void m1(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end ...");
}
public synchronized void m2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2 end ...");
}
public static void main(String[] args) {
Demo05 demo05 = new Demo05();
new Thread(()->demo05.m1()).start();
}
}
原因分析:线程在执行m1时,会得到一个对象锁,在一个同步方法里面,调用另外一个同步方法,其实用的还是同一把锁
在继承中能具体体现:子类调用父类的同步方法
public class T {
public synchronized void m(){
System
.out
.println("m start...");
try {
Thread
.sleep(1000);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
System
.out
.println("m end...");
}
static class TT extends T{
@Override
public synchronized void m() {
System
.out
.println("child m start...");
super.m();
System
.out
.println("child m end...");
}
}
public static void main(String
[] args
) {
TT tt
= new TT();
new Thread(new Runnable() {
@Override
public void run() {
tt
.m();
}
}, "t1").start();
}
}
五:程序在执行过程中 如果出现异常,在默认情况下会释放锁
public class Demo07 {
int count
= 0 ;
public synchronized void m(){
System
.out
.println(Thread
.currentThread().getName()+"start...");
while (true){
count
++;
System
.out
.println(Thread
.currentThread().getName()+"把count修改成:"+count
);
try {
TimeUnit
.SECONDS
.sleep(1);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
if(count
== 5){
int i
= 1/0;
System
.out
.println(i
);
}
}
}
public static void main(String
[] args
) {
Demo07 demo07
= new Demo07();
new Thread(new Runnable() {
@Override
public void run() {
demo07
.m();
}
}, "t1").start();
try {
TimeUnit
.SECONDS
.sleep(1);
} catch (InterruptedException e
) {
e
.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
demo07
.m();
}
}, "t2").start();
}
}
2.2. synchronized底层实现:
一:在早期 synchronized是一个重量级锁,在后来的改进中,有锁升级的概念,
详情可以百度(我就是厕所所长)
二:在synchronized(Object)的代码块中,当线程第一次访问,markword 记录这个线程的id(偏向锁)。
解释:在线程第一次访问的时候,会记录该线程的id,当下次这个线程访问时,一看还是这个id,
就会偏向这个线程id,就会放行,而其他线程过来就必须乖乖的等着,所以效率高,基本上是一个线程来执行。
三:如果出现了线程争用:升级为自旋锁。
解释:当一个哥们占用一个马桶时,这时来了另外一个哥们等着,
第二个哥们会用一个类似where(true)的循环在马桶旁边转圈圈(大量占用资源,必须打死),
结果发现第一个哥们儿还是没从马桶离开。(默认自旋锁是循环10次,一个哥们占着马桶,另个哥们循环10次后,如果还得不到马桶)
这个时候锁会再次升级为重量级锁(去操作系统申请资源)
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
1. 普通同步方法,锁是当前实例对象this
2. 静态同步方法,锁是当前类的class对象
3. 同步方法块,锁是括号里面的对象
当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁。
synchronized的同步操作主要是monitorenter和monitorexit这两个jvm指令实现的。
有了synchronized改进后,偏向,自旋,重量级,但是有人会说synchronized很慢很慢,但是在实际开发中synchronized和其他锁对比,不是很慢哦!
2.2.1 分析:自旋锁和重量级锁的使用场景?
在很多锁中大多数用的是自旋锁,原因是什么呢?
解释:自旋锁虽然占用大量CPU资源,但是不去访问操作系统,所以呢它是在用户态解决问题,不经过内核态。因此自旋锁的加锁和解锁效率高。
答案:首先两个锁对比一下,自旋锁的好处我就不再陈述了,重量级锁的好处是会去访问操作系统,其他线程会进入等待队列。
1.执行时间短并且线程数量不能太多建议用自旋锁
(假设一个线程在执行,另外999个线程在自旋,我就想问CPU顶得住嘛)。
2.执行时间长并且线程数量太多建议用系统锁,
3 线程状态
3.1 线程状态介绍
查看Thread源码,能够看到java的线程有六种状态:
public enum State
{
NEW
,
RUNNABLE
,
BLOCKED
,
WAITING
,
TIMED_WAITING
,
TERMINATED
;
}
NEW(新建) 线程刚被创建,但是并未启动。
RUNNABLE(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,
这取决于操作系统处理器。
BLOCKED(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,
则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。
进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TIMED_WAITING(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。
这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
TERMINATED(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
3.2 sleep() 和 wait() 有什么区别?
sleep()方法(休眠)是线程类Thread的静态方法,调用此方法会让当前线程暂停执行指定的时间,
将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。
wait()方法是Object类的方法,调用锁对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),
进入对象的等待池(wait pool),只有调用对象的 notify()方法 或 notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
3.3 线程停止
结束线程有以下三种方法:
(1)设置退出标志,使线程正常退出。
(2)使用interrupt()方法中断线程。
(3)使用stop方法强行终止线程
(不推荐使用Thread.stop, 这种终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
使用interrupt()方法来中断线程有两种情况:
1)线程处于阻塞状态
如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。
当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,
通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。
2)线程未处于阻塞状态
使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,
中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
3.4 线程优先级
3.4.1 优先级priority
在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。
3.4.2 join()方法
join作用是让其他线程变为等待。thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
3.4.3 yield方法
Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果) yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
4 多线程并发的3个特性
4.1 原子性
原子性: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作
突然中止。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
4.2 可见性
可见性: 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
当线程1执行 int i = 0 这句时, i 的初始值0加载到内存中,然后再执行 i = 10 ,那么在内存中 i 的值变为10了。
如果当线程1执行到 int i = 0 这句时,此时线程2执行 j = i,它读取 i 的值并加载到内存中,
注意此时内存当中i的值是0,那么就会使得 j 的值也为0,而不是10。
这就是可见性问题,线程1对变量 i 修改了之后,线程2没有立即看到线程1修改的值。
4.3 有序性
有序性: 程序执行的顺序按照代码的先后顺序执行
int count = 0;
boolean flag = false;
count = 1; //语句1
flag = true; //语句2
以上代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。
从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?
不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
什么是重排序?一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,
它不保证程序中各个语句的执行先后顺序同代码中的顺序一致。
as-if-serial:无论如何重排序,程序最终执行结果和代码顺序执行的结果是一致的。Java编译器、
运行时和处理器都会保证Java在单线程下遵循as-if-serial语意)。
上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,
语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,
那么它靠什么保证的呢?
再看下面一个例子:
int a = 10; //语句1
int b = 2; //语句2
a = a + 3; //语句3
b = a*a; //语句4
这段代码有4个语句,那么可能的一个执行顺序是: 语句2 语句1 语句3 语句4。不可能是这个执行顺序:
语句2 语句1 语句4 语句3。因为处理器在进行重排序时是会考虑指令之间的数据依赖性,
如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程会有影响。
下面看一个例子:
//线程1:
init = false
context = loadContext(); //语句1
init = true; //语句2
//线程2:
while(!init){//如果初始化未完成,等待
sleep();
}
execute(context);//初始化完成,执行逻辑
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,
在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,
去执行execute(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
5 Java内存可见性
5.1 JVM内存结构
我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。其中有些区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域结构如下:
JVM内存结构,由Java虚拟机规范定义。描述的是Java程序执行过程中,由JVM管理的不同数据区域。各个区域有其特定的功能。
5.2 Java对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
HotSpot虚拟机中(Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机),
设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,
而Klass用来描述对象实例的具体类型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个 instanceKlass 对象,保存在方法区,
用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,
JVM会创建一个instanceOopDesc 对象,这个对象中包含了对象头以及实例数据。
5.3 java内存模型
Java内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,
保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
有兴趣详细了解Java内存模型是什么,为什么要有Java内存模型,Java内存模型解决了什么问题的同学,
参考:https://www.hollischuang.com/archives/2550 。
Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。
他只是一个抽象的概念。JSR-133: Java Memory Model and Thread Specification中描述了,
JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,
在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。
JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。
JMM线程操作内存的基本的规则: 第一条关于线程与主内存:线程对共享变量的所有操作都必须在自己的工作内存(本地内存)中进行,不能直接从主内存中读写。 第二条关于线程间本地内存:不同线程之间无法直接访问其他线程本地内存中的变量,线程间变量值的传递需要经过主内存来完成。
主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,
不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、
静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。
本地内存
主要存储当前方法的所有本地变量信息(本地内存中存储着主内存中的变量副本拷贝),
每个线程只能访问自己的本地内存,即线程中的本地变量对其它线程是不可见的,
就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,
当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,
线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
先到这里结束吧!