本文Java高并发的内容将从三个阶段记录,参考资料【Java并发编程详解】:
相信学过操作系统的同学都知道线程和进程的关系,对于计算机来说一个任务就是一个进程,一个进程里面至少有一个线程。想必学习的时候会不会问,一个APP就对应一个进程,一个进程难道就是一个JVM吗?那经常写的函数是不是就是一个线程呢?
通常来说,一个APP是一个进程,但是也有可能多个进程。一个进程就是一个JVM
(虚拟机),里面有很多个线程运行,接下来就是操作系统的知识了。
每个线程都有自己的局部变量表、程序计数器以及生命周期,线程的生存状态分为以下五个主要的阶段:
new
创建一个Thread对象时,此时并不处于执行状态,因为没有调用start方法。此时只是一个对象的状态,就跟平常创建一个对象一样,当用start
方法时就会进入到`RUNNABLE``状态。阻塞状态
,就会进入BLOCKED状态。比如调用了sleep,或者wait方法而加入了waitSet中。比如为获得锁资源,而被进入阻塞队列中等待。最终状态
,在状态中线程不会切换到其他状态,线程进入TERMINATED状态,意味着该线程的整个生命周期都结束
了。比如线程正常结束任务,比如JVM crash,所有线程结束。在面试过程中,我们都会遇到这样的面试题,怎样创建一个线程?
答:
1)继承 Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executor框架
其实在JDK中代表线程的就只有Thread这个类,线程执行单元
就是run方法。所以创建线程
只有一种方式那就是构造Thread
类,而实现线程的执行单元则有两种方式,第一种就是重写Thread
的run方法,第二种实现runnable接口方法,并将Runnable实例
用作构造Thread的参数。
我们接入一个例子来引入上面的实例:假设一个银行的办事大厅,有三个柜台,每个柜台需要为顾客办理业务。顾客依次进门取号,等待办理,那么可以怎样模仿这个实例呢?
public class demo implements Runnable{
private int index = 1;//当前的顾客
private int MAX = 50;
@Override
public void run() {
while (index < MAX){
System.out.println(Thread.currentThread() + "的号码是:"+(index++));
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void main(String arg[]){//多线程测试hashmap 容易出现死循环
final demo runnable = new demo();
Thread thread_1 = new Thread(runnable,"线程一");
Thread thread_2 = new Thread(runnable,"线程一");
Thread thread_3 = new Thread(runnable,"线程一");
thread_1.start();
thread_2.start();
thread_3.start();
}
}
只贴部分输出情况,可以看到,某几个线程重复的index情况。
Thread[线程四,5,main]的号码是:17
Thread[线程二,5,main]的号码是:17
为什么会这样呢?
相信看过我的ConcurrentHashMap高并发讲解
的就知道,线程执行的时候会有缓存,还没等线程修改刷新至内存就被其他线程修改了,所以导致重复!那么怎么解决呢,一是定义static共享变量
,二是使用volatile
关键字强制刷新
到内存。
研究线程那么必然要懂得JVM的内存模型,以及各个内存区域之间的关系。JVM在执行Java程序的时候会把对应的物理内存划分为不同的区域,每一个区域都存放着不同的数据。
程序计数器
相信这个大家都不陌生,操作系统都需要通过控制总线向CPU发送机器指令,而程序计数器就是当前执行指令地址。每个线程都需要一块独立的程序计数器,因此内存区域是线程私有的。
Java虚拟栈
线程创建时,都会为其创建一个虚拟机栈,虚拟机栈大小可以用-xss
来配置。在线程中,方法执行会创建一个名为栈帧
的东西,主要用于存放局部变量表
、操作栈
、动态链接
等信息,其区域也是私有
。
本地方法栈
Java提供了调用本地方法
的接口(JNI Java Native Interface),也就是操作系统程序方法。大家都知道,JVM是运行在操作系统上的,而操作系统是最终的管理。在JVM中经常会使用JNI方法,比如网络通信,文件操作系统的底层,甚至String的intern
等都是JNI方法。
堆内存
堆内存是JVM中最大
的一块内存区域,被所有的线程共享
,Java创建的所有对象几乎都存在这里。该内存区域也会垃圾回收器经常照顾的对象,所以有时候被称为“GC堆”
。
方法区
方法区也是被多个线程共享
的内存区域,主要用于存储被虚拟机加载的类信息、常量(运行常量池)、静态变量即时编译器(JIT)编译后的代码等数据。
所以可以得出进程的内存大小为:堆内存+线程数量*栈内存
那么JVM中可以创建多少个线程呢,我们是可以通过公式算出来的。
线程数量=(最大内存空间-JVM堆内存-ReserverOsMemory)/ThreadStackSize(xss)
当然线程数量还跟操作系统的一些内核配置有很大的关系。
你知道JVM在什么情况下会退出吗?
我们说的是正常的退出,而不是调用System.exit()
。正常退出就需要理解守护线程,守护线程是一类比较特殊的线程,一般用于处理后台的一些工作,比如JDK的垃圾回收。若线程中没有非守护线程就会,则JVM的进程就会退出。
设置守护线程的方法就是thread.setDaemon(true)
,true就是代表守护线程,false就是正常线程。主要注意的就是,setDaemon方法只在线程启动之前才能失效,如果一个线程死亡,那么设置setDaemon
则会抛出IllegalThreadStateException
异常。
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(24);
TimeUnit.SECONDS.sleep(17);
TimeUnit.MILLISECONDS.sleep(88);
yield方法
是属于一种启发式
的方法,调用yield方法会使当前线程从RUNNING状态
切换到RUNNABLE状态
。总结:
1.sleep
会导致当前线程暂停指定
的时间,没有CPU的时间消耗。
2.yield只是对CPU调度器的一个提示
,如果CPU没有忽略这个提示,他会导致线程上下文切换
。
3.sleep会使线程短暂block
,会在给定的时间内释放CPU的资源
4.sleep会百分百的完成给定的时间消耗,而yield不一定担保
5.一个线程sleep另一个线程调用interrupt会捕获中断信号,而yield不会。
多线程里最重要的内容之一,那就是数据同步
、线程安全
、锁
等内容。在串行化任务执行时,由于不存在资源的共享,线程安全的问题几乎不用担心。但是现在都是追求高效率的执行,都需要满足多线程对共享资源的竞争。
在前面的例子中,讲了多个线程对index变量
的竞争引起的,解决竞争问题可以用synchronized
关键字,synchronized提供了一种排他
机制,也就是在同一时间只能有一个线程执行操作。
什么是synchronized关键字?
synchronized就是同步的意思,可以实现一种简单的策略来防止线程干扰和内存一致性错误,具体表现为如下:
monitor enter
和monitor exit
两个JVM指令,能够保证随时都能执行到monitor enter之前都能必须从内存
中获取数据。java happens-before
规则,一个monitor exit指令之前必定有一个monitor enter。Monitorenter
每一个对象都与一个monitor
关联,一个monitor的lock锁只能被一个线程在同一时间获得 。synchronized关键字获得锁,其实就是获取对象的monitor。