一、什么是线程
程序、进程、线程区别
程序:程序是一段静态的代码,是应用软件执行的蓝本。
进程:是程序的一次静态执行过程,它对应了从代码加载(从磁盘加载到内存)、执行(到CPU中执行)、结束的完整过程。这也是进程从开始到消失的过程。作为执行蓝本的同一段程序,可以多次加载到系统的不同内存区域执行,形成不同进程。
线程:包含在进程里面,是进程里面的一个能独立执行自身指令的指令流(即一个子任务)。一个进程里面有一条条指令流(线程),以一定顺序加载到CPU中执行。
进程与线程的物理组成:
进程由代码、数据、内核状态和一组寄存器组成。
线程由表示程序运行状态的寄存器(如程序计数器、栈指针)以及堆栈组成。不包含进程地址空间中的代码和数据。
进程是一个内核级别的实体,进程结构的所有成分都在内核空间中,一个用户程序不能直接访问这些数据。
线程是一个用户级实体,线程结构驻留在用户空间中,能够被普通用户级函数直接访问。
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间。
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。
很经典的一句话——
线程是最小调度单位,进程是资源分配的最小单位
并行与并发
并发——针对单核CPU
线程串行执行(微观串行,宏观并行(因为切换很快,让人以为是并行)),一般会将这种线程轮流使用CPU的做法称为并发。同一时间只能是调度一个线程。
并行——针对多核CPU
多核CPU,每个核(core)都可以调度运行线程,这时候线程可以是并行的。
Java中的线程模型
多线程是指一个程序中包含多个执行流(即多个子任务),多线程是实现并发的一种有效手段。
一个执行流是CPU运行程序代码并操作程序的数据所形成的。**因此线程又被认为是以CPU为主体的行为。在Java中的线程模型就是一个虚拟的CPU、程序代码和数据的封装体。**其中代码和数据构成了线程体,线程体决定了线程的行为。而虚拟的CPU实在创建线程时由系统自动封装进Thread类的实例中的。
如图:
代码与数据是相互独立的,代码和数据可以与其他线程共享,两个线程可以同时访问同一个对象,它们将共享代码、数据。
而Java天生支持多线程编程。
线程模型在java.lang.Thread类中进行定义与描述,程序中的线程都是Thread类的实例。用户可以通过创建Thread的实例或者定义并创建Thread子类的实例建立和控制自己的线程。
二、线程的创建
用户可以通过创建Thread的实例或者定义并创建Thread子类的实例建立和控制自己的线程。
在Java中,创建线程的关键是构建线程体。**线程体要应用程序通过一个对象传递给Thread类的构造函数。**线程体是在线程类中的run()方法中定义的,在其中定义线程的具体行为,线程开始执行时也是由run()方法开始执行的,就像Java applicantion里面的main()函数一样。
一共有两种方法构建线程体:
(一)通过实现Runnable接口创建
Runnable接口定义为:
public interface Runnable{
void run();
}
使用这种方式创建线程有两大步:
(1)定义一个类实现Runnable接口,在该类中提供run()方法的实现。
(2)把Runnable的一个实例作为作为参数传递给Thread类的一个构造方法,该实例对象提供线程体run()。
例子:
Hello类的两个实例对象分别创建了t1 t2两个线程,并将线程启动。在创建的线程中,Hello类的run方法就是线程体,int i是线程的数据。线程t1、t2启动时是从Hello类的run方法开始执行的。
如图,每个线程分别打印5个字符串——
Hello0
Hello1
Hello2
Hello3
Hello4
Hello0
Hello1
Hello2
Hello3
Hello4
注意:新建的线程不会自动运行,必须调用线程的start()方法。该方法的调用把嵌入在线程中的虚拟CPU置为可运行(Runnable)状态,意味着其可以被调度运行,但不会被立即运行。
(二)通过继承Thread类创建
使用这种方法也有分为两步:
(1)从Thread类派生子类,并重写其中的run()方法定义线程体。
(2)创建该子类的实例对象创建线程。
如下:
结果与上面的一致。
(三)实现Callable接口
使用这种方式创建线程有两大步:
(1)继承callable接口并实现call方法,并返回一个sum(随意)值
(2)执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果。 FutureTask 是 Future 接口的实现类
package com.test;
import java.util.concurrent.Callable;
public class calldemo implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i =0;i<33;i++){
System.out.println(i);
sum+=i;
}
return sum;
}
}
package com.test;
import java.util.concurrent.FutureTask;
public class calltest {
public static void main(String[] args) {
calldemo td = new calldemo();
//需要一个callable的实现
//1、执行callable 方式,需要futuretask实现类的支持,用于接受运算结果
FutureTask<Integer> fu = new FutureTask<>(td);
new Thread(fu).start();
try {
Integer sum = fu.get();
System.out.println(sum);
System.out.println("****");
}catch (Exception e){
e.printStackTrace();
}
}
}
(四)线程池
项目中常常使用
拓展——使用lamabd表达式便捷创建线程——
public class Lambda {
public static void main(String[] args) {
//匿名内部类创建多线程
new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建新线程1");
}
}.start();
//使用Lambda表达式,实现多线程
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"创建新线程2");
}).start();
//优化Lambda
new Thread(()-> System.out.println(Thread.currentThread().getName()+"创建新线程3")).start();
}
}
结果:
Thread-0创建新线程1
Thread-1创建新线程2
Thread-2创建新线程3
lamabd表达式创建的底层原理——
Lambda 表达式 (参数列表) -> { 方法体 }
实际上是一个函数式接口的实例。函数式接口是只有一个抽象方法的接口。在 Java 中,Runnable
接口就是一个函数式接口,它只有一个抽象方法 run()
,因此 Lambda 表达式 (参数列表) -> { 方法体 }
可以被赋值给 Runnable
接口的对象。
当你使用 Lambda 表达式 (参数列表) -> { System.out.println("t1"); }
作为 Thread
构造方法的参数时,编译器会将这个 Lambda 表达式转换为一个匿名内部类的实例,该匿名内部类会实现 Runnable
接口的 run()
方法,并在其中定义了方法体 System.out.println("t1"); }
。
因此,Lambda 表达式实际上就是一个 Runnable
接口的实现,它的方法体就是 run()
方法的具体实现。
(五)四种方法对比——
(1)runnable和callable 有什么区别?
(2)在启动线程的时候,可以使用run方法吗?
run()和 start()有什么区别?
(六)
总之记住了三点就好——
①Thread类相当于虚拟CPU
②run()方法相当于线程体
③实例对象作为驱动CPU运行的数据。
而线程由虚拟CPU和线程体组成。故二者缺一不可。现实中使用方案一较多。
三、线程的调度与基本控制
线程的调度
虽然概念上多个线程可以并发执行,但是目前计算机多数是单个CPU的,所以一个时刻只能运行一个线程。(因为切换速度快,常常认为是并发运行的),在单个CPU上以某种顺序运行多个线程称为线程的调度。
基本控制
Thread类里面的两个重要方法:sleep()、join()
t.sleep()=>把CPU让给优先级比t低的线程。自己先休眠。
t.join()=>使当前的线程等待直到线程t结束为止,该线程才恢复到Runnable状态。
四、线程同步
(一)问题引出
在多线程的程序中,当多个线程并发执行时,虽然各个线程的代码执行顺序是确定的,但是线程的相对执行顺序是不确定的。
在有些情况下如多线程对共享数据操作时,这种线程运行顺序的不确定性将会产生执行结果的不确定性,使共享数据的一致性被破坏,因此在某些应用程序中必须对线程的并发操作进行控制。
解决方案是以下的对象锁。
(二) 对象锁
临界区
一个程序的的各个并发线程中对同一个对象访问的代码段,称为临界区。
在Java中,一个临界区可以是一个语句块或一个方法,并用synchronized关键字标识。临界区的控制是通过对象锁进行的。
Java将每个由synchronized(someObjects){ }语句指定的对象someObjects设置一个锁,称为对象锁。对象锁是一种独占的排它锁,含义是——当一个线程获得了一个对象的锁后,便拥有该对象的操作权,其他任何线程不能该对象进行任何操作。
线程在要进入临界区时,首先通过synchronized(someObjects){ }语句测试并获得对象的锁,只有获得对象锁后才能继续执行临界区的代码,否则将进入等待状态。
例如:
使用对象锁注意事项:
(1)关于对象锁的返还。
对象的锁在如下几种情况下由持有线程返还。
• 当 synchronized()语句块执行完后。
• 当在synchronized()语句块中出现异常(Exception)。
•当持有锁的线程调用该对象的wait()方法。此时该线程将释放对象的锁,而被放人对象的 wait pool 中,等待某种事件的发生。
(2)共享数据的所有访问都必须作为临界区,使用synchronized 进行加锁控制。
对共享数据所有访问的代码,都应该作为临界区使用 synchronized 进行标识。
这样保证所有的操作都能够通过对象锁的机制进行控制。如果有一种访问操作未标记为synchronized,则这种操作将绕过对象锁,很可能破坏共享数据的一致性。
(3)用 synchronized保护的共享数据必须是私有的。
将共享数据定义为私有的,使线程不能直接访问这些数据,必须通过对象的方法。而对象的方法中带有由synchronized 标记的临界区,实现对并发操作多个线程的控制。
(4) 如果一个方法的整个方法体都包含在synchronized 语句块中,则可以把该关键字放在方法的声明中。
如在例10-5(c)的程序中,Push()方法也可定义为:
public synchronized void push( char c) l
data[ idx] = c;
data [ idx]
= с;
idx ++ ;
这种方式程序的可读性好,便于理解,因此比较常用。但控制对象锁的时间稍长,因此并发执行的效率会受到一定的影响,但影响不是很大。
(5) Java 中对象锁具有可重入性。
Java 运行系统中,一个线程在持有某个对象的锁的情况下,可以再次请求并获得该对象的锁,这就是对象锁具有可重入性的含义,锁的可重人性是很重要的,因为这可以避免单个线程因自己已经持有的锁而产生死锁。
(三)死锁的防治
死锁——
如果程序中多个线程互相等待对方持有的锁,而在得到对方锁之前都不会释放自己的锁,由此导致这些线程不能继续运行,这就是死锁。(循环之感)
Java 中没有检测与避免死锁的专门机制。因此完全由程序进行控制,防止死锁的发生。
应用程序可以采用的一般做法是:如果程序要访问多个共享数据,则要首先从全局考虑定义个获得锁的顺序,并且在整个程序中都遵守这个顺序。释放锁时,要按加锁的反序释放。
五、线程间的交互
线程间的交互 wait()和 notify()
有时,当某个线程进入 synchronized 块后,共享数据的状态并不满足它的需要,它要等待其他线程将共享数据改变为它需要的状态后才能继续执行。
但由于此时它占有了该对象的锁,其他线程无法对共享数据进行操作。(为此Java 引人wait()和 notity()。这两个方法是Java.lang. Object类的方法,是实现线程通信的两个方法。
wait()
如果线程调用了某个对象X的wait()方法X.wait(),则该线程将放人X的wait pool,并且该线程将释放×的锁。
notify()
当线程调用X的notify()方法使对象X的wait pool 中的一个线程移入lock pool,在lock pool
中等待X的锁,一旦获得便可运行。
notifyAll()
当线程调用X的notifyAll()方法把对象 wait pool中的所有线程都移人 lock pool。
因此用 wait(和 notify()可以实现线程的同步。
当某线程需要在 synchronized 块中等待共享数据状态改变时,可以调用wait()方法,这样该线程等待并暂时释放共享数据对象的锁,其他线程可以获得该对象的锁并进入 synchronized 块对共享数据进行操作。
当其操作完后,只要调用notify()方法就可以通知正在等待的线程重新占有锁并运行。
不建议使用的方法stop()
stop ()
stop()强行终止线程的运行,容易造成数据的不一致。如在堆栈的例子中,一个线程在压入值但末修改指针时被调用stop()方法终止,就将造成堆栈数据不一致。建议使用标志flag 终止其他线程。
六、线程状态与生命周期
六种线程状态
线程创建后,就开始了它的生命周期。在不同的生命周期阶段线程有不同的状态。对线程调用各种控制方法,就使线程从一种状态转换为另一种状态。
线程的生命周期主要分为如下6个状态:——
NEW(新建状态)、
RUNNABLE.(可运行状态)、
BLOCKED(线程阻塞等待监视器锁的线程状态)、
WAITING(等待线程的线程状态)、
TIMED WAITING(具有指定等待时间的等待线程的线程状态)、
TERMINATED(已终止线程的线程状态。线程已完成执行)
(线程的状态可以参考JDK中的Thread类中的枚举State)
线程状态相互转化
(记住上图便可记住所有状态与转化。————横着3态,竖着3态)
解释上文——
七、线程中的其他常见问题
新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
可以使用线程中的join方法解决
t. join(),阻塞调用此方法的线程进入timed_waiting直到线程t执行完成后,此线程再继续执行
注意上文使用的是lamabd表达式,详见点击这里。
notify()和 notifyALL()有什么区别?
notifyAll():唤醒所有wait的线程
notify():只随机唤醒一个 wait 线程
java中wait和sleep方法的异同?
共同点
wait()、wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态(BLOCKED)
不同点
1.方法归属不同
sleep(long)是Thread的静态方法
而wait(),wait(long)都是0bject的成员方法,每个对象都有
2.醒来时机不同
执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
wait(long)和 wait()还可以被 notify 唤醒,wait()如果不唤醒就一直等下去
它们都可以被打断唤醒
3.锁特性不同(重点)
wait 方法的调用必须先获取wait对象的锁,而sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
而sleep如果在synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)
如何停止一个正在运行的线程(4种)?
有四种方式可以停止线程
(0)原始方法
——程序运行完就自然结束。
(1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
package com.mozq.thread.interrupt;
/**
* 结束线程方法1:使用结束标志
* @author jie
*
*/
class StopThread implements Runnable{
private boolean exit = false;
public void setExit(boolean exit) {
this.exit = exit;
}
@Override
public synchronized void run() {
while(!exit) {
System.out.println(Thread.currentThread().getName() + "run...");
}
System.out.println(Thread.currentThread().getName() + "结束了...");
}
}
public class MyInterrupt2 {
public static void main(String[] args) {
StopThread stopThread = new StopThread();
Thread t = new Thread(stopThread);
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//业务逻辑。。。
//此处想结束线程
stopThread.setExit(true);
}
}
(2)使用stop方法强行终止(不推荐,方法已作废)
(3)使用interrupt方法中断线程
打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
打断正常的线程,可以根据打断状态来标记是否退出线程
八 、线程中并发安全问题
1、synchronized关键字的底层原理
synchronized关键字的底层原理——基础篇
一、synchronized为对象上锁
先来看一下实际中的线程安全问题——
抢票实现——
未上锁——导致线程竞争,超卖。
synchronized为对象上锁后——线程井然有序
二、synchronized底层分析——monitor
汇编分析——
javap-v xx.class 查看class字节码信息
深入monitor——
Monitor 被翻译为锁监视器,是由jvm提供,c++语言实现
当一个线程进入synchronized代码块之后,会经历以下3步——
详解monitor三个属性——
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
- WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
monitor总结——
当一个线程进入对象的synchronized代码块后,会让对象锁与monitor关联,检查monitor中的属性Owner是否为NULL,如果是,就让该线程持有对象锁,如果不为,则进入EntryLIst中,进入阻塞状态(BLOCKED),等待其他线程释放锁。如果线程调用了wait()方法,则进入waitSet中,进入等待状态(WAITING)等待被唤醒
总结synchronized基础篇
synchronized关键字的底层原理——进阶篇
锁升级(重量锁——>偏向锁/轻量级锁)
monitor是JVM提供的,而JVM属于系统级别的(也就是内核态的)。C++实现的而Java对象要使用moniter就需要完成内核态与用户态的转化。(内核态与用户态详情介绍请点击这里)同时进程的上下文切换,成本较高,性能低。
二、Java对象的内存结构
Java对象都是在堆中创建的
hotspot将对象分为3部分——对象头、实例数据、对齐填充
其中对象头中的Markword详细描述了对象是如何关联到monitor监视器的。
深入MarkWord
MarkWord对象头占4字节32位,里面各种状态——
对象如何关联到monitor的——
该对象对象头的Mark Word 中记录了指向 Monitor 对象的指针
再次回到monitor中,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
轻量级锁及流程——
假如此时来临了一个线程——
要去执行method1()方法,则该线程执行时就会创建一个锁记录——Local Record每个线程的栈帧都包含了一个锁记录的结构,内部就可以存储锁定对象的MarkWord。
交换成功——
如果CAS失败,交换失败,则(两种情况)——
情况一:多线程竞争导致交换失败
==>直接升级为重量级锁。一旦发生竞争就会升级为重量级锁。
情况二:锁重入导致失败
==>此时会在栈帧上再加一层Lock Record记录重入的锁
执行完了之后,会释放method2的锁,直接删除最上面的Lock Record即可
再次释放method1的锁(注意交换回数据)
偏向锁
引出——
注意上文说的轻量级锁在重入时,每重入一次就会执行CAS交换一次。性能还是不算太好。此时可以用偏向锁再次提升——
偏向锁只会第一次持有锁时CAS连接,并记录线程ID,重入时不会再CAS,只会判断锁的线程ID是否只自己的就行
总结——
强调一下——
一旦那发生竞争就升级为重量锁(monitor)。
2、JMM
定义——
JMM(Java Memory Model)——Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
内存分为两块——共享内存、工作内存
使用————
总结——
3、CAS 你知道吗?
介绍与使用CAS
自旋锁优劣——
- 因为没有加锁,所以线程不会陷入阻塞,效率较高
- 如果竞争激烈,重试频繁发生,效率会受影响
CAS 底层——
依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
总结——
乐观锁与悲观锁——
4、请谈谈你对 volatile 的理解
(1)volatile 性质一——保证线程间的可见性
运行上述代码,结果——
线程t1、t2可以运行,但是线程3还是出错
原因分析——
解决方案一——添加VM参数
解决方案二——使用volatile修饰stop
两者都能使线程t3运行——
则说明线程1的修改在线程3中生效了。
即volatile能够让一个线程对共享变量的修改对另一个线程可见
(2)volatile 性质二——禁止进行指令重排序
注意——指令在cpu读取,顺序不固定的,计组里面的知识
尤其是在高并发的情况下,情况4发生得更加多。为了阻止其发生,可用接下来的volatile的功能。——
解决方案——volatile修饰变量,禁止指令重排序
指令重排:用[volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,
阻止其他读写操作越过屏障,从而达到阻正重排序的效果
注意,变量修饰不能更改——
也就是volatile加在X上不行
总结volatile
5、什么是AQS?——抽象队列同步器AbstractQueuedSynchronizer
AQS基本工作方式——
(1)AQS-多个线程共同去抢这个资源是如何保证原子性的呢?
(2)AQS是公平锁吗,还是非公平锁?–>都可以实现
而在排队的锁就是公平锁
公平锁、非公平锁——
公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁