文章目录
- ==多线程基础==
- 进程
- 线程
- 浏览器的进程和线程(案例)
- 线程的异步和同步
- 多线程的优势
- ==多线程的实现方式==
- 第一种:继承Thread类
- 第二种:实现Runnable接口
- 第三种:通过Callable 和 Future 创建线程
- 第四种:通过线程池创建线程 Executers 和 ThreadPoolExecutor 两种方案
- 创建线程的方案一:通过Executors创建
- 方法一:newCachedThreadPool创建线程
- 方法二:newFixedThreadPool创建线程(创建固定个数的线程池)
- 方法三:newScheduledThread创建线程
- 方法四:newSingleThreadExecutor创建线程
- 案例:Executor类创建线程池
- 创建线程的方案二:ThreadPoolExecutor
- 案例
- ThreadFactory介绍
- Future类
- 线程停止执行可调用的方法
- ==线程池==
- 概述
- Thread类的相关方法
- 线程控制sleep、join、setDaemon
- Object类的等待和唤醒方法
- ==线程调度==
- 线程有两种调度模型
- java使用抢占式调度模型
- ==线程生命周期==
- 概述
- 案例:卖票
- 案例:卖票改进
- 多线程数据安全问题,买票问题解决
- ==线程安全(同步)==
- 同步代码块
- 同步方法
- 案例:同步代码块 和 同步方法
- ==lock锁==
- ==线程安全的类==
- ==生产者消费者案例==
- 概述
- 案例代码
- ==JMMjava内存模型==
- 概述
- 重排序
- as-if-serial语义
- happens-bofore原则
- 概述
- 1 程序顺序规则
- 2 监视器锁规则
- 3 Volatile变量规则
- 4 传递性
- 5 start()规则
- 6 join()规则
- 7 程序中断规则
- 8 对象终结规则
- 构造方法 和 析构方法 作用正好相反
- finalize()方法
- as-if-serial规则 和 happens-before规则区别
多线程基础
进程
- 概念:正在运行的程序
- 是系统进行资源分配和调用的独立单位
- 每一个进程都有它自己的内存空间和系统资源
线程
- 是进程中的单个顺序控制流,是一条执行路径
- 描述了执行一段指令所需的时间
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
单线程举例:打开记事本,给记事本写内容,当又进行设置纸张大小时,此时再给记事本写内容就写不进去了,这就证明这是一个单线程。 - 多线程:一个进程如果有多条执行路径,则称为多线程程序
多线程举例:扫雷程序,鼠标控制的游戏点击、页面右上角的时间控制。
多线程的运行安全体现在:原子性、可见性、有序性。
浏览器的进程和线程(案例)
浏览器中有渲染进程、插件进程、网络进程、浏览器主进程、GPU进程。
备注2022/8/11
- 调用start()方法可以使一个线程成为可运行的,但是它不一定立即开始运行。√
使用start方法只能将进程放到就绪队列中,并不能立即执行。 - 当一个线程因为抢占机制而停止运行,它被放在可运行队列的后面。
- 一个线程可能因为不同的原因停止(cease)并进入就绪状态。√
一个线程可能由于之前资源不可用导致进入阻塞状态,然后当资源可用的时候,就会进入到就绪态。
线程的时间片用完会从运行态转为就绪态。
优先级更高的线程会抢占当前线程,使当前线程进入就绪态。
线程的异步和同步
区别 | 同步 | 异步 |
概念 | 同步过程调用发出后,在没有得到结果之前,该调用不返回或不继续执行后续操作 多个线程同时访问同一个资源,一个线程访问结束之后,别的线程才能访问 | 异步过程调用后,调用者没有得到结果之前,就可以继续执行后续操作 调用结果,通过状态、通知和回调来通知调用者 |
操作 | 必须执行到底才能执行其他操作 | 操作可以同时执行 |
使用锁 | 多个线程使用同一把锁 | 多个线程在执行过程中使用不同的锁 |
多线程的优势
- 为了充分利用计算机资源,让多个程序在同时运行的过程中公平竞争资源,充分利用计算机的处理能力;
- 线程为多处理器系统中并行的使用硬件提供了一个自然而然的分解,同一个程序的多个线程可以在多个CPU上的情况下同时调度;
举例
CPU、内存、IO之间的差异:CPU运行的单位是cycle,时钟周期,一次简单的运行大约需要1-2个时钟周期,也就是1纳秒;内存一次寻址时间是1-10微秒,则CPU就需要等待1000-10000个时钟周期;硬盘寻址是1-10毫秒,寻址一次就需要等待上百万个时钟周期;一个线程等待IO的时候,可以让CPU去调度别的线程;
多线程的实现方式
- Thread类
- 在java.lang包下,使用不需要导包
- 线程是程序中执行的线程,java虚拟机允许应用程序同时执行多个执行线程。
第一种:继承Thread类
1 将一个类声明为一个Thread的子类
public class 子类 extends Thread{}
2 该子类应该重写Thread类中的run方法,
@Override
public void run(){
线程具体执行操作;
}
3 创建该子类的对象,
子类 对象名 = new 子类();
4 启动线程;
对象名.start();
使用start()方法启动。
void start()导致此线程开始执行,java虚拟机调用此线程的run方法
案例:
// 310-test1
// 测试类
public class Demo {
public static void main(String[] args){
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
// 直接调用MyThread类中的run方法并没有启动线程;
// m1.run();
// m2.run();
// 这种方式 m1执行完0-99的输出之后,m2才开始执行0-99的输出。
// void start()导致此线程开始执行,java虚拟机调用此线程的run方法
m1.start();
m2.start();
// 这种方式,输出结果不再是0-99 0-99 而变成: 0 0-17 1-7 18-99 8-99
}
}
// 继承Thread的子类
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0 ;i < 100;i++){
System.out.println(i);
}
}
}
第二种:实现Runnable接口
1 声明一个实现Runnable接口的类
public class 类xxx implements Runnable{}
2 类xxx中重写run方法
@Override
public void run(){
这里要获取线程的名字,不能直接使用getName(),因为本类没有继承Thread类,getName()是Thread类方法。
获取线程名字:Thread.currentThread().getName()
}
3 创建类xxx的对象
类xxx 对象名x = new 类xxx();
4 创建Thread类对象,把类xxx对象作为构造方法的参数
Thread 对象名th = new Thread(对象名x);
5 启动线程
对象th.start();
实现Runnable接口的方式创建线程的好处:
①没有继承Thread类,好处是可以有它自己的父类 ——避免了java单继承的局限性;
②创建实现Runnable接口的类,所创建的对象,作为参数传递到Thread构造方法,这样看成一个资源有多个线程。
适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想。
缺点
Runnable接口里面的run方法不能返回操作结果。
代码:
//310-test5
实现Runnable接口的类
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 100 ; i ++){
System.out.println(Thread.currentThread().getName() + ": " + i);
// 不能直接使用getName方法
}
}
}
测试:
public class Demo {
public static void main(String[] args) {
// 创建类xxx的对象
MyRunnable mr = new MyRunnable();
// 创建Thread类的对象
// 构造方法:Thread (Runnable target)
/* Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);*/
// 构造方法 Thread (Runnable target,String name)
Thread t1 = new Thread(mr,"pretty");
Thread t2 = new Thread(mr,"sunshine");
// 启动线程
t1.start();
t2.start();
}
}
第三种:通过Callable 和 Future 创建线程
为了解决Runnable接口中run方法不能返回操作结果的问题。java提供Callable接口。
涉及内容
- Callable接口
Callable接口实现线程创建,需要重写call方法; - FutureTask类
public class FutureTask< V > implements RunnableFuture< V >
该接口主要负责Callable接口对象操作,且RunnableFuture接口继承自Runnable、Future接口。
创建过程
1 创建实现Callable接口的类
public class 子类 implements Callable{}
2 重写Callable接口的call方法
@Override
public object call() throws Exception{
所要执行的操作
return 返回值;
}
3 创建子类对象
子类 对象名son = new 子类();
4 创建FutureTask对象 包装Callable对象
FutureTask ft = new FutureTask(对象名son);
5 启动线程 通过Thread(Runnable target) 创建线程 start()方法启动线程
new Thread(ft).start();
6 获取线程的返回值,通过FutureTask对象的get方法
ft.get();
第四种:通过线程池创建线程 Executers 和 ThreadPoolExecutor 两种方案
创建线程的方案一:通过Executors创建
类名 | 说明 |
newCachedThreadPool | 一个可缓冲线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程 |
newFixedThreadPool | 一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待 |
newScheduledThread | 一个定长线程池,支持定时及周期性任务执行 |
newSingleThreadExecutor | 一个单线程化的线程池,只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行 |
Executor方法的弊端
- FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度是Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM(Out of Memory)。
- CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM(Out of Memory)。
四种方法总结
- 四种方式都是Executors类中的方法。
- 线程池具体的操作,newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor都是execute方法,而newScheduledThread是schedule方法
- 四种方法创建的对象都是ExecutorService对象。
方法一:newCachedThreadPool创建线程
1 通过newCachedThreadPool创建ExecutorService对象,Executors.newCachedThreadPool()方法
ExecutorService es = Executors.newCachedThreadPool();
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
es.execute(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
});
方法二:newFixedThreadPool创建线程(创建固定个数的线程池)
1 通过newFixedThreadPool创建ExecutorService对象,Executors.newFixedThreadPool()方法
ExecutorService es = Executors.newFixedThreadPool();
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
es.execute(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
});
方法三:newScheduledThread创建线程
1 通过newScheduledThread创建ScheduledExecutorService对象,Executors.newScheduledThread()方法
ScheduledExecutorService ses = Executors.newFixedThreadPool();
2 通过ScheduledExecutorService对象的schedule(Runnable target)方法创建对象
ses.schedule(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
},3,TimeUnit.SECONDS)};
介绍
ScheduledFuture<?> ScheduledExecutorService对象.schedule(Runnable command,long delay,TimeUnit unit) 创建并执行在给定延迟后启动的单次操作。
Runnable command:要执行的任务;long delay:从现在开始延迟执行的时间;TimeUnit unit:延迟参数的时间单位。
方法四:newSingleThreadExecutor创建线程
1 通过newSingleThreadExecutor 创建ExecutorService 对象,Executors.newSingleThreadExecutor()方法
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
es.execute(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
});
案例:Executor类创建线程池
// 310-test5
public class ExecutorsDemo {
public static void main(String[] args) {
test4();
}
public static void test1(){
// 创建可缓冲的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0 ; i <10 ; i++){
int temp = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("ThreadName" + Thread.currentThread().getName() + ",i" + temp);
}
});
}
//输出结果 :创建线程的顺序和执行println语句的顺序不一样,按理说第一个创建的线程是thread-1,i0、第二个是thread-2,i1,但是第二个创建的线程是第五个执行println的。
// ThreadNamepool-1-thread-1,i0
//ThreadNamepool-1-thread-5,i4
//ThreadNamepool-1-thread-4,i3
//ThreadNamepool-1-thread-3,i2
//ThreadNamepool-1-thread-2,i1
//ThreadNamepool-1-thread-6,i5
//ThreadNamepool-1-thread-7,i6
//ThreadNamepool-1-thread-8,i7
//ThreadNamepool-1-thread-9,i8
//ThreadNamepool-1-thread-10,i9
}
public static void test2(){
// 创建定长线程池,超出的线程会在队列等待;
ExecutorService es = Executors.newFixedThreadPool(3);
for(int i = 0 ; i <10 ; i++){
int temp = i;
es.execute(new Runnable() {
@Override
public void run() {
System.out.println("ThreadName" + Thread.currentThread().getName() + ",i" + temp);
}
});
}
}
//输出结果 创建一个定长线程,最大并发数是3,其余线程会在队列中等待。
// 并发最大数是3, Thread.currentThread().getName()获得内容是1-3 说明并发线程数是3
// ThreadNamepool-1-thread-1,i0
//ThreadNamepool-1-thread-2,i1
//ThreadNamepool-1-thread-3,i2
//ThreadNamepool-1-thread-2,i3
//ThreadNamepool-1-thread-3,i4
//ThreadNamepool-1-thread-1,i6
//ThreadNamepool-1-thread-2,i5
//ThreadNamepool-1-thread-1,i8
//ThreadNamepool-1-thread-3,i7
//ThreadNamepool-1-thread-2,i9
public static void test3(){
// 创建定长线程池,且定时及周期性执行任务
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
for(int i = 0 ; i <10 ; i++){
final int temp = i;
ses.schedule(new Runnable() {
public void run() {
System.out.println("i:" + temp);
}
},3, TimeUnit.SECONDS);
}
}
// 输出结果 :表示延迟三秒
//ThreadNamepool-1-thread-5,i4
//ThreadNamepool-1-thread-1,i0
//ThreadNamepool-1-thread-2,i2
//ThreadNamepool-1-thread-4,i3
//ThreadNamepool-1-thread-3,i1
//ThreadNamepool-1-thread-4,i8
//ThreadNamepool-1-thread-2,i7
//ThreadNamepool-1-thread-1,i6
//ThreadNamepool-1-thread-5,i5
//ThreadNamepool-1-thread-3,i9
public static void test4(){
//1.创建单线程
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
newSingleThreadExecutor.shutdown();
}
//输出结果:结果依次输出,相当于顺序执行各个任务,执行shutdown 停止线程。
//ThreadNamepool-1-thread-1,i0
//ThreadNamepool-1-thread-1,i1
//ThreadNamepool-1-thread-1,i2
//ThreadNamepool-1-thread-1,i3
//ThreadNamepool-1-thread-1,i4
//ThreadNamepool-1-thread-1,i5
//ThreadNamepool-1-thread-1,i6
//ThreadNamepool-1-thread-1,i7
//ThreadNamepool-1-thread-1,i8
//ThreadNamepool-1-thread-1,i9
}
介绍
ExecutorService类的shutdown()方法,返回值是void,描述:启动有序关闭,其中先提交的任务将被执行,但不会接收任何新任务。
创建线程的方案二:ThreadPoolExecutor
ThreadPoolExecutor构造方法有7个参数:
参数名 | 介绍 |
int corePoolSize | 核心线程数量 要保留在池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut |
int maximuPoolSize | 最大线程数量 |
long keepAliveTime | 存活时间 当线程数大于核心线程数时,多于的空闲线程在终止之前等待新任务的最大时间 |
TimeUnit unit | 时间单位 |
BlockingQueue< Runnable > workQueue | 阻塞队列 执行任务之前用于保存任务的队列, |
ThreadFactory threadFactory | 拒绝策略 线程工厂:执行程序创建线程时使用的工厂方法。该方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等 |
RejectedExecutionHandler handler | 执行被阻止时使用的处理程序,也就是说队列和线程池都满了,线程池处于一种饱和状态,需要采取的一种策略处理提交的新任务。 默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。 |
ThreadPoolExecutor参数值介绍:
参数名 | 参数值说明 |
TimeUnit unit | 配合 keepAliveTime使用,参数值:TimeUnit.xxx其中xxx = DAYS HOURS MINUTES SECONDS MILLISECONDS MICROSECONDS MANOSECONDS |
BlockingQueue< Runnable > workQueue | ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue、LinedTransferQueue、LinkedBlockingDeque |
BlockingQueue参数具体值介绍:
阻塞队列名字 | 说明 |
ArrayBlockingQueue | 数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 链表结构组成的有界阻塞队列 |
LinkedTransferQueue | 链表结构组成的无界阻塞队列,和SynchronousQueue类似,还含有非阻塞方式 |
LinkedBlockingDeque | 链表解耦组成的双向阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列,即直接提交给线程不保持它们 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的无界阻塞队列,只有在延时期满时才能从中提取元素 |
RejectExecutionHandler参数介绍
策略 | 说明 |
AbortPolicy 默认 | 直接抛出异常 |
CallerRunsPolicy | 只用调用者所在线程来运行任务 线程调用运行该任务的execute本身 |
DiscardPolicy | 不能执行的任务将被删除 |
DiscardOldestPolicy | 丢弃队列里面最近的一个任务,并执行当前任务 如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序 |
threadPoolExecutor常用方法介绍
方法名 | 说明 |
public Future< ? > submit(Runnable task) | 提交一个可运行的任务执行,并返回一个表示该任务的Future 该方法继承自AbstractExecutorService类 Future 表示异步计算的结果 |
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue< Runnable > workQueue, ThreadFactory threadFactory) | 使用给定的初始参数和默认拒绝的执行处理程序 创建一个线程工厂 |
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue< Runnable > workQueue) | 使用给定的初始参数 创建一个线程工厂 |
思路
1 通过ThreadFactory创建线程工厂,重写 Thread newThread(Runnable r) 方法
ThreadFactory threadfactoryxhj = new ThreadFactory(){
@Override
public Thread newThread(Runnable r){ }
};
2 通过ThreadPoolExecutor创建线程池
ThreadPoolExecutor threadPoolExecutorxhj
= new threadPoolExecutor(corePoolsize核心线程数,maximumPoolSize最大线程数,
keepAliveTime等待时间,unit等待时间的单位,
workQueue阻塞队列,threadfactoryxhj创建线程的工厂);
3 使用自定义的线程工厂,使用匿名构造类的形式,submit参数是Runnable接口,run里面内容是对于创建的线程具体执行什么
threadPoolExecutorxhj.submit( new Runnable(){
@Override
public void run(){}
};
案例
package com.javaface430.test1;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadFactoryTest {
public static void main(String[] args) {
// 创建线程工厂
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
// 创建线程池中的线程
Thread thread = new Thread(r);
// 设置线程名称 hashcode方法是Object中的,用于返回对象的哈希码值
thread.setName("Thread-" + r.hashCode());
// 设置线程优先级(最大值:10)
thread.setPriority(Thread.MAX_PRIORITY);
// ......
return thread;
}
};
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0,TimeUnit.SECONDS, new LinkedBlockingQueue<>(),threadFactory);
// 参数说明:核心线程数 = 10;最大线程数 = 10;线程数多于核心线程数时,多于空闲线程等待新任务的时间 = 0;等待时间的单位 = 秒;阻塞队列 = 链表组成的有界阻塞队列;threadFactory = 创建线程的工厂
// 使用自定义的线程工厂
threadPoolExecutor.submit(new Runnable() {
// submit 提交一个可运行的任务执行;采用的匿名构造类的形式
@Override
public void run() {
// 获取当前正在执行的线程的引用
Thread thread = Thread.currentThread();
// 输出线程的名字以及线程的优先级
System.out.println(String.format("线程:%s,线程优先级:%d",thread.getName(), thread.getPriority()));
}
});
}
// 输出结果
// 线程:Thread-1879492184,线程优先级:10
}
ThreadFactory介绍
在软件包java.util.concurrent下,使用需要导包;
public interface ThreadFactory 根据需要创建新的线程;
具体实现类,需要重写public Thread newThread(Runnable r){} 方法。
最简单的案例:
class SimpleThreadFactory implements ThreadFactory{
@Override
public Thread newThread(Runnable r){
return new Thread(r);
}
}
Future类
在软件包java.util.concurrent包下,使用的时候需要导包;
public interface Future < V > 表示异步计算的结果。提供方法来检查计算是否完成,等待其完成,并检索计算结果;
常用方法
方法名 | 说明 |
boolean cancel(boolean mayInterruptIfRunning) | 尝试取消执行此任务 |
V get() | 等待计算完成,然后检索其结果 |
V get(long timeout,TimeUnit unit) | 如果需要等待最多在给定时间计算完成,然后检索其结果 |
boolean isCancelled() | 如果此任务在正常完成之前被取消,则返回true |
boolean isDone() | 如果此任务完成,则返回true |
线程停止执行可调用的方法
①程序中可以直接使用thread.stop()来强行终止线,调用之后创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放所有的子线程所持有的所有锁。
②sleep() Thread类的方法,主要是为了暂停当前线程,让出cpu,使线程进入阻塞状态,直到休眠时间满了,就进入就绪状态;
③yield() Thread类的方法,让线程进入就绪状态,让出CPU的使用权。
④join() 调用该方法的线程强制执行,其他线程处于阻塞状态,该线程执行完毕后,其他线程在执行。
线程池
概述
线程池是运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。
线程池好处:
好处 | 说明 |
降低资源消耗 | 通过重复利用已创建的线程,降低线程创建和销毁造成的消耗 |
提高响应速度 | 当任务到达时,任务可以不需要等待线程创建就能立即执行 |
提高线程的可管理性 | 线程是稀缺资源,无限制地创建,会消耗系统资源并降低系统的稳定性。 |
两个小问题:
- 为什么重写run()方法?
run()是用来封装被线程执行的代码 - run方法和start方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用,无法达到多线程的目的。一个线程可以多次调用run方法。
start():启动线程,轮到该线程执行由JVM调用此线程的run方法。一个线程只能调用一次start方法。
Thread类的相关方法
方法名 | 说明 |
void setName(String name) | 将此线程的名称更改等于参数name |
String getName() | 返回此线程的名称 |
构造方法 Thread(Runnable target,String name) Thread(Runnable target) | 构造方法含义 通过构造方法设置线程名称 创建线程 |
static Thread currentThread() | 返回当前正在执行的线程对象的引用 |
Thread.currentThread.getName() | 获取当前正在执行的线程对象的名称 |
public final int getPriority() | 返回该线程的优先级 |
public final void setPriority(int newPriority) | 设置该线程的优先级为newPriority |
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 也就是当一个线程调用了join方法,则其他线程必须等待这个线程执行完毕之后才能执行 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,java虚拟机将退出 |
extends Object public int hashcode() | 返回对象的哈希码值 |
注意:
- getName方法是在继承自Thread的子类中使用的
没有设置线程名称的时候,默认的线程名称是 Thread-x
线程名称是Thread-x的原因: 因为子类使用无参构造方法创建对象,会自动调用父类的无参构造方法。
父类的无参构造方法:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
- setName方法是在 测试类中书写
对象.setName(String s)实现线程名的设置
要想使用Thread类的带参构造方法直接在创建线程的时候设置线程名,需要在其子类中定义带参构造方法,并使用super(参数);
// 创建继承Thread类的子类
public class 子类 extends Thread{
// 构造方法
public 子类(String name){
super(name);
}
}
- 获取main方法的线程的名称
static Thread currentThread() 返回当前正在执行的线程对象的引用
Thread.currentThread.getName() 获取当前正在执行的线程对象的名称
案例:
// 310-test2
测试:
public class Demo {
public static void main(String[] args){
// 无参构造方法给出默认值
/* MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
m1.setName("pretty");
m2.setName("sunshine");
*/
/* setName的底层代码,说明它是一个同步方法。
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name; 最关键
if (threadStatus != 0) {
setNativeName(name);
}
}
*/
/*
m1.start();
m2.start();
// 没有设置线程名称 有默认的,是Thread-x
// 为什么线程名称是Thread-x 因为子类使用无参构造方法创建对象,会自动调用父类的无参构造方法。*/
// 带参构造方法
/* MyThread m1 = new MyThread("one");
MyThread m2 = new MyThread("two");
m1.start();
m2.start();*/
System.out.println(Thread.currentThread().getName());
}
}
Thread的子类:
public class MyThread extends Thread{
public MyThread(){}
public MyThread(String name){
super(name);
}
@Override
public void run() {
for(int i = 0 ;i <100 ; i++){
System.out.println(getName() + ": " + i);
}
}
/* Thread的构造方法、成员方法getName、
private volatile String name;
// 该成员变量使用volatile修饰,说明该成员变量可能会被其他线程所修改。
构造方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
this.name = name;
}
private static int threadInitNumber; // 0,1,2...
private static synchronized int nextThreadNum() {
return threadInitNumber++; // 0,1....
}
成员方法
public final String getName() {
return name;
}
* */
}
线程控制sleep、join、setDaemon
方法名 | 说明 |
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 也就是当一个线程调用了join方法,则其他线程必须等待这个线程执行完毕之后才能执行 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,java虚拟机将退出 |
注意:
- 三个方法都是Thread类的方法
- sleep在继承自Thread类的子类中使用; Thread.sleep()
- join在测试类中使用;Thread对象.join()
- setDaemon在测试类中使用;Thread对象.setDaemon()
代码:
// 310-test4
sleep:
public class ThreadSleep extends Thread {
@Override
public void run() {
for(int i = 0 ;i < 100 ; i++){
System.out.println(getName() + ": " + i);
// 使用sleep方法 使每次执行之后的线程休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
join:
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();
tj1.setName("康熙");
tj2.setName("四阿哥");
tj3.setName("八阿哥");
tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
}
}
setDaemon:
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();
td1.setName("关羽");
td2.setName("张飞");
// 设置主线程为刘备
Thread.currentThread().setName("刘备");
// 设置守护进程
td1.setDaemon(true);
td2.setDaemon(true);
td1.start();
td2.start();
for(int i = 0; i < 10 ; i++){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
// 案例背景:由于关羽、张飞、刘备桃园三结义,刘备为主线程,所以当刘备执行结束时,关羽张飞也应该停止。
}
}
注意
把某些线程设置为守护线程, 当主线程停止时,其余设置为守护进程的不是立即消失,而是等一小段时间再消失。
Object类的等待和唤醒方法
方法名 | 说明 |
void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或者notifyAll()方法 |
void notify() | 唤醒正在等待 对象监视器 的单个线程 |
void notifyAll() | 唤醒正在等待 对象监视器 的所有线程 |
线程调度
线程有两种调度模型
- 分时调度模型:所有线程轮流获得CPU的使用权,平均分配每个线程占用CPU的时间片;
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,则随机选择一个。优先级高的线程获取的CPU时间片相对多一些。
java使用抢占式调度模型
- 解释:假设只有一个cpu,它在某一个时刻只能执行一条指令,线程只有得到CPU的时间片(使用权)才可以执行指令;多线程程序的执行具有随机性,谁抢到CPU的使用权是不一定的。
- Thread类中获得和设置线程优先级的方法:
public final int getPriority():返回该线程的优先级
public final void setPriority(int newPriority):更改此线程的优先级
注意:
- 线程默认的优先级是 NORM_PRIORITY = 5,最大的优先级:MAX_PRIORITY = 10,最小的优先级:MIN_PRIORITY = 1
- 线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到想要的效果。
代码:
// 310-test3
public class Demo {
public static void main(String[] args){
// ThreadPriority该类是继承自Thread类的子类 自己创建的。
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();
tp1.setName("rose");
tp2.setName("lily");
tp3.setName("sunflower");
/* tp1.start();
tp2.start();
tp3.start();*/
// 获取线程的优先级
/*System.out.println(tp1.getPriority()); // 5
System.out.println(tp2.getPriority()); // 5
System.out.println(tp3.getPriority()); // 5*/
// 设置参数优先级
// tp1.setPriority(10000);//IllegalArgumentException
// 表示优先级参数不在指定范围内。优先级范围是 MIN_PRIORITY = 1 和 MAX_PRIORITY = 10
// NORM_PRIORITY= 5
tp1.setPriority(1);
tp2.setPriority(5);
tp3.setPriority(10);
tp1.start();
tp2.start();
tp3.start();
}
}
线程生命周期
概述
包括五个部分:
一:新建——创建线程对象
二:就绪——有执行资格,没有执行权
三:运行——有执行资格,有执行权
四:阻塞——没有执行资格,没有执行权
五:死亡——线程死亡,变成垃圾
案例:卖票
需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票,请设计一个程序模拟该电影院买票。
// 310-test6
类:
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while(true){
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
}
测试代码:
public class Demo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread th1 = new Thread(st,"窗口1");
Thread th2 = new Thread(st,"窗口2");
Thread th3 = new Thread(st,"窗口3");
th1.start();
th2.start();
th3.start();
}
}
案例:卖票改进
需求:每次出票时间100毫秒,用sleep方法实现
代码出现问题:
- 相同的票出售了多次
- 出现售卖负数票的情况
问题的原因:
- 线程执行的随机性导致的。
// 310-test7
public class SellTicket implements Runnable {
private int tickets = 100;
// @Override
// public void run() {
// while(true){
// if(tickets > 0){
// // 通过sleep方法来模拟出票时间
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
// tickets--;
// }
// }
// }
// 问题分析:
// 采用sleep模拟出票时间的时候,出现问题
** 问题1 : 相同的票出现多次
// 视频中相同票出现很多次的情况,在自己电脑上运行 出现的是 窗口1售卖所有的票。
// 证实是由于jdk版本问题导致的问题。
// @Override
// public void run() {
// while(true){
// // tickets = 100
// // t1 t2 t3
// // 假设t1线程抢到CPU的执行权
// if(tickets > 0){
// // 通过sleep方法来模拟出票时间
// try {
// Thread.sleep(100);
// // t1 需要等待100毫秒
// // t2 抢到了CPU的执行权,t2开始执行,执行到这里,t2也会等待100毫秒
// // t3 抢到了CPU的执行权,t3开始执行,执行到这里,t3也会等待100毫秒
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// // 假设 按照抢到CPU的执行权的顺序 解释等待的100毫秒
// // t1 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
// // t2 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
// // t3 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
// System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
// // 按理说这里应该是 和 上一代码一起执行的,但是由于三个线程同时运行,这里也会被依次执行三次,最终票变成了97
// tickets--;
// }
// }
// }
** 问题2 : 输出结果售卖的票 变成负数
@Override
public void run() {
while(true){
// tickets = 1
// t1 t2 t3
// 假设t1线程抢到CPU的执行权
if(tickets > 0){
// 通过sleep方法来模拟出票时间
try {
Thread.sleep(100);
// t1 需要等待100毫秒
// t2 抢到了CPU的执行权,t2开始执行,执行到这里,t2也会等待100毫秒
// t3 抢到了CPU的执行权,t3开始执行,执行到这里,t3也会等待100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 假设 按照抢到CPU的执行权的顺序 解释等待的100毫秒
// t1 抢到CPU的执行权,在控制台输出,窗口1正在出售第1张票
// 继续执行tickets--,这时tickets = 0;
// t2 抢到CPU的执行权,在控制台输出,窗口1正在出售第0张票
// 继续执行tickets--,这时tickets = -1;
// t3 抢到CPU的执行权,在控制台输出,窗口1正在出售第-1张票
// 继续执行tickets--,这时tickets = -2;
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
}
多线程数据安全问题,买票问题解决
判断多线程程序是否会有数据安全问题的标准
- 是否 是多线程环境
- 是否 有共享数据
- 是否 有多条语句操作共享数据
解决多线程安全问题:
- 基本思想:让程序没有安全问题的环境。
实现:
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行。
- Java提供了同步代码块的方式来解决。
代码:
// 310-test8
售票类:
public class SellTicket implements Runnable {
private int tickets = 100;
// 要想实现锁住程序,必须使用同一把锁,也就是同一个Object对象
private Object obj = new Object();
@Override
public void run() {
while (true) {
// tickets = 100
// 线程t1 t2 t3
// 假设t1抢到了cpu执行权
// 假设t2抢到了cpu执行权 继续向下执行 发现被锁了 只能等待
# 使用new Object()这样当每一个线程进来的时候,都会创建一个新的锁,这样是锁不住的。
也就是当synchronized的参数是new Object()的时候,是不能实现锁住程序的,因为每次进来都是新的obj。
synchronized (obj) {
// t1 进来,把这段代码锁起来了
if (tickets > 0) {
// 通过sleep方法来模拟出票时间
try {
Thread.sleep(100);
// t1休息100毫秒 此时假设t2抢到了cpu执行权
} catch (InterruptedException e) {
e.printStackTrace();
}
// 窗口1正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--; // tickets = 99
}else{
break;
}
// t1出来,锁被释放。
}
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread th1 = new Thread(st,"窗口1");
Thread th2 = new Thread(st,"窗口2");
Thread th3 = new Thread(st,"窗口3");
th1.start();
th2.start();
th3.start();
}
}
线程安全(同步)
同步的好处和弊端:
好处:解决了多线程的数据安全问题。
弊端:当线程很多时,每个线程都会去判断同步上的锁,会造成资源的浪费,会降低程序的运行效率。
死锁
概念:
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况。
某一个同步代码块同时拥有“两个以上对象锁”时,就可能会发生死锁问题。
产生死锁的四个必要条件:
条件 | 解释 |
互斥条件 | 一个资源每次只能被一个进程使用 |
请求与保持条件 | 一个进程因请求资源而阻塞时,对已获得的资源保持不放 |
不可剥夺条件 | 进程已经获得的资源,在未使用完毕之前,不能强行剥夺 |
循环等待条件 | 若干进程之间形成一种头尾相接的循环等待资源关系 |
同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现。
格式:
synchronized(obj){
多条语句操作共享数据的代码。
}
synchronized(obj):将相当于给代码加锁了,任意对象obj就可以看成是一把锁
内置锁
普通方法中的同步代码块的锁是:this
静态方法中的同步代码块的锁是:类名.class
解释
obj称之为同步监视器,可以是任意对象,推荐使用共同资源作为同步监视器;
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是对象本身、或者是class(反射中讲解)
同步监视器的执行过程:
第一个线程访问,锁定同步监视器,执行其中代码;
第二个线程访问,发现同步监视器被锁定,无法访问;
第一个线程执行完毕,解锁同步监视器;
第二个线程访问,发现同步监视器没有被锁定,然后锁定并访问。
同步方法
同步方法:就是把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){}
同步静态方法:就是把synchronized关键字加到static方法上
格式:
public static synchronized 修饰符 方法名(方法参数){}
案例:同步代码块 和 同步方法
// 310-test9
public class SellTicket implements Runnable {
// private int tickets = 100;
// 将sellTicket方法设置为static,那么其中的变量tickets也得是static修饰
private static int tickets = 100;
// 要想实现锁住程序,必须使用同一把锁,也就是同一个Object对象
private Object obj = new Object();
private int x = 0;
@Override
public void run() {
while (true) {
if (x % 2 == 0) {
// 原始的是: synchronized (obj) {
// 使用方法sellTicket出现错误,这里改为this就可以了
// 视频中是这样的,但是自己电脑没有出现问题。
// synchronized (this) {
// 使用静态方法 视频出错,
// 这里改为 synchronized (方法名.class) {
// 这种形式 在自己电脑上报错
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
} else {
break;
}
}
} else {
sellTicket();
if (tickets == 0) {
break;
}
}
x++;
}
}
// 视频中,一个采用方法,一个直接书写,会出现两个窗口同时售卖一张票的情况,原因是 if 和 else 的锁不一样,方法的锁是this,但是现在却没有问题,可能的原因是:jdk视频是1.9 jdk自己是1.8
/*private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}*/
// 同步静态方法
// 视频中使用静态方法 没有加锁 会报错,但是实际操作中却没有报错
// 视频中给静态方法加synchronized 但是还是报错
// 需要将if中的锁改为sellTicket.class
private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
lock锁
- interface Lock 说明它是接口。
- 它实现提供比 synchronized方法和语句 更广泛的锁定操作。
- 它提供了获得锁和释放锁的方法:void lock()获得锁;void unlock() 释放锁
- 它是接口不能直接创建对象,所以需要它的实现类,使用ReentrantLock(可重入锁)
- ReentrantLock():创建一个ReentrantLock的实例
理解:
加锁和释放锁的工作:在Runnable的实现类中加的
一般采用如下形式加锁以及释放锁:
private Lock lock = new ReentrantLock();
try{
lock.lock();
}finally{
lock.unlock();
}
代码:
//311-test1
public class SellTicket implements Runnable {
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread.currentThread().getName() 获取当前正在运行的线程的名字
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
ticket--;
}
}finally {
lock.unlock();
}
}
}
}
线程安全的类
StringBuffer
- 线程安全,可变的字符序列。
- jdk5之后被StringBuilder替代,它被设计为使用一个线程。
- 一般情况下使用StringBuilder类,因为它支持与StringBuffer相同的所有操作,但它更快,因为它不执行同步。
Vector
- implements List,说明它是List体系的集合,使其成为java Collections Framework的成员;
- Vector类实现了可扩展的对象数组,说明和ArrayList用法相同。
- 两者的区别:Vector被同步,如果不需要线程安全,建议使用ArrayList替代Vector;
HashTable
- implements Map< K, V >说明它和HashMap差不多,使其成为java collections Framework的成员;
- 该类实现了一个哈希表,它将键映射到值。任何非null对象都可以用作键值或值;
- Hashtable被同步,如果不需要线程安全,建议使用HashMap替代Hashtable;
总结:
- 1 上述三个类 其中的方法都加了synchronized关键字,说明它是同步的,也就是线程安全的;
- 2 StingBuffer在多线程环境中被使用,剩余两个Vector和Hashtable被xx替代。
1 Vector可以被这个方法所替代。
Collections.synchronizedList(new ArrayList<>())
2 Hashtable可以被这个方法所替代
Collections.synchronizedMap(new HashMap<K,V>())
生产者消费者案例
概述
- 生产者消费者模式是一个十分经典的多线程写作模式
- 生产者消费者问题,包含两个线程:①生产者线程用于生产数据 ②消费者线程用于消费数据
- 生产者消费者关系解耦 :生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为。
案例代码
//311-test2
// 奶箱类 表示共享数据区
public class Box {
private int milk;
// 定义成员变量,表示奶箱的状态
private boolean state = false;
// 报异常IllegalMonitorStateException 因为 wait方法没有使用在同步中。
// 存储牛奶 同步方法
public synchronized void put(int milk){
// 如果奶箱有牛奶,等待消费
if(state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有牛奶,就生产牛奶
this.milk = milk;
System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");
// 生产完毕之后,修改奶箱状态
state = true;
// 唤醒其他等待的线程
notifyAll();
}
// 获取牛奶 同步方法
public synchronized void get(){
// 如果奶箱没有牛奶,就等待
if(!state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果 有就可以取牛奶
System.out.println("用户拿到第" + this.milk + "瓶奶");
// 这里不应该milk--吗?
// 消费完毕之后,修改奶箱状态
state = false;
// 唤醒其他线程
notifyAll();
}
}
// 生产者类
public class Producer implements Runnable {
private Box b;
public Producer(Box b){
this.b = b;
}
@Override
public void run() {
for(int i=1;i<=5;i++){
b.put(i);
}
}
}
// 消费者类
public class Customer implements Runnable {
private Box b;
public Customer(Box b){
this.b = b;
}
@Override
public void run() {
while(true){
b.get();
}
}
}
// 测试类
public class Demo {
public static void main(String[] args) {
Box b = new Box();
Producer p = new Producer(b);
Customer c = new Customer(b);
Thread t1 = new Thread(p,"生产者");
Thread t2 = new Thread(c,"消费者");
t1.start();
t2.start();
// 输出结果
// 送奶工将第1瓶奶放入奶箱
//用户拿到第1瓶奶
//送奶工将第2瓶奶放入奶箱
//用户拿到第2瓶奶
//送奶工将第3瓶奶放入奶箱
//用户拿到第3瓶奶
//送奶工将第4瓶奶放入奶箱
//用户拿到第4瓶奶
//送奶工将第5瓶奶放入奶箱
//用户拿到第5瓶奶
}
}
JMMjava内存模型
概述
- 概念:
java内存模型即Java Memory Model,简称JMM,它定义了java虚拟机(JVM)在计算机内存(RAM)中的工作方式。
用于描述java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量的底层细节。
JVM是整个计算机虚拟模型,则JMM隶属于JVM。 - 作用:
java线程之间的通信采用的是共享内存模型,JMM决定一个线程对共享变量的写入何时对另一个线程可见,以及如何同步访问共享变量。
JMM定义了线程与主内存之间的抽象关系。
Java内存模型JMM:为了解决在并发环境下由于CPU缓存、编译器和处理器的指令重排序 导致的可见性、有序性问题。 - 其他:
JMM解决指令重排其实是定义了一项happens-before规则。
不同的平台内存模型是不同的,可以把内存模型理解成在特定的操作下,对特定的内存或高速缓存进行读写访问的过程的抽象。
java内存模型图示
线程执行过程读取主内存与工作内存过程:
线程执行的时候,①从主内存读数据 ②load到工作内存中的副本中 ③ 传给处理器执行 ④给工作内存中的副本赋值 ⑤将工作内存的值赋值给主内存。
线程之间的通信机制是 共享内存和消息传递。
共享内存:线程之间共享内存的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的就是通过共享对象进行通信;
消息传递:线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,典型消息传递方式wait和notify。
线程之间的同步机制:
共享内存的并发模型中,同步是显式的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行(比如使用synchronized);
消息传递的并发模型中,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
重排序
重排序的设计角度:程序员而言,需要内存模型易于理解、编程,需要一个强内存模型来编码;编译器和处理器而言,希望约束少一点,这样不会限制他们的执行效率。
提出原因:
为了提高并行度、优化程序性能,编译器和处理器会对代码进行指令重排序。
重排序是JMM设计策略:
①对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序;而对于不会改变执行结果的重排序,JMM不做处理;
②JMM为了满足对编译器和处理器的约束尽量少的条件,遵循规则是:只要不改变程序的执行结果,编译器和处理器想怎么优化就怎么优化;
java内存模型底层是通过内存屏障(memory barrier)来禁止重排序的。
as-if-serial语义
含义:
编译器和处理器不管如何重排序,单线程(程序)的执行结果都不能被改变;
编译器、runtime和处理器都必须遵守as-if-serial语义;
编译器和处理器不会对存在数据依赖的的操作进行重排序,这会改变执行结果。
案例:
int a=1;
int b=2;
int c=a+b;
//分析:
a和c、b和c之间都存在数据依赖关系,最终执行的指令序列中,c不能排到a和b的前面。
happens-bofore原则
概述
- 出现原因:
为了解决编译器和处理器的指令重排序; - happens-before规则:前一个操作的结果对后续操作是可见的。
- JMM设计分为两部分:程序员 + 编译器处理器
JMM为程序员提供的视角是:按顺序执行,且满足一个操作happens-before于另一个操作,即第一个操作执行结果对第二个操作可见。(JMM向程序员做出的保证)
JMM为编译器和处理器提供的视角是:在不改变程序结果的前提下,编译器和处理器怎么优化都可以。 - 目的:
都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 - happens-before本质:
一种可见性,A happens-before B 意味着A事件对B事件来说是可见的,无论A事件和B事件是否发生在同一个线程里。
假设事件A发生在线程1上,事件B发生在线程2上,happens-before规则保证线程2上能看到A事件的发生。 - 一个happens-before规则对应于一个或多个编译器和处理器重排序规则。
- happens-before定义了8大原则。
1 程序顺序规则
一个线程中,按照程序顺序,前面的操作happens-before于后续的任意操作。
xhj理解:即前面操作的结果对后续操作是可见的
double pi = 3.14; //A
double r = 1.0; // B
double area = pi * r * r; // C
// 操作A happens-before 于操作B
2 监视器锁规则
一个unlock操作 先行发生于 后面对同一个锁的lock操作;
该锁说的就是synchronized。
xhj:解锁操作 执行可见于 之后(时间顺序)对同一把锁的加锁操作。
synchronized(this){ // 自动加锁
// x共享变量,初始值是10
if(this.x < 12){
this.x = 12;
}
}
// 自动解锁
// 加锁和释放锁操作都是编译器帮我们实现的。
// 也就是说线程A对共享变量的修改,在线程A释放锁之后,线程B进入代码块的时候,能够看到共享变量修改后的值 x = 12
3 Volatile变量规则
对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
xhj:对volatile域的写,对于后续对这个volatile域的读 是 可见的。(感觉像是禁用缓存的意思)
4 传递性
如果A happens-before B,且 B happens-before C,那么A happens-before C。
5 start()规则
Thread对象的start操作,先行于 此线程的任何一个动作;
6 join()规则
线程中所有的操作 先行于 对此线程的终止检测(Thread.join结束、Thread.isAlive返回检测结果)
7 程序中断规则
对线程interrupt方法调用 优先发生于 被中断线程的代码检测到该中断时间的发生( Thread.interrupt() 方法检测 )
8 对象终结规则
一个对象的初始化完成(构造函数执行结束) 先行于 发生它的finalize()方法的开始。
构造器的最后一个操作 先行于 析构器的第一个操作。
构造方法 和 析构方法 作用正好相反
构造方法 | 类构造对象时调用的方式,通常用来实例化对象以及开辟存储空间。 |
析构方法 destruct | 用来做清理垃圾碎片的工作 例如:在建立对象的时候,用new开辟了一块内存空间,应在退出前在析构方法中将它释放。 |
finalize()方法
在java的Object类中,protected void finalize() 是一个受保护的方法;
由于其在Object类中,且所有类都继承自Object类,那么任何类都能够覆盖这个方法,该方法用于释放对象所占有的相关资源。
特点:
- 垃圾回收器是否会执行该方法以及何时执行该方法,都是不确定的;
- finalize()方法有可能使对象复活,使对象恢复到可触及状态;
- 垃圾回收器在执行finalize()方法的时候,假设出现异常,垃圾回收器不会报告异常,程序继续正常执行。
as-if-serial规则 和 happens-before规则区别
区别 | as-if-serial | happens-before |
保证xxx的执行结果不被改变 | 单线程内的程序 | 正确同步的多线程 |
执行顺序 | 单线程程序,幻觉 = 按程序的顺序执行 | 多线程程序,幻觉 = 按照该规则顺序执行 |
作用 | 不改变执行结果的前提下,尽可能提高程序的执行并行度 | 不改变执行结果的前提下,尽可能提高程序的执行并行度 |