其他文章链接
Java基础
Java集合
多线程
JVM
MySQL
Redis
docker
计算机网络
操作系统
进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。
在Java中,当我们启动main函数时其实就是启动了⼀个JVM的进程,⽽main函数所在的线程就是这个进程中的⼀个线程,也称主线程。
在windows中通过查看任务管理器的⽅式,可以看到window当前运⾏的进程。
线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。
Java程序天⽣就是多线程程序,⼀个Java程序的运⾏是main线程和多个其他线程同时运⾏。
从上图可以看出:⼀个进程中可以有多个线程,多个线程共享进程的堆和⽅法区(JDK1.8之后的元空间)资源,但是每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈。
线程是进程划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反。
程序计数器主要有下⾯两个作⽤:
需要注意的是,如果执⾏的是native⽅法,那么程序计数器记录的是undefined地址,只有执⾏的是Java代码时程序计数器记录的才是下⼀条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。
堆和⽅法区是所有线程共享的资源,其中堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象(所有对象都在这⾥分配内存),⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 并发:同⼀时间段,多个任务都在执⾏(单位时间内不⼀定同时执⾏);
- 并⾏:单位时间内,多个任务同时执⾏。
- 从计算机底层来说:线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。另外,多核CPU时代意味着多个线程可以同时运⾏,这减少了线程上下⽂切换的开销。
- 从当代互联⽹发展趋势来说:现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。
深⼊到计算机底层来探讨:
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁。
- NEW:初始状态,线程被构建,但是还没有调用start()方法。
- RUNNABLE:运行状态,Java线程将操作系统中的就绪 REDAY 和运行 RUNNING 两种状态笼统地称作“运行中”。
- BLOCKED:阻塞状态,表示线程阻塞于锁。
- WAITING:等待状态,表示线程进人等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的。
- TERMINATED:终止状态,表示当前线程已经执行完毕。
- 线程创建之后它将处于NEW(新建)状态。
- 调⽤start()⽅法后开始运⾏,线程这时候处于READY(可运⾏)状态。可运⾏状态的线程获得了cpu时间⽚(timeslice)后就处于RUNNING(运⾏)状态。
- 当线程执⾏wait()⽅法之后,线程进⼊WAITING(等待)状态。进⼊等待状态的线程需要依靠其他线程的通知才能够返回到运⾏状态。
- TIME_WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,⽐如通过sleep(long millis)方法或wait(long millis)⽅法可以将Java线程置于TIMEDWAITING状态。当超时时间到达后Java线程将会返回到RUNNABLE状态。
- 当线程调⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到BLOCKED(阻塞)状态。
- 线程在执⾏Runnable的run()⽅法之后将会进⼊到TERMINATED(终⽌)状态。
多线程编程中⼀般线程的个数都⼤于CPU核⼼的个数,⽽⼀个CPU核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
概括来说就是:当前任务在执⾏完CPU时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。
上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU时间,事实上,可能是操作系统中时间消耗最⼤的操作。
Linux相⽐与其他操作系统(包括其他类Unix系统)有很多的优点,其中有⼀项就是,其上下⽂切换和模式切换的时间消耗⾮常少。
多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。
举例:线程A持有资源2,线程B持有资源1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。
产⽣死锁必须具备以下四个条件:
- 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
- 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
- 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。
- 破坏互斥条件:这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件:⼀次性申请所有的资源。
- 破坏不剥夺条件:占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
- 区别:sleep()⽅法没有释放锁,⽽wait()⽅法释放了锁。
- 相同:两者都可以暂停线程的执⾏。
wait()通常被⽤于线程间交互/通信,sleep()通常被⽤于暂停执⾏。wait()⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的notify()或者notifyAll()⽅法。sleep()⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤wait(longtimeout)超时后线程会⾃动苏醒。
new⼀个Thread,线程进⼊了新建状态。调⽤start()⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。start()会执⾏线程的相应准备⼯作,然后⾃动执⾏run()⽅法的内容,这是真正的多线程⼯作。但是,直接执⾏run()⽅法,会把run()⽅法当成⼀个main线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结:调⽤start()⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏run()⽅法的话不会以多线程的⽅式执⾏。
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。
在 Java 早期版本中, synchronized 属于重量级锁,效率低下。
因为监视器锁(monitor)是依赖于底层的操作系统的MutexLock来实现的,Java的线程是映射到操作系统的原⽣线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,⽽操作系统实现线程之间的切换时需要从⽤户态转换到内核态,这个状态之间的转换需要相对⽐较⻓的时间,时间成本相对较⾼。
在Java6之后Java官⽅对从JVM层⾯对synchronized较⼤优化,所以现在的synchronized锁效率也优化得很不错了。JDK1.6对锁的实现引⼊了⼤量的优化,如⾃旋锁、适应性⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
所以,⽬前不论是各种开源框架还是JDK源码都⼤量使⽤了synchronized关键字。
sunchronized void method() {
//业务代码
}
sunchronized void static method() {
//业务代码
}
sunchronized(this) {
//业务代码
}
总结:
构造⽅法不能使⽤ synchronized 关键字修饰。
构造⽅法本身就属于线程安全的,不存在同步的构造⽅法⼀说。
synchronized 关键字底层原理属于 JVM 层⾯。
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
当执⾏ monitorenter 指令时,线程试图获取锁也就是获取对象监视器 monitor 的持有权
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对 象中都内置了⼀个 ObjectMonitor对象。 另外, wait/notify等⽅法也依赖于 monitor对象,这就是为什么只有在同步的块或者⽅法 中才能调⽤ wait/notify等⽅法,否则会抛出 java.lang.IllegalMonitorStateException的异常的原因。
在执⾏monitorenter时,会尝试获取对象的锁,如果锁的计数器为0则表示锁可以被获取,获取后将锁计数器设为1也就是加1。
在执⾏monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位 置。
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。
不过两者的本质都是对对象监视器 monitor 的获取。
类⽐我们开发⽹站后台系统使⽤的缓存(⽐如Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题。
我们甚⾄可以把内存可以看作外存的⾼速缓存,程序运⾏的时候我们把外存的数据复制到内存,由于内存的处理速度远远⾼于外存,这样提⾼了处理速度。
总结:CPU Cache缓存的是内存数据⽤于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据⽤于解决硬盘访问速度过慢的问题。
在JDK1.2之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进⾏特别的注意的。⽽在当前的Java内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。
要解决这个问题,就需要把变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使⽤它都到主存中进⾏读取。
所以,volatile关键字除了防⽌JVM的指令重排,还有⼀个重要的作⽤就是保证变量的可⻅性。
synchronized关键字和volatile关键字是两个互补的存在,⽽不是对⽴的存在。
- volatile关键字是线程同步的轻量级实现,所以volatile性能⽐synchronized关键字好。但是volatile关键字只能⽤于变量,⽽synchronized关键字可以修饰⽅法以及代码块。
- volatile关键字能保证数据的可⻅性,但不能保证数据的原⼦性。synchronized关键字两者都能保证。
- volatile关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽synchronized关键字解决的是多个线程之间访问资源的同步性。
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量就要使用到 JDK 中提供的 ThreadLocal类。ThreadLocal类主要解决的就是让每个线程绑定⾃⼰的值,可以将ThreadLocal类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
如果创建了⼀个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使⽤get()和set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。
使⽤线程池的好处:
⽅式⼀:通过构造⽅法实现
⽅式⼆:通过Executor框架的⼯具类Executors来实现
我们可以创建三种类型的ThreadPoolExecutor:
注意:《阿⾥巴巴Java开发⼿册》中强制线程池不允许使⽤Executors去创建,⽽是通过ThreadPoolExecutor的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽的⻛险
Executors 返回线程池对象的弊端如下:
ThreadPoolExecutor3个最重要的参数:
ThreadPoolExecutor其他常⻅参数:
如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了任 时, ThreadPoolTaskExecutor 定义⼀些策略:
举个例⼦: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使⽤的是 ThreadPoolExecutor.AbortPolicy 。在默认情况下, ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应⽤程序,建议使⽤ ThreadPoolExecutor.CallerRunsPolicy 。当最⼤池被填满时,此策略为我们提供可伸缩队列。
Runnable接⼝不会返回结果或抛出检查异常,但是Callable接⼝可以。
所以,如果任务不需要返回结果或抛出异常推荐使⽤Runnable接⼝,这样代码看起来会更加简洁。
⼯具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
- execute()⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
- submit()⽅法⽤于提交需要返回值的任务。线程池会返回⼀个Future类型的对象,通过这个Future对象可以判断任务是否执⾏成功,并且可以通过Future的get()⽅法来获取返回值,get()⽅法会阻塞当前线程直到任务完成,⽽使⽤get(longtimeout, TimeUnitunit)⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。
Atomic是指⼀个操作是不可中断的。即使是在多个线程⼀起执⾏的时候,⼀个操作⼀旦开始,就不会被其他线程⼲扰。
所以,所谓原⼦类说简单点就是具有原⼦/原⼦操作特征的类。
基本类型
使⽤原⼦的⽅式更新基本类型
AQS 的全称为 AbstractQueuedSynchronizer,这个类在 java.util.concurrent.locks包下⾯。
AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器,⽐如ReentrantLock ,Semaphore ,其他的诸如 ReentrantReadWriteLock , SynchronousQueue , FutureTask 等等皆是基于 AQS 的。我们也能利⽤ AQS构造出符合我们⾃⼰需求的同步器。