多线程与并发编程
- 1. 进程和线程
- 2. 为什么要使用多线程 ?
- 3. 使用多线程可能带来什么问题 ?
- 4. 线程的生命周期 ?
- 5. 什么是上下文切换 ?
- 6. 死锁
- 7. 创建线程的三种方式
- . 并发编程三要素 ?
- . 说说yield()、join()、sleep() 方法和 wait() 方法区别和共同点 ?
- . 直接调用线程对象的run()方法和调用start()方法有什么区别?
- . 说一说Synchronize同步锁 ?
- . 说一说volatile ?
- . 说说 synchronized 关键字和 volatile 关键字的区别 ?
- . 谈谈 synchronized和ReentrantLock的区别 ? ※
- . CAS
- . ThreadLocal
- . ThreadLocal与Synchronized的区别 ?
- . JMM
1. 进程和线程
引入:
引入进程是为了能使多道程序并发执行,提高资源利用率和吞吐量;
而引用线程是为了则是为了减小程序在并发时的开销,提高并发性能;
1.1. 何为进程 ?
进程是程序的⼀次执⾏过程,是系统运行程序的基本单位,是操作系统进行资源分配和调度的基本单位;
在Java中,启动main函数就是启动了一个JVM的进程;
⼀个进程在其执行的过程中可以产生多个线程;
【操作系统中】,进程实体 = PCB进程控制块 + 程序段 + 数据段
系统利用PCB来描述进程的状态,以此控制和管理进程;
1.2. 何为线程 ?
线程与进程相似,是比进程更小的执行单位,是CPU执行的最小单元;
⼀个进程在其执行的过程中可以产生多个线程;
【在Java中】,Java进程中有多个线程,共享进程的堆和方法区资源,每个线程有⾃己私有的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
2. 为什么要使用多线程 ?
1.提高系统资源(CPU、内存)利用率,执行效率;
2.线程切换的开销远比进程小;
3.如今高并发的需求量,需要多线程机制来满足需求;
3. 使用多线程可能带来什么问题 ?
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序
运⾏速度的,⽽且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
4. 线程的生命周期 ?
5. 什么是上下文切换 ?
概括来说就是:当前任务在执行完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。
任务从保存到再加载的过程就是⼀次上下⽂切换。
进程:
①保存原数据到PCB
②对新进程数据的恢复
6. 死锁
指多个进程因竞争资源造成的一种互相等待的僵局,
程序在等待资源被释放,但资源无法被是否,若无外力作用将无法先前推进;
死锁的必要条件(其中一个不成立就不会死锁):
1.互斥条件:资源只能被一个线程占用
2.请求保持条件:【线程阻塞时】,对已获得的资源不释放(自己不放)
3.不剥夺条件:线程已获得的资源不能被抢走(不能被抢)
4.循环等待条件:线程形成头尾相接的循环等待资源的关系
如何避免死锁:
1.互斥条件:无法破坏,用锁就是为了要互斥(临界资
源需要互斥访问,一次仅允许一个进程使用的资源即临界资源)
2.请求保持条件:一次性申请所有资源;
3.不剥夺条件: 若线程申请不到资源,则主动释放资源;
4.循环等待条件: 多用户访问资源时,按照相同资源访问顺序进行处理,破坏循环等待条件;
例子中解决方法:一次性申请所有资源,让线程t1 先用完o1、o2,再开启线程t2 (线程2 先Thread.sleep(2000) );
7. 创建线程的三种方式
1.子类extends 继承 java.lang.Thread线程类,重写run() 方法,使用子类创建线程对象,最后调用start()
2.implements实现java.lang.Runnable接口,重写run()方法,建立Runnable的对象引用放入Thread的构造参数中,最后调用start()
3.编写类implements实现Callable接口,重写call方法,将其对象引用放入FutureTask构造参数中,再将FutureTask的对象引用放入Thread构造参数中,最后调用start() (FutureTask 实现了Runnable接口,可以作为Thread的参数输入!)
. 并发编程三要素 ?
1)原子性
指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
实现:Synchronized
2)可见性
指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
实现:volatile
3)有序性
有序性,即程序的执行顺序按照代码的先后顺序来执行。
实现:volatile(禁止指令重排)
. 说说yield()、join()、sleep() 方法和 wait() 方法区别和共同点 ?
yield()
会让线程从运行状态直接回到就绪状态,下一次继续抢占时间片,区别于阻塞;
join()
使当前线程进入 阻塞
,直到调用join()的线程执行结束,阻塞解除,当前线程才能回到就绪态
继续抢占时间片,类似插队;
sleep()
使当前正在执行的线程进入 阻塞
状态,时间结束后进入就绪态
继续抢占时间片;可使用 interrupt
方法结束sleep;
wait()和notify()方法不是线程当中的方法,是任何一个Java对象都有的方法(Object自带的)
wait()
:让【当前对象上】活动的线程进入等待状态,无期限等待,直到调用o.notify方法被唤醒为止;②并且释放掉t线程之前占有的o对象的锁
notify()
: 唤醒正在o对象上等待的线程;
notifyAll()
:方法可以唤醒o对象上处于等待的所有线程 / 没有waitAll !
sleep()和wait()对比
- sleep()是Thread类的方法,而wait()是Object类的方法;
- 最大的区别在于sleep() 没有释放锁,而wait()方法会释放锁;
- 两者都可以暂停线程的执行;
- wait()方法调用后,线程不会自己恢复,需要其他线程调用【同一个对象上】的notify()方法来唤醒;sleep() 到时间就会从阻塞回到就绪状态;
. 直接调用线程对象的run()方法和调用start()方法有什么区别?
start()方法的作用是启动分支线程,是在JVM中开辟一个新的栈空间,这段代码任务完成之后瞬间就结束了。
这段代码的任务只是为了开辟新的空间,只要空间开出来后,start()方法结束,线程进⼊了就绪状
态,线程启动成功。
启动成功的线程会自动调用run()方法;
如果把t.start()改为t.run() ,则并未开辟分支栈,就是普通的调用方法,还是在main主线程里运行;
. 说一说Synchronize同步锁 ?
Synchronized原理
作用:
1.synchronized关键字主要解决多个线程之间访问资源的同步性;
2.已经能够保证在【读写数据时】对数据的原子性和可见性;
Synchronized实现可见性: ※
1.线程在【加锁时】,先清空线程的工作内存;
2.在【主内存中】拷贝最新变量的副本到工作内存;
3.执行完代码【开锁前】,将更改后的共享变量的值刷新到主内存中;
4.释放锁
synchronized关键字最主要的三种使用方式:
1.修饰代码块: 指定加锁对象,向上找到共享对象;
2.修饰实例方法: 对this当前对象实例加锁;
3.修饰静态方法:表示找类锁,类锁永远只有一把!而对象锁则是一个对象一把锁;
. 说一说volatile ?
volatile
它主要有两重个作用,一是保证多个线程对共享变量访问的可见性,二防止指令重排。
. 说说 synchronized 关键字和 volatile 关键字的区别 ?
- volatile主要用于解决变量在多个线程之间的可见性,volatile不保证数据的原子性;
- synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized保证数据的原子性和可见性;(volatile和synchronized都可以保证数据的可见性!)
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定⽐synchronized关键字要好。
- volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块,主要是对共享对象加锁;
. 谈谈 synchronized和ReentrantLock的区别 ? ※
ReentrantLock锁:ReentrantLock
ReentrantLock底层基于AQS实现,即使用volatile修饰的state属性和阻塞队列来实现线程的串行执行,从而达到线程安全性的目的;
ReentrantLock是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(具有更多的方法),能解决synchronized关键字在一些并发场景下不适用的问题;
-
两者都是可重⼊锁
“可重⼊锁”概念是:⾃⼰可以再次获取⾃⼰的内部锁。⽐如⼀个线程获得了某个对象的锁,此时这个对象锁还没有释放,当线程再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重⼊的话,就会造成死锁。
同⼀个线程每次获取锁,锁的计数器都⾃增1,所以要等到锁的计数器下降为0时才能释放锁。
重入锁的设计目的是为了解决死锁的问题; -
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 juc的API;
-
ReentrantLock可以指定是公平锁还是非公平锁。⽽synchronized只能是⾮公平锁。所谓的公平
锁就是先等待的线程先获得锁。 ReentrantLock默认情况是⾮公平的,可以通过 ReentrantLock
类的 ReentrantLock(boolean fair) 构造⽅法来制定是否是公平的。 -
synchronized关键字与wait()和notify()/notifyAll()⽅法相结合可以实现等待/通知机制,
ReentrantLock类当然也可以实现,但是需要借助于Condition接⼝与newCondition() ⽅法。
Condition是JDK1.5之后才有的,它具有很好的灵活性,⽐如可以实现多路通知功能也就是在⼀
个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的
Condition中,从⽽可以有选择性的进⾏线程通知,在调度线程上更加灵活。 在使⽤
notify()/notifyAll()⽅法进⾏通知时,被通知的线程是由 JVM 选择的,⽤ReentrantLock类结
合Condition实例可以实现“选择性通知” ,这个功能⾮常重要,⽽且是Condition接⼝默认提供
的。⽽synchronized关键字就相当于整个Lock对象中只有⼀个Condition实例,所有的线程都注
册在它⼀个身上。如果执⾏notifyAll()⽅法的话就会通知所有处于等待状态的线程这样会造成
很⼤的效率问题,⽽Condition实例的signalAll()⽅法 只会唤醒注册在该Condition实例中的所
有等待线程。 -
可中断:synchronized只能等待同步代码块执行结束,不可以中断,而reentrantlock可以调用线程的interrupt方法来中断等待,继续执行下面的代码。
可以设置超时时间:调用lock.trylock(),如果没有设置等待时间的话,没获取到锁,将返回false
可以设置为公平锁:公平锁其实是为了解决饥饿问题,当一个线程由于优先级太低的时候,就可能没有办法获取到时间片
可以支持多个变量:类似于调用wait方法时,不满足条件的线程进入waitset队列等待CPU随机调度,支持多个变量表示支持多个类似自定义waitset,这样就可以指定对象来唤醒了。
. CAS
即Compare and Swap,是非阻塞的方式实现并发,是一种轻量级的乐观锁;
CAS体现的是无锁并发,无阻塞并发; (无阻塞所以上下文切换少,效率高)
乐观锁思想:允许其他线程来修改数据,先核验再执行;
悲观锁思想:不允许其他线程来修改数据;
CAS操作包含了三个操作数据:
主内存的共享变量(V)、预期原值(A)和新值(B) ;
实现:
先比较主内存中的共享变量和预期原值的大小是否相同,相同即主内存中的变量还没有被其他线程所改变,则修改为新值;否则继续尝试;
如:
compareAndSet会比较prev旧值和内存中的共享变量,如果相同则返回true,将旧值修改为新值;否则继续尝试;
CAS和volatile :
CAS需要volatile
以保证内存中共享变量的值为最新,因为每次compareAndSet,会比较共享变量最新的结果;然后将最新的结果和pre旧值去比较,如果相等才进行更新;
如果共享变量没有用volatile修饰,则拿到的共享变量可能不是最新的,那么就可能出问题;
为什么无锁效率更高 ?
因为就算compareAndSet失败了,但是while(true)一直在高速运行没有停下来,上下文切换少,所以效率高;
而用Synchronized时,则会因为线程占有Owner则会在EntryList队列中阻塞等待,线程会发生上下文切换,成本高;
. ThreadLocal
. ThreadLocal与Synchronized的区别 ?
1.Synchronized用于线程间的数据共享,而恰恰相反,ThreadLocal用于线程间的数据隔离;
2.Synchronized是利用锁的机制,以阻塞的方式 使变量或代码块在同一时该只能被一个线程访问;而ThreadLocal为每一个线程都提供了变量的副本;
. JMM
Java 内存模型是一种规范;
所有的变量都存储在主内存中;
每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的拷贝副本。
线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
不同的线程之间无法直接访问对方本地内存中的变量。
三大特性:
即在【多线程读写共享数据时】(成员变量、数组)时,数据的原子性、可见性、有序性 ;
原子性:
由synchronized来实现;
可见性:
比如说一个线程对某个变量进行修改,其他线程能够立刻看到,即为可见。
由volatile 、synchronized来实现;
有序性:
代码的执行顺序应该上到下按序执行,但编译器有优化机制,会为了提升程序的性能和执行速度而调整执行顺序,单线程时结果看起来没什么变化,多线程下结果就可能出问题。
由volatile 来实现;