1.了解多线程的意义和使用
1.1.什么是进程?什么是线程?
进程:是一个正在执行中的程序 每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。我们打开电脑上的qq时,点击qq.exe,电脑就会运行一个qq的程序,这个程序就叫做进程。
线程:是进程中的一个控制单元
线程控制着进程的执行。当qq运行后, 我们可能会使用qq来打开多个聊天窗口进行聊天,那么每一个聊天窗口就算是一个线程。所以说,进程可以包括很多的线程。
两者关系: 一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
线程和进程的区别?
进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位。一个进程可以包括多个线程。
线程之间的执行是同时进行的,例如我们在qq聊天时,可以一边聊天,一边下载文件。此时下载文件和聊天这个两个操作就是两个线程。如果是一个线程的话,那么下载文件的过程中,我们就不能聊天了,只有等待文件下载完成之后我们才可以继续聊天,这就叫串形,而我们一边聊天一边下载就是并行。
说的再通俗一点:例如说我们现在想要打扫卫生,那么串形就是我自己一个人,就代表一个线程。我要先打扫卫生间,在打扫厨房,在打扫客厅,再打扫卧室.... 因为我一个人,所以只能按照顺序先后执行,这样的话,就会非常的耗时间。那么还有一种方式就是我找几个朋友或者找几个家政保洁一起打扫。这样的话每一个人就相当于一个线程,大家一块打扫,你打扫你的,我打扫我的,互相之间并没有关联。此时打扫卫生总耗时就是耗时最多的一个人的时间,比如客厅空间比较大,那么打扫完整个房间的总耗时就是打扫客厅的时间,这就是多线程与单线程的区别。单线程也叫串形,多线程也叫并行。
并发/高并发 简单来说,并发是指单位时间内能够同时处理的请求数。默认情况下Tomcat可以支持的最大请求数是150,也就是同时支持150个并发。当超过这个并发数的时候,就会开始导致响应延迟,连接丢失等问 题。
影响服务器吞吐量的因素
硬件
CPU、内存、磁盘、网络 软件层面
>最大化的利用硬件资源
线程数量、JVM内存分配大小、网络通信机制(BIO、NIO、AIO)、磁盘IO 线程数量如何提升服务端的并发数量 并发和并行 并行是指两个或者多个事件在同一时刻发生; 并发是指两个或多个事件在同一时间间隔内发生,这个词可以冲宏观和微观两个层面来讲,如果从微观 角度来看。以线程为例,假设当前电脑的cpu是单核,但是能不能支持多线程呢?当然也是能的,此时 如果是多线程运行的话,那么CPU是通过不断分配时间片的方式来实现线程切换,由于切换的速度足够 快,我们很难感知到卡顿的过程。
1.2 Java中的线程
Runnable 接口 Thread 类 Callable/Future 带返回值的 Thread这个工具在哪些场景可以应用 网络请求分发的场景 文件导入 短信发送场景
1.3 线程的生命周期
线程的启动 -> 结束 阻塞状态 WAITING ===> 调用wait方法时 TIME_WAITING===> 调用sleep方法是 BLOCKED===>阻塞状态,未获取到锁 IO阻塞
代码:
public class Demo {
public static void main(String[] args) {
new Thread(()->{
while(true){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"STATUS_01").start(); //阻塞状态
new Thread(()->{
while(true){
synchronized (Demo.class){
try {
Demo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"STATUS_02").start(); //阻塞状态
new Thread(new BlockedDemo(),"BLOCKED-DEMO-01").start();
new Thread(new BlockedDemo(),"BLOCKED-DEMO-02").start();
}
static class BlockedDemo extends Thread{
@Override
public void run() {
synchronized (BlockedDemo.class){
while(true){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
找到class类路径使用命令行执行jps命令查看进程号, 使用jstack查看对应pid的线程的堆栈信息
需要注意的是,操作系统中的线程除去 new 和 terminated 状态,一个线程真实存在的状态,只有: ready :表示线程已经被创建,正在等待系统调度分配CPU使用权。 running :表示线程获得了CPU使用权,正在进行运算 waiting :表示线程等待(或者说挂起),让出CPU资源给其他线程使用 在加上新建状态和死亡状态,一共5种
线程的启动
new Thread().start(); //启动一个线程
Thread t1=new Thread()
t1.run(); //调用实例方法
线程的终止
线程什么情况下会终止 ? run方法执行结束
interrupt()的作用 设置一个共享变量的值 true 唤醒处于阻塞状态下的线程。
2. 并发编程的挑战
一个问题引发的思考
public static int count=0;
public static void incr(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main( String[] args ) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->App.incr()).start();
}
Thread.sleep(3000); //保证线程执行结束
System.out.println("运行结果:"+count);
}
结果是小于等于1000的随机数。
原因: 可见性、原子性 count++的指令, 使用命令 javap -v 类名.class 查看java指令
14: getstatic #5 // Field count:I
15: iconst_1
16: iadd
17: putstatic #5
2.1 锁(Synchronized)
互斥锁的本质是什么.
共享资源
锁的使用 可以修饰在方法层面和代码块层面
class Test {
// 修饰非静态方法
synchronized void demo() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void demo01() {
synchronized(obj) {
// 临界区
}
}
}
锁的作用范围 synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:
-
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁(对象锁)
-
静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁(常量锁)
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。(类锁)
public class SynchronizedDemo {
Object lock=new Object();
//只针对于当前对象实例有效.
public SynchronizedDemo(Object lock){
this.lock=lock;
}
void demo(){
synchronized(lock){
}
}
void demo03(){
synchronized (this){
//线程安全性问题.
}
}
//-------------//类锁. 针对所有对象都互斥
synchronized static void demo04(){
}
void demo05(){
synchronized (SynchronizedDemo.class){
}
}
//锁的范围
// 实例锁,对象实例
// 静态方法、类对象、 类锁
// 代码块
public static void main(String[] args) {
Class clazz=SynchronizedDemo.class;
Object object=new Object();
Object object1=new Object();
SynchronizedDemo synchronizedDemo=new SynchronizedDemo(object);
SynchronizedDemo synchronizedDemo2=new SynchronizedDemo(object);
//锁的互斥性。
new Thread(()->{
synchronizedDemo.demo();
},"t1").start();
new Thread(()->{
synchronizedDemo2.demo();
},"t2").start();
}
}
2.2 锁的升级
偏向锁
在大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设 计了偏向锁。偏向锁,顾名思义,就是锁偏向于某个线程。 当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这 段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线 程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了 在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。(偏向锁的目的是消除数据在无竞争情 况下的同步原语,进一步提高程序的运行性能。)
轻量级锁
如果偏向锁被关闭或者当前偏向锁已经已经被其他线程获取,那么这个时候如果有线程去抢占同步锁 时,锁会升级到轻量级锁。
重量级锁
多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒 这些线程; Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的:os pthread_mutex_lock() ; 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指 针,此时等待锁的线程都会进入阻塞状态
总结 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进 入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占 的情况。 轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的 LockRecord,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一 把锁的情况。 重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。
2.3 线程的通信(wait/notify)
在Java中提供了wait/notify这个机制,用来实现条件等待和唤醒。这个机制我们平时工作中用的少,但 是在很多底层源码中有用到。比如以抢占锁为例,假设线程A持有锁,线程B再去抢占锁时,它需要等待 持有锁的线程释放之后才能抢占,那线程B怎么知道线程A什么时候释放呢?这个时候就可以采用通信机 制。
3. 小知识点
3.1.join方法
当A线程执行到B线程的join方法时,A就会等待,等待B线程都执行完后,A才会执行。 join可以用来临时加入线程执行。
3.2.守护线程
setDaemon方法 public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
3.3.优先级
优先级默认为5级,可以通过setPriority(int newPriority)方法来更改线程的优先级 最明显的值为 MAX_PRIORITY=10 NORM_PRIORITY=5 MIN_PRIORITY=1
3.4.yield()方法
暂停当前正在执行的线程对象,并执行其他线程。
3.5.死锁
public class DeadLock {
public static void main(String[] args) {
final Object locka = new Object();
final Object lockb = new Object();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (locka) {
System.out.println(Thread.currentThread().getName() + "获取locka...");
synchronized (lockb) {
System.out.println(Thread.currentThread().getName() + "获取lockb...");
}
}
}
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
synchronized (lockb) {
System.out.println(Thread.currentThread().getName() + "获取lockb...");
synchronized (locka) {
System.out.println(Thread.currentThread().getName() + "获取locka...");
}
}
}
}
}, "t2").start();
}
}
4. 探索线程安全性背后的本质之volatile
一个问题引发的思考
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while(!stop){
i++;
// System.out.println("rs:"+i);
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(1000);
stop=true;
}
print就可以导致循环结束
活性失败. JIT深度优化
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while(!stop){
i++;
// System.out.println("rs:"+i);
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(1000);
stop=true;
}
这里分为两个层面来解答
内存,可以通过如下代码去证明。
Thread thread=new Thread(()->{
int i=0;
while(!stop){
i++;
synchronized (JITDemo.class){
}
}
});
Thread.sleep(0)
官方文档上是说,Thread.sleep没有任何同步语义,编译器不需要在调用Thread.sleep之前把缓存在寄 存器中的写刷新到给共享内存、也不需要在Thread.sleep之后重新加载缓存在寄存器中的值。 编译器可以自由选择读取stop的值一次或者多次,这个是由编译器自己来决定的。 但是在Mic老师认为:Thread.sleep(0)导致线程切换,线程切换会导致缓存失效从而读取到了新的值。
4.1 volatile关键字(保证可见性)
什么是可见性?
在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那 这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。但是在多线程 环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最 新的值。这就是所谓的可见性
硬件层面 CPU/内存/IO设备
CPU层面的高速缓存
-
CPU层面增加了高速缓存
-
操作系统,进程、线程、| CPU时间片来切换
-
编译器的优化 ,更合理的利用CPU的高速缓存.
总线锁&缓存锁
总线锁,简单来说就是,在多cpu下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出 一个LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定 的开销比较大,这种机制显然是不合适的 。 如何优化呢?最好的方法就是控制锁的保护粒度,我们只需要保证对于被多个CPU缓存的同一份数据是 一致的就行。在P6架构的CPU后,引入了缓存锁,如果当前数据已经被CPU缓存了,并且是要协会到主 内存中的,就可以采用缓存锁来解决问题。 所谓的缓存锁,就是指内存区域如果被缓存在处理器的缓存行中,并且在Lock期间被锁定,那么当它执 行锁操作回写到内存时,不再总线上加锁,而是修改内部的内存地址,基于缓存一致性协议来保证操作 的原子性。
指令重排序
通过使用volatile关键字防止指令重排序和内存屏障机制导致的内存可见性问题
final可以保证内存的可见性