wait()
:由Object类提供,当一个 线程调用了一个共享变量的wait()方法时,该线程将会被阻塞挂起,直到发生以下事件才返回。
- 其它线程调用了该共享对象的
notify()
或者notiftAll()
方法 - 其它线程调用了该线程的
interrupt()
方法,该线程抛出IllegalMonitorStateExcception
异常返回。
若调用
wait()
方法时没有事先获取该对象的监视器锁,则会抛出IllegalMonitorStateExcception
异常。获取该对象的监视器锁方法如下:
(1)使用线程同步代码块时,将该共享变量作为参数;
(2)使用同步方法来实现线程安全。
为防止线程发生虚假唤醒(即从挂起状态变为可运行状态,而没有被其它线程调用notify()
等方法通知,被中断,被等待超时),需要不停测试改线程被唤醒的条件是否满足。
//线程同步代码块获得监视器锁对象!
synchronized (obj){
//调用while()循环不断检查线程唤醒条件是否满足
while (条件不满足){
obj.wait()
}
}
-
notify()
函数,由Object类提供,一个线程调用共享对象的该方法后会唤醒一个在该变量上调用wait()系列方法后别挂起的线程,若有多个这种线程,被唤醒的等待线程是随机的。同样,这样被唤醒的线程不能马上从wait()方法返回并继续执行,必须获取改共享对象的监视器锁后才能返回,因为唤醒它的线程在释放该共享变量的监视器锁后,需要该线程与其他线程一起竞争该锁,竞争到才能继续执行。
-
notifyAll()
会唤醒所有在该共享变量上由于调用wait()系列方法而被挂起的线程。 -
join()
方法:由Thread类提供,该方法无参且无返回值,作用是等待该线程执行完毕后再返回。
- sleep()方法:由Thread类提供,调用该方法将使该线程暂时让出指定时间的执行权,不参与CPU调度,但该线程所有的监视器资源(比如锁)是不让出的,指定时间到之后,线程正常返回,并出于就绪状态,参与CPU调度,获取到CPU资源就可以继续运行。
睡眠期间的线程如果被其它线程调用该线程
interrupt()
方法中断,则该线程会在调用sleep()方法的地方抛出InterruptedExceptinon
异常。
-
void interrupt()
方法:中断线程。调用该方法可以将该线程的中断标志位设置为true
并立即返回。设置标志位后线程并没有立即被中断,它会继续向下执行。若一个线程因调用wait()系列函数、join方法或sleep方法二被阻塞挂起,另一个线程再调用该线程的interrupt()方法则会抛出InterruptedExceptinon
异常。 -
boolean isInterrupted()
方法:检测当前线程是否被中断,是则返回true,否返回false,不清除中断标志。public boolean isInterrupted(){ //传递false,说明不清楚中断标志 return isInterrupted(false); }
-
static boolean interrupted()
方法: 检测当前线程是否被中断,是则返回true,否返回false。与前者不同的是,此方法若发现当前线程被中断,则会清楚中断标志。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪态 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(超时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
如图所示:
使用线程池的原因:频繁的创建与销毁线程池不仅繁琐且影响系统的稳定性,浪费资源,为了使得创建的线程能过复用,因此引入了线程池。
主要解决的两个问题:
- 为执行大量异步任务提供较好的性能;
- 提供了一种资源限制和管理的手段。如限制线程的数量,动态新增线程
- 线程池:就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
原理图简单理解如下:
使用线程池的好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
- Java里面线程池的顶级接口是
java.util.concurrent.Executor
, - 真正的线程池接口是
java.util.concurrent.ExecutorService
配置一个线程池是比较复杂,官方建议使用Executors工程类来创建线程池对象。
方法如下:
-
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量) -
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。
使用线程池创建对象的步骤如下:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
示例代码:
定义Runnabel任务对象:
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i =0; i<3; i++){
System.out.println(Thread.currentThread().getName()+"===>"+i);
}
}
}
定义Callable任务对象:
class MyCallable implements Callable<String>{
//需求:使用线程池,计算1-100,1-200,1-300的和并返回。
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i =0; i<n; i++) {
System.out.println(Thread.currentThread().getName() + " isCallable===>" + i);
sum += i;
}
return Thread.currentThread().getName()+"执行的结果是"+sum;
}
}
创建线程池,开启任务并接收结果。
public class ThreadPoolDemo {
public static void main(String[] args) {
//1.创建一个线程池,指定线程的固定数量是3
ExecutorService pools = Executors.newFixedThreadPool(3);
//2.添加线程任务让线程池处理
// Runnable target = new MyRunnable();
// Callable callable = new MyCallable();
// FutureTask<String> futureTask = new FutureTask(callable);
// Thread newThread = new Thread(futureTask);
// newThread.start();
// try {
// String name = futureTask.get();
// System.out.println(name);
// } catch (Exception e) {
// e.printStackTrace();
// }
// pools.submit(target); //第一次提交任务,此时线程池创建新线程
// pools.submit(target); //第二次提交任务,此时线程池创建新线程
// pools.submit(target); //第三次提交任务,此时线程池创建新线程
// pools.submit(target); //第四次提交任务,复用之前的线程
//提交Callable任务对象后返回一个未来任务对象!
Future<String> t1 = pools.submit(new MyCallable(100));
Future<String> t2 = pools.submit(new MyCallable(200));
Future<String> t3 = pools.submit(new MyCallable(300));
//获取当前线程池执行的结果
try {
String task1 = t1.get();
String task2 = t2.get();
String task3 = t3.get();
System.out.println(task1);
System.out.println(task2);
System.out.println(task3);
}catch (Exception e){
e.printStackTrace();
}
}
}
简单理解:就是当前线程使用完时间片之后,就会处于就绪状态并让出CPU让其它线程占用。
上下文切换的时机:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。
死锁是指两个或者两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在没有外力干扰的情况下会一直等待下去的场景。
- 互斥条件:线程对持有的资源进行排他性使用,其它线程想要使用该资源只能等待。
- 请求并持有条件:指一线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已经被其他线程占有,导致当前线程被阻塞,同时又不释放自身的资源。
- 不可剥夺条件:指线程获取到的资源在自己使用完毕之前不能被其他线程抢占,只有在自己使用完才由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个线程-----资源的环形链。(如上图死锁示意图)
必然死锁案例:
package 死锁案例;
/**
* Create By Chenhui Peng on 2021/3/3.
*/
public class ThreadDead {
//定义一个资源对象
public static Object rescourse01 = new Object();
public static Object rescourse02 = new Object();
public static void main(String[] args) {
//定义2个线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (rescourse01){
System.out.println("线程1占用资源1请求资源2");
try {
//使线程1睡1秒后再请求资源2,必然发生死锁
Thread.sleep(1000);
synchronized (rescourse02){
System.out.println("线程1成功占用资源2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (rescourse02){
System.out.println("线程2占用资源2,请求资源1");
try {
//使线程2睡1秒后再请求资源1,必然发生死锁
Thread.sleep(1000);
synchronized (rescourse01){
System.out.println("线程2成功占用资源1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
要避免死锁,只需要破坏掉至少一个构成死锁的必要条件即可。
目前只有请求并持有和环路等待条件是可以被破坏的。
synchronized
块是Java提供的一种原子性内置锁,Java中的每一个对象都可以把他当做一个同步锁来使用。
问题引入:并发编程下,多线程访问变量的不可见性问题
产生原因:多个线程访问共享变量,会出现一个线程修改变量的值后,其它线程看不到最新值的情况。
并发编程下,多线程修改变量会出现线程间的变量不可见性。
示例代码:
package volatile关键字;
import examples.TheadDemo;
/**
* Create By Chenhui Peng on 2021/3/3.
*/
public class VolatileDemo extends Thread{
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程中修改变量
flag = true;
System.out.println("flag:"+flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
class visibilityDemo{
public static void main(String[] args) {
//1.启动子线程,修改flag的变量为true,
VolatileDemo t = new VolatileDemo();
t.start();
//2.主线程检查子线程flag变量是否为true
while (t.isFlag()){
System.out.println("主线程进入执行!");
}
}
}
运行结果如下:
flag:true
Process finished with exit code 0
此时主线程并没有访问到线程里已经改变的flag值.
Java内存模型(之和java并发编程有关):JMM(java memory model)
(重点)JMM是java虚拟机规范中定义的一中内存模型,JMM是标准化的,屏蔽了底层不同计算机之间的区别。他描述了JAVA程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样底层的细节。
JMM的规定如下:
- 所有的共享变量都存储于主内存。这些共享变量指实例变量和类变量,不包含局部变量,因局部变量为线程私有,无需竞争。
- 每个线程拥有自己的工作内存,在这里保留了线程所使用的变量的工作副本。
- 线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
本地内存和主内存之间的关系:
前一例代码的原因如图所示:
内存不可见性的原因:
- 每个线程都有自己的工作内存,线程都是从主内存拷贝共享变量的副本值。
- 每个线程都是在自己的工作内存中操作共享变量的。
两种常见解决方案:
- 加锁
- 对共享变量进行volatile关键字修饰
加锁(synchronized)的解决流程如下:
- 线程获得锁
- 线程清空工作内存(由于加了锁)
- 线程从主内存拷贝共享变量的最新值到工作内存成为副本
- 执行线程任务代码
- 将修改后的副本的值刷新到主内存中
- 线程释放锁
class visibilityDemo{
public static void main(String[] args) {
//1.启动子线程,修改flag的变量为true,
VolatileDemo t = new VolatileDemo();
t.start();
//加锁!避免并发编程的变量的不可见性。
synchronized (visibilityDemo.class){
//2.主线程检查子线程flag变量是否为true
while (t.isFlag()){
System.out.println("主线程进入执行!");
}
}
}
}
使用volatile关键字修饰该变量:
public class VolatileDemo extends Thread{
//volatile关键字修饰
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatile关键字工作原理:
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其它地方,而是会把值刷新到主内存。当其它线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的副本值。
当线程写入volatile变量值就相当于线程退出synchronized同步块,当线程读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。
原子性操作即执行一系列操作时,要么全部一起执行,要么全部都不执行,不存在只执行其中一部分操作的情况。
举例说明:自己给对方账户转账500,对方账户也需要同时+500,这两个操作必须同时完成,要么都失败,要么都成功。
注意: volatile关键字只能保证线程解决共享变量的不可见性问题(访问最新的变量值),但不能保证各个线程操作的原子性。
解决方案:
- 使用锁机制,直接给线程任务加锁,但性能较差(因为一次只有一个线程在对该共享变量执行)。
- 使用原子类(性能高效,线程安全)。
什么是原子类?
概述:java从JDK1.5开始提供了java.util.concurrent.atomic
包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
原子型Integer,可以实现原子更新操作。
public AtomicInteger()
:初始化一个默认值为0的原子型Integerpublic AtomicInteger(int initialValue)
:初始化一个指定值的原子型Integerint get()
: 获取值int getAndIncrement()
: 以原子方式将当前值加1,注意,这里返回的是自增前值。int incrementAndGet()
: 以原子方式将当前值加1,注意,这里返回的是自增后的值。int addAndGet(int data)
: 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。int getAndSet(int value):
以原子方式设置为new Value
的值,并返回旧值。
示例:
class MyRunnable02 implements Runnable{
//创建一个Integer原子类 AtomicInteger来更新,初始为0
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
synchronized (MyRunnable.class){
for (int i =1; i<=100 ; i++){
//使用原子类的方法来操作共享变量,保证各个线程访问该共享变量的操作原子性
System.out.println("count ======> " + atomicInteger.incrementAndGet());
}
}
}
}
CAS即Compare and Swap,是JDK
提供的非阻塞原子性操作,通过一系列硬件保证了比较--更新一系列操作的原子性,实现了read-modify-check-write的原子性操作。
jdk
里面的Unsafe
类提供了一系列的CAS方法,实现操作的原子性。
CAS的四个操作数:
- 对象内存位置obj
- 对象中的变量的偏移量 valueOffset
- 变量的预期值 except
- 新的值 update
原理:如果对象Obj中的内存偏移量为valueOffset的变量值为except,则使用新的值update替换旧的值except。
CAS中的ABA问题:
比如说现在有三个线程来操作同一个地址A中的变量X,X的初始值是l。线程1和线程2的作用都是将变量X的值修改为m,但线程2在访问到变量X后被阻塞(BLocked),线程1成功执行完毕,讲变量X的值修改为m,此时线程3开始启动。线程3将变量X的值m再修改为l。线程3执行完毕之后线程2从阻塞中恢复,继续执行操作,通过检查自己的工作内存中的值是否与主内存中的变量X的值相等,此时发现相等,再继续执行,将变量X的值再修改为m。
(变量X的值发生了环形转换!)
ABA问题的影响很严重,比如说取钱,初始余额100,重复提交了两次取50操作,理想情况下应该是只执行1次,结果执行了一次操作的的时候你妈给你又转了50,在第一次取钱成功的情况下,余额又从50 变为了100,然后此时第二次取钱的操作开始生效,检查发现此时余额的值和自己当时保存的副本都是100元,继续执行!余额又变为50,而你则少了50!
解决ABA问题的方案是给每个变量的状态值都加上一个版本号!若发现版本号不对应,则取消本次操作,重新从主内存中获取新值继续操作。
CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
Synchronized是从悲观的角度出发(悲观锁)
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!
CAS是从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!
java内存模型(JMM)允许编译器和处理器对指令进行重新排序以提高运行性能,并且只会对不存在数据依赖性的指令进行重排序。如以下代码:
int a = 1; //(1)
int b = 2; //(2)
int c = a + b; //(3)
前两条指令的执行顺序并不会造成影响,但第三条指令的顺序则有影响。