Java基础 (四)
进程与线程
进程是指一个内存中运行的应用程序,每一个进程都有自己的一块内存空间,即进程空间。
一个进程可以启动一个或多个线程比如windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行多个线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
单线程:排队执行
多线程:在任务中切换执行,切换速度非常快,给人同步运行的感受,效率较高。使用多个线程可以帮助我们在单个处理系统中实现更高的吞吐量,如果一个程序是单线程的,这个处理器在等待一个同步I/O操作完成的时候,他仍然是空闲的。在多线程系统中,当一个线程等待I/O的同时,其他的线程也可以执行。
在Java中,每次程序运行,至少启动两个线程,一个是main线程,一个是垃圾回收线程,每当使用Java命令执行一个类时,实际上都会启动一个JVM,每一个Java虚拟机实际上就是在操作系统中启动了一个进程。
线程生命周期
新建
- new 关键字创建一个线程,该线程处于新建状态
- JVM为线程分配内存,初始化成员变量值
就绪
- 当线程调用start( )方法后,该线程处于就绪状态
- JVM为线程创建方法栈和程序计数器,等待调度器调度
运行
- 就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态
阻塞
- 当线程调用sleep()方法,主动放弃所占的处理器资源
- 线程调用了一个阻塞式的IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步锁,但该同步锁正被其他线程持有
- 线程在等待某个通知
- 程序调用了线程的suspend()方法,将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法
死亡
- run() 或call()方法执行完成,线程正常结束
- 线程抛出一个未捕获的异常或error
- 调用该线程的stop()方法来结束该线程,该方法容易导致死锁,不推荐使用
并行与并发
并发:并发通常指提高运行在单处理器上的程序的性能,通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。在某一时刻只能让一个线程运行。"并发"在微观上不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行,从宏观外来看,好像是这些进程都在执行。
并行:在多CPU系统中,可以让两个以上的线程同时运行,这种可以同时让两个以上线程同时运行的方式叫做并行(parallel)。
串行
其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子,我们下载多个文件,在串行中它是按照一定的顺序去进行下载的,也就是说必须等下载完A之后,才能开始下载B,它们在时间上是不可能发生重叠的。
同步和异步
同步和异步 关注的是消息通信机制 (synchronous communication/ asynchronous communication)。 着重点在消息通知的方式,也就是调用结果通知的方式。
同步:顺序执行,执行完当前的任务再执行下一个,需要等待、协调运行。当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
【任务完成后,将任务结果通知调用者】
异步:在等待某事件的过程中继续做别的事,不需要等待这一事件完成后再工作。多线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
调用在发出之后,这个调用就直接返回了,没有直接返回结果,而是通过返回状态来通知调用者。
【任务开始时就立刻通知调用者,任务开始进行了,调用者不必等待,可以干别的事情,调用结束之后,通过消息回调来通知调用者结果。】
同步就是洗车,需要自己去轮询(每隔一段时间去看看车洗完了没),异步就是车洗完,然后老板会通知你已经洗完了,你可以回来开走了。
同步和异步是相对于操作结果来说,会不会等待结果返回。
阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用:是指调用结果返回之前,当前线程会被挂起。一直处于等待消息通知,不能够执行其他业务,调用线程只有在得到结果之后才会返回。
非阻塞调用:指在不能立刻得到结果之前,该调用不会阻塞当前线程。
同步阻塞:线程在进行任务1时,只能够等待,直到任务1结束,发出结束的通知消息【洗车店洗车,人不会收到消息通知(同步),只能干等(阻塞),直到受到洗车结束的消息通知(同步)】
同步非阻塞:线程在进行任务1时,在等待结束通知的时间内,可以做别的事情。【洗车店洗车,人不会收到消息通知(同步),但在等待的过程中可以玩手机(非阻塞),直到受到洗车结束的消息通知(同步)】
异步阻塞:线程在开始进行任务时,会立刻返回,【洗车店洗车,老板告诉人等一会(异步),人啥也不干,干等(阻塞)】
异步非阻塞:【洗车店洗车,老板告诉人等一会(异步),人在等待的过程中玩手机(非阻塞)】
阻塞就是说在等待洗车的过程中,你不可以去干其他的事情,非阻塞就是在同样的情况下,可以同时去干其他的事情。阻塞和非阻塞是相对于线程是否被阻塞。
阻塞和非阻塞是指进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪。
而同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞,异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。(晕了。。。)
线程安全
经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。(一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。)
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,在Items[0] 添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
// 一个线程不安全的卖电影票的例子
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "窗口一");
Thread t2 = new Thread(ticket, "窗口二");
Thread t3 = new Thread(ticket, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
public class Ticket implements Runnable{
private int ticketNum = 100;
@Override
public void run() {
while (true){
if (ticketNum > 0){
// 模拟出票时间
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印进程号和票号,票数-1
String name = Thread.currentThread().getName();
System.out.println("线程: "+name+"售票:" + ticketNum--);
}
}
}
}
线程安全的根本问题:
- 多个线程操作共享的数据
- 操作共享的数据的现成代码有多条
- 多个线程对共享线程有写操作
为什么需要“线程同步”
在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应操作,保证了数据的同步性,解决了线程不安全的问题。
线程间共享代码和数据可以节省系统开销,提高程序运行效率,但同时也导致了数据的“访问冲突”问题,如何实现线程间的有机交互、并确保共享资源在某些关键时段只能被一个线程访问,即所谓的“线程同步”(Synchronization)就变得至关重要。
临界资源
多个线程间共享的数据称为临界资源(Critical Resource),由于是线程调度器负责线程的调度,程序员无法精确控制多线程的交替顺序。因此,多线程对临界资源的访问有时会导致数据的不一致性。
为了保证每个线程都能正常执行共享资源操作,Java引入7种线程同步机制:
- 同步代码块(synchronized)
- 同步方法(synchronized)
- 同步锁(ReentrantLock)
- 特殊域变量
- 局部变量
- 阻塞队列(LinkBlockingQueue)
- 原子变量
同步代码块
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
synchronized(同步锁) {
需要同步操作的代码
}
public class Ticket implements Runnable{
private int ticketNum = 100;
@Override
public void run() {
while (true){
synchronized (obj){
if (ticketNum > 0){
// 模拟出票时间
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印进程号和票号,票数-1
String name = Thread.currentThread().getName();
System.out.println("线程: "+name+"售票:" + ticketNum--);
}
}
}
}
}
同步锁:
对象的同步锁只是一个概念,在对象上标记了一个锁。
锁对象可以是任意类型
多个线程要使用同一把锁
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他线程只能在外等着(BLOCKED)
同步方法
使用synchronized
修饰的方法,就叫做同步方法,保证线程在执行该方法时,其他线程只能在方法外等着
public synchronized void 方法名{
方法体
}
public class Ticket implements Runnable{
private int ticketNum = 100;
Object obj = new Object();
@Override
public void run() {
while (true){
sellTicket();
}
}
// 同步方法
private synchronized void sellTicket(){
if (ticketNum > 0){
// 模拟出票时间
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印进程号和票号,票数-1
String name = Thread.currentThread().getName();
System.out.println("线程: "+name+"售票:" + ticketNum--);
}
}
}
同步锁
java.util.concurrent.locks.Lock
机制提供了比同步代码块和同步方法跟那个广泛的锁定操作。同步代码块/方法具有的功能,Lock都有,除此之外更强大,更能体现面向对象。
// 定义锁对象, 构造方法参数为线程是否公平获取锁。true公平 false不公平,既由某个线程独占,默认是false
Lock lock = new ReentrantLock(true);
lock.lock(); //加同步锁
lock.unlock(); // 关闭同步锁
synchronized
和Lock
的区别
synchronized
是Java内置关键字,Lock
是Java类;synchronized
无法判断是否获取锁的状态,Lock
可以判断是否获取到锁;synchronized
会自动释放锁(线程执行过程中发生异常会释放锁)Lock
需要在finally中手动释放锁,否则容易造成线程死锁;- 使用
synchronized
关键字的两个线程(1和2),如果当前1获得锁,2线程等待,如果1阻塞,2会一直等待下去,而使用Lock
就不一定会等待下去,若尝试获取不到锁,线程可以不用一直等待就结束了; synchronized
的锁可重入,不可中断,非公平。而Lock
可重入,可中断,可公平,可非公平;Lock
适合大量同步代码的同步问题,synchronized
锁适合代码少量的同步问题。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable{
private int ticketNum = 100;
// 定义锁对象, 构造方法参数为线程是否公平获取锁。true公平 false不公平,既由某个线程独占,默认是false
Lock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
lock.lock();
try {
if (ticketNum > 0){
// 模拟出票时间
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印进程号和票号,票数-1
String name = Thread.currentThread().getName();
System.out.println("线程: "+name+"售票:" + ticketNum--);
}
} finally {
lock.unlock();
}
} }
}
死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(相互等待),若无外力作用,这些进程都将无法向前推进。
死锁产生的必要条件(只要发生死锁,以下条件必然成立,主要有一个条件不满足,就不会发生死锁)
- 互斥条件:进程要求对所分配的资源进行排他性控制。即在一段时间内,某资源仅为一个进程所占用。此时,若有其他进程请求该资源,则请求进程只能等待
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行抢走,即只能由获得该资源的进程自己来主动释放。
- 请求和保持条件:进程已经保持了至少一个资源,但是又提出了新的资源请求,而该资源已经被其他进程所占有,
死锁处理
预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防死锁的发生。
避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态
检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构即使检测死锁的发生,并采取适当措施加以清除
解除死锁:当前检测出死锁后,便采取适当措施将进程从死锁状态解脱出来。
死锁预防
至少破坏产生死锁的四个必要条件之一
- “互斥”条件是无法被破坏的,因此在死锁预防里主要是破坏其他几个必要条件
锁机制
有些业务逻辑在执行过程中要求对数据进行排他性的访问,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。