线程、进程、程序之间的关系:
程序:是一段用某种语言编写的具有某种功能的指令集合。
进程:正在运行的程序,Windows系统中,资源调度的最小单位。
线程:进程的进一步细化,是一个进程内部的最小执行单元,是操作系统进行任务调度的最小 单元,属于进程。
一个进程可以有多个线程,一个线程只能属于一个进程。
java创建线程的方式有三种:
①继承Thread类,重写run方法;②实现Runnable接口,重写run方法;③实现Callable接口,重写call方法;
使用实现Callable接口方式,在调用线程时需要借助FuTure类获取实现类对象的返回值,然后用传入Thread对象实例中。如下:
//创建实现Callable接口类的对象
CallableDemo callableDemo = new CallableDemo();
//借助FutureTask获取返回值
FutureTask<Integer> futureTask = new FutureTask(callableDemo);
//利用Thread开启线程
Thread t = new Thread(futureTask);
//开启线程
t.start();
//futureTask.get()获取call方法的返回值
System.out.println(futureTask.get());
Thread中会有一些线程相关的方法:
start()开启线程;
setName(String name)设置线程的名称;
getName()获取线程名称;
setPriority(int newPriority) 设置线程的优先级;
getPriority()获得线程的优先级;
join()使线程处于等待状态(抛异常);
sleep(long millis)使线程进入休眠状态(单位为:毫秒,抛异常);
currentThread()返回当前正在执行线程的对象的引用。
线程有新建、就绪、执行、阻塞、死亡五种状态。
java中的线程可以分为守护线程可用户线程。当有用户线程运行时守护线程就会一直运行,当所有的用户线程都终止时守护线程才会终止。
java是支持多线程的。
wait()是Object类中的方法,sleep()是Thread中的方法;
两个方法都可以使线程进入等待状态,但是wait后需要使用notify或者notifyAll唤醒,sleep在休眠结束后自动苏醒;
wait后释放掉锁(不释放掉锁,其他线程如何获取锁,又如何调用notify或者notifyAll方法将其唤醒呢),sleep只释放CPU资源,不释放锁;
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep没有限制。
wait,notify是用于线程之间的协调通信,wait表示让线程进入阻塞状态,notify表示让阻塞的线程被唤醒。wait和notify必然是成对出现的,如果一个线程wait后必然需要另一个线程通过notify进行唤醒,从而达到线程之间的通信,在多线程中要实现多线程之间的通信除了使用管道流外只能使用共享变量的方法实现,也就是说线程t1访问和修改共享变量s,线程t2获得修改后的共享变量s,从而完成多线程的通信,但是多线程本身具有并行执行的特性,也就是说同一个时刻多个线程同时执行,这种状态下线程t2在访问共享变量s时必须知道s是否已经被修改,否则需要等待。同时t1在修改后还需要把处于等待的线程t2唤醒,所以在这种状态下实现线程通信必须由一个静态条件去控制线程什么时候等待,什么时候被唤醒。而synchronize同步关键字就可以实现这个互斥的条件,也就是使用共享变量来实现多个线程通信的场景里,参与通信的线程必须要获得共享锁资源才能有资格进行修改共享变量,修改完成后释放锁,其他线程就可以再次竞争同一个共享锁资源,然后访问修改后的共享变量。从而完成线程之间的通信,synchronized同步锁可以实现线程之间的互斥从而实现条件等待和条件唤醒,为了规范wait和notify的使用,jdk强制要求将wait和notify写在同步代码块中,否则运行时会报出IllegalMonitorException异常。wait和notify非常适合实现生产者和消费者的模型。
多线程:就是一个程序包含多个执行单元,一个程序内同一时间可以多个线程执行多个不同的任务。
当有多个任务需要执行时,当需要一些后台程序时,当程序需要一些等待任务时。
多线程的优点:提高CPU的利用率,提高程序的响应速率,改善程序结构,将复杂的程序结构分给不同的线程,独立运行。
多线程的缺点:线程过多时会影响性能,线程越多需要的存储空间也越多,线程之间对共享资源的访问会相互影响,必须解决资源争夺问题。
线程安全就是,在多线程当中,有多个线程同时访问并操作同一个共享资源。这样会导致数据错乱。例如从银行卡里取钱,可以线下去ATM机中取现金,也可以用微信或者支付宝线上提现,线程安全问题就是,同时使用线上、线下两种方式,银行卡的金额只减少一次。
在单核CPU中所有的线程时串行,但是操作系统中有个组件叫做任务调度器,会将CPU的时间片分给不同的线程,而CPU在线程之间切换的非常快,给人的感觉就好像是线程在同时执行。总结就是微观串行,宏观并行。一般将CPU轮流执行线程的做法叫做并发。并行是相对于多核CPU而言的,多个线程进入不同的CPU执行。举个例子:一个人轮流吃三个馒头(并发),和三个人吃三个馒头(并行)。
为了解决多个线程同时对共享资源操作,可能会引起数据混乱的问题,引入线程同步机制。
线程同步:当一个线程调用某种功能时,为保证数据的一致性,其他线程不能在调用此功能。
同步机制就是要求方法需要加锁,确保同一时间只有一个线程操作共享资源。
同步锁:同步锁可以是任意对象,但是必须唯一,确保多线程获取的是同一个锁对象。
synchronized | Lock |
---|---|
关键字 | 接口 |
不需要手动释放锁 | 需要手动释放锁 |
不能中断 | 可以中断 |
线程通信:多个线程之间相互牵制、相互调用,相互作用。
Object提供的wite()方法:当前线程处于阻塞状态,并释放同步监视器。
notify()方法唤醒其他线程,当有多个线程时优先唤醒优先级高的。
notifyAll()方法唤醒所有阻塞线程。
一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
JMM(内存模型):是java规范的一种工作模式。将内存分为主内存和工作内存,变量存储在主内存中,线程在操作变量时,需要将变量从主内存中复制到工作内存中,操作完成后又写回到主内存中。主内存的读写速度慢,工作内存的读写速度快。
这样做的好处:提高效率;缺点:多个线程同时修改了同一共享变量,会出现数据不准确的问题。
read(读取):从主内存读取数据
load(载入):将从主内存读取到的数据写入到工作内存中
use(使用):从工作内存中读取数据用来计算
assign(赋值):将计算好的值重新赋值到工作内存中
store(存储):将工作内存的数据写入到主内存中
write(写入):将store过去的变量赋值给主内存中的变量
lock(锁定):将主内存变量加锁,表示为线程独占状态
unlock(解锁):将主内存变量解锁,解说后其他线程可以锁定该变量
可见性、有序性、原子性。
可见性对应就有不可见问题,因为JMM内存模型,一个线程在工作内存中修改共享变量其他线程时不知道的,也就是对其他线程时不可见的,这样就会容易出现数据不准确。
有序性对应的无序性是因为,java在编译的时候为了性能的优化,会将一些指令顺序重排,例如执行读指令的时候在等待返回的时候会先执行下一条指令。指令重拍会遵循两个原则:as-if-serial和happens-before原则。
as-if-serial原则:不管怎么重排序,单线程下程序执行的结果不能改变。编译器、runtime和处理器都必须遵循。
happens-before原则:判断数据是否存在竞争、线程是否安全的依据。原则内容(8个):
volatile可以保证可见性和有序性。
原子性指的是“不可分割”的意思,这个指令要么执行,要么不执行不可中断,高级语言的一条语句可能并不是原子指令,例如:count++操作就不是一个原子操作,它分为三步:①、从内存中读取count;②、执行count+1操作;③、将count+1的结果写回内存。如果不是原子指令执行中途是可以中断的(也就是线程切换)可能会出现问题:线程1读取到了count=0,此时线程切换,线程2也读取到count=0,线程2继续执行并将结果count=1写回到主内存,此时继续执行线程1,线程1也将结果count=1写回到主内存中,此时进行了两次count++可是主内存的count还是=1,出现错误。
解决非原子就是要求同一时间内只能有一个线程对共享资源操作,同一时间只允许一个线程操作称为互斥。所以要达到原子性可以同过加锁来实现。
volatile关键字可以保证并发编程的可见性和有序性。
实现可见性:缓存一致性协议(MEIS):要求线程的工作内存在修改完一个变量后立即将该变量写回到主内存,所有的线程对总线程存在嗅探机制,一但嗅探到自己工作内存中的某个变量的值被改变了,就会将自己工作内存的该变量失效。进而重新从主内存总读取修改后的变量。底层主要是通过汇编lock前缀指令。
lock指令的作用:①会将当前处理器缓存行的数据立即写回到主内存;②写回到主内存的操作会引起其他线程里缓存了该内存地址的数据失效。
保证有序性:volatile修饰变量。
内存屏障机制:内存屏障是一条指令,可以对编译器和处理器的指令重排进行一些限制,比如内存屏障指令可以禁止它之后的指令被提取到它的前面。
单例模式
public class Window {
private static volatile Window window=null;
private Window(){
}
public static Window getWindow(){
if(window==null){
synchronized(Window.class){
if(window == null){
window = new Window();
}
}
}
return window;
}
}
/*
jvm底层new一个对象的过程是:
1. 类加载检查:虚拟机遇到一条new指令时,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号应用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有必须经过相应的过程。
2. 分配内存:虚拟机将为新生的对象分配内存,也就是对象内存大小将在加载完成后就已经确定了,为对象分配内存的任务等同于把一块确定大小的内存从堆中划分出来。
3. 初始化零值:此时为对象赋上默认值,并不是真正程序要赋予的值。
4. 设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何找到对应的类的元数据信息,对象的哈希码,对象的GC年龄等信息。这些信息都在对象的对象头中存储。HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(header)、实例数据(Instance Data)和对齐填充(Padding)。对象头又分2部分:一部分用于存储运行时数据,如哈希吗、GC年龄、锁状态等;另一部分是类型指针,如对象指向它的类元数据的指针。
5. 执行init方法:执行init方法时才真正的为对象赋予程序员想要赋予的初始值和执行构造方法。执行完init方法才算是一个对象真正构造完成。
6. 设置方法区中的变量指向堆中刚创建的实例对象
所以new一个对象并不是一个原子操作。并且这些指令可能会重排序
当由于指令重排使得第六步优先于第五步,而恰巧又在第六步执行完之后切换到其他线程中,其他线程会判断对象已经初始化,直接返回,但是返回的对象只是一个半成品(对象半初始号问题),如果禁止指令重排,那就不会出现这个问题。volatile在此处的防止指令重排就会起到作用。
*/