进程与线程
程序:为了完成特定任务,用某种编程语言编写的一组指令的集合,即指一段静态的代码,静态对象;
进程:APP
-
进程是操作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器并由处理器执行的一个实体;
-
进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
-
进程就是正在运行的程序,它会占用对应的内存区域,由CPU进行执行与计算。
-
特点:
- 独立性:进程是系统中独立运行的实体,可以拥有自己的独立资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
- 并发性:多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.
一个Java 应用程序,Java.exe其实最少因该有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程,如果发生异常则会影响主线程;
线程
概念:线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.是一个程序内部的一条执行路径。
一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程。
我们看到的进程的切换,切换的也是不同进程的主线程
多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能
进程与线程的关系
- 线程必然属于某一个进程,线程要运行必须要有相应的资源,而进程就是这个资源的提供者,所以线程存在于进程中。所以进程=线程+资源。
- 总结:线程是具有能动性、执行力、独立的代码块。进程=线程+资源。执行流、调度单元、运行实体都是针对线程而言的,线程才是解决问题的思路、步骤,只有它才能上处理器运行。
- 进程和线程的区别是线程没有自己独享的资源,因此没有自己的地址空间,它要依附在进程的地址空间中从而借助进程的资源运行
- 其实一个java.exe程序至少有三个线程:一个主线程main(),gc()垃圾回收线程,异常处理线程;
创建线程的方式一共四种:
jkd5.0新增2种
多线程
特性:
-
随机性:一个CPU【单核】只能执行一个进程中的一个线程。
-
是因为CPU以纳秒级别甚至是更快的速度高效切换着,看起来像同时运行
-
串行是指同一时刻一个CPU只能处理一件事,类似于单车道
并行是指同一时刻多个CPU可以处理多件事,类似于多车道 -
并发:一个CPU处理多个线程;
-
-
分时调度:时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间,如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。
注意:我们无法控制OS选择执行哪些线程,我们控制不了OS的时间片分配,OS底层有自己规则,如:- FCFS(First Come First Service 先来先服务算法)
- SJS(Short Job Service短服务算法
-
线程的状态:三态模型
就绪: 可运行状态,只要获得CPU,就可立即执行 start()
执行:运行状态,正在执行的状态 --在时间片内
堵塞:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程运行堵塞。
4.可以再添加2种状态
-
创建状态new:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
-
终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
执行完run方法或者直接调用stop()方法会让线程立刻死亡,或者出现异常未处理;
-
操作系统为每一个进程都提供了一个PCB:程序控制块,它记录着与此进程相关的信息:进程状态、优先级、PID等等。
每个进程都有自己的PCB,所有的PCB都放在一张表格中维护,这就是进程表。调度器根据这个表来选择处理器上运行的进程。我们设计的系统中PCB只占一页:4K。
-
多线程实现方法
方法一:继承thread类
-
1.自定义一个类,继承thread类。
-
thread类中的常用方法:
-
1.start():启动线程和调用run方法; 2.run():重写run()方法,写需要执行的代码 3.currentThread():静态方法,可以直接由Thread类调用,返回当前正在执行的线程 4.getname():获取线程名 5.setname():修改线程名 6.yield():释放当前CPU的执行权; 7.join():在线程a中调用线程b的join方法,此时会执行线程b,线程a处于阻塞的状态,直到线程B完全执行完毕,a才解除阻塞,进入就绪状态; 8.sleep():可以设定时间,让线程每隔多少时间执行一次;
-
2.在自定义类中重写父类的run()方法,这个方法里放自己的业务。
-
3.创建自定义线程类对象–对应的是线程的新建状态
-
4.调用start方法,以多线程方式将线程对象加入到就绪队列中等待OS选中,调用start方法,JVM会自动调用run()方法
-
run()和start()区别:
-
run():主要用来封装我们自定义的代码业务,直接调用本方法相当于普通方法的调用
-
start():主要用来以多线程的方式启动线程,然后由**jvm调用本线程的run()**方法执行业务。
-
注意:这里的启动指的是将线程对象加入到就绪队列,具体什么时间执行要看OS调用。
//定义一个线程类继承Thread类
class MyThread extends Thread{
//重写run方法
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
/*
public final String getName() {
return name;}
*/
//获取线程的名字
System.out.println(getName()+"="+i);//getName()是父类的方法,子类继承之后可以直接调用
//对应的是线程的新建状态
MyThread mt1 = new MyThread();
MyThread mt2= new MyThread();
MyThread mt3 = new MyThread();
MyThread mt4 = new MyThread();
//测试1:如果自己调用run方法,只是一个普通的方法,不能起到多线程的效果。
// mt1.run();
// mt2.run();
// mt3.run();
// mt4.run();
//测试二:调用start方法,以多线程方式启动,调用start方法,JVM会自动调用run()方法
mt1.start();/*对应的是线程的就绪状态*/
mt2.start();
mt3.start();
mt4.start();
方法二:实现Runnable接口
-
1.自定义多线程类,并实现Runnable接口
-
2.重写接口中的抽象run()方法,run()方法里放着我们的业务
-
3.创建自定义对象,只创建一次,作为业务对象存在
-
4.创建多个Thread线程对象,并将业务对象交给线程对象来完成
-
5.以多线程的方式启动多线程对象。
-
注意:在获取执行中的线程的名字时,先通过Thread.currentThread(),因为它返回的是正在执行的线程对象的引用,然后通过引用.来调用getName()方法,获取名字。
-
关于如何获取Thread中的Start()方法,只需要用到Thread的构造方法:
public Thread(Runnable target):括号里面可以等价于Runnable target=new MyRunnable();
代表的接口类型引用变量指向接口实现类的对象。然后将这个对象当成参数传入Thread的构造方法。—这也表示所有 的线程共享此一个实现类的对象。
@Override
public void run() {
//完成业务:打印10此当前正在运行的线程名
for (int i = 0; i <10 ; i++) {
/*
由于自定义类和接口中都没有获取名字的方法,所以需要到Thread类里找
public static native Thread currentThread()
currentThread():静态方法,获取当前正在执行的线程对象
getName():获取当前正在执行的线程对象的名称
*/
System.out.println(Thread.currentThread().getName()+"="+i);
}
public static void main(String[] args) {
Runnable target = new MyRunnable();//接口类型引用变量指向接口的实现类
//public Thread(Runnable target)--Thread的构造方法
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
//以多线程的方式启动线程
t1.start();
t2.start();
t3.start();
通过匿名内部类实现接口:
public static void main(String[] args) {
new MyRunnable().go();
}
}
class MyRunnable{
public void go() {
//创建匿名内部类来实现接口
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"="+i);
}
System.out.println("匿名内部类,实现Runnable,启动线程");
}
};
//通过线程类调用start方法。
Thread tread1 = new Thread(r);
Thread tread2 = new Thread(r);
Thread tread3 = new Thread(r);
tread1.start();
tread2.start();
tread3.start();
}
使用
对于以上2种创建方式:优先使用实现Runnable接口
- 首先是java是单继承结构,如果采用继承Thread类,会对本类的后续继承产生影响;
- 如果需要多线程共享数据,那么实现接口比较方便,不需要创建静态属性;
创建多线程方式三:线程池
Excutors是用来辅助创建线程池的工具类
public static ExecutorService newFixedThreadPool(int nThreads)
常用方法:newFixedThreadPool(int)这个方法可以创建指定数目线程的线程池对象
public interface ExecutorService extends Executor
创建出来的线程池对象就是ExecutorService,用来新建/启动/销毁 线程*/
ExecutorService pool = Executors.newFixedThreadPool(4);
//使用Executors工具创建一个最多有5个线程的线程池对象ExecutorService
for (int i = 0; i <5 ; i++) {
//execute(target)让线程池中的线程来执行业务,每次调用都会将一个线程加入就绪队列。
pool.execute(target);//本方法的参数就是需要执行的业务,也就是目标业务类对象
}
等价于:Thread t1=new Thread(target);
Thread t2=new Thread(target);
Thread t3=new Thread(target);
Thread t4=new Thread(target);
t1.start();
t2.start();
t3.start();
t4.start();
方法四 实现Callable 接口
-
与Runnble接口相比 Callable功能更加强大,效率更高;
-
相比run()方法 可以有返回值
-
方法可以抛出异常,被外面的操作捕获 获取异常信息
-
支持泛型的返回值 \
-
需要借助FutureTask类,比如获取返回值
-
步骤:
-
1.继承Callable接口 2.重写接口中的call方法 3.创建实现类的对象 4.创建FutureTask类对象 5.调用FutureTask类中的get方法 来接收线程的返回值 6.调用Thread类中的start方法启动线程
-
//1.创建类实现Callable接口 class NumThread implements Callable { //1.1重写接口中的call方法 类似Runnable接口中的run方法; //将此线程需要的操作放在此方法中 @Override public Object call() throws Exception { int sum=0; for (int i = 1; i <=100 ; i++) { if (i%2==0){ System.out.println(i); sum+=i; } } //1.2返回值类型为Object类型 自动装箱 Integer; return sum; } } public class ThreadNew { public static void main(String[] args) { NumThread num=new NumThread(); //2.要实现Callable 还需要借助FutureTask //FutureTask的构造器参数为Callable接口的实现类对象 FutureTask futureTask = new FutureTask<>(num); //4.启动线程的话还需要调用Thread类中的start方法; new Thread(futureTask).start(); try { //3.通过FutureTask类对象调用get方法来接call方法的返回值 Object sum = futureTask.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }
如何判断线程是否存在安全问题
在多线程程序中+有共享数据+多条语句操作共享数据
当同时出现上面3中情况,就存在安全问题!!!—多线程在抢占时会出现重载或者过载的情况!!
-
由于业务的需要,当前业务是需要数据共享的,所以只能在第三点上进行操作
把可能出现问题的代码包裹起来,一次只让一个线程执行。
同步和异步
-
同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去
-
异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待
-
什么时候使用同步:
- 只要在几个线程之间共享非 final 变量,就必须使用synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改。
- 因为多线程将异步行为引进程序,所以在需要同步时,必须有一种方法强制进行。例如:如果2个线程想要通信并且要共享一个复杂的数据结构,如链表,此时需要确保它们互不冲突,也就是必须阻止B线程在A线程读数据的过程中向链表里面写数据(A获得了锁,B必须等A释放了该锁)。
解决方案
方法一:同步代码块
同步锁:synchronized–同步的
锁的范围:操控共享数据的代码,即为需要被同步的代码
当多个对象操作共享数据时,可以使用同步锁解决线程安全问题,被锁住的代码就是同步的
锁对象必须保证唯一,才能出现同步的效果!
写法:
synchronized (锁对象){undefined
需要同步的代码(也就是可能出现问题的操作共享数据的多条语句);
}
public void run() {
while (true){
/*同步代码块:synchronized(锁对象){会出现安全隐患的所有代码}
在同步代码块中的代码,同一时刻只会被一个线程执行
注意:同步代码块必须保证所有线程对象使用同一把唯一的锁
锁对象必须唯一,锁对象的类型不做限制,唯一就行。
* */
// synchronized (Object o)这些写是不对的,锁不唯一
synchronized (o){ //上面new了一次,所以锁是唯一的。
if (tickets>0){
try {
Thread.sleep(1);//强制休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印当前正在售票的线程,每次售票,票数减1
System.out.println(Thread.currentThread().getName()+"="+tickets--);
}
if (tickets<=0) break;//注意设置循环出口
}
}
public void run() {
/*我们每通过class关键字创建一个类,就会在工作空间生成唯一类名.class字节码文件
这个类名.class对应的对象,我们称之为字节码对象
字节码对象及其重要,是反射技术的基石,字节码对象包含了当前类所有关键信息
所以,用这样一个唯一且明确的对象作为同步代码块的锁对象,再合适不过了。
*/
while (true) {
synchronized (MyTreadV2.class) {
if (sum > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "=" + sum--);//getName()是父类的方法,子类继承之后可以直接调用
}
if (sum <= 0) break;
}
}
使用前提:1.必须有2个及以上的线程,单线程不存在多线程安全问题
2.多个线程之间必须使用同一把锁,也就是锁必须是唯一的。
方法二:同步方法
注意:
-
共享数据:如果操作共享数据的代码完整的声明在一个方法中,就可以把此方法声明成同步的;
-
同步监视器:锁是系统默认存在,不需要显示声明;
一:当通过Runnable接口去实现多线程时:非静态同步方法 就创建一个实现类的对象 全局共享
public synchronized void show () {}
此时存在默认的同步监视器(锁):this-- 代表的是本类的对象
二:当通过继承Thread类实现多线程时:静态同步方法
public static synchronized void show () {}
此时存在默认的同步监视器为:Sail.class -- 代表当前类本身
当通过Runnable接口去实现多线程时
class Sail implements Runnable{
int tickets=100;
@Override
//2.通过run方法去调用同步方法;
public void run() {
while (true) {
show();
}
}
//1.show方法里面包含操作共享数据的代码
public synchronized void show () {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "=" + tickets);
tickets--;
}
}
通过继承Thread类实现多线程时
public class MyThread extends Thread{
private static int tickets=100;
@Override
public void run() {
while (true) {
//在操作共享数据的代码上面加锁(同步监视器)
show();
}
}
private static synchronized void show(){ //此时锁this有4个 不唯一 需要加static 此时锁变成MyThread.class
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "=" + tickets--);
}
}
特点:
-
synchronized同步关键字可以用来修饰代码块,称为同步代码块,使用的锁对象类型任意,但注意:必须唯一!
-
synchronized同步关键字可以用来修饰方法,称为同步方法
-
同步的缺点是会降低程序的执行效率,但我们为了保证线程的安全,有些性能是必须要牺牲的
-
但是为了性能,加锁的范围需要控制好,比如我们不需要给整个商场加锁,试衣间加锁就可以了
注意:
为什么同步代码块的锁对象可以是任意的同一个对象,但是同步方法使用的是this呢?
因为同步代码块可以保证同一个时刻只有一个线程进入
但同步方法不可以保证同一时刻只能有一个线程调用,所以使用本类代指对象this来确保同步
方法三:lock锁
概述:JDK5.0之后,java提供了更强大的线程同步机制
- 通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当
- Lock接口是控制多个线程对共享资源进行访问的工具;
- 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock锁对象加锁;
- 线程开始访问共享资源前现获取Lock对象
- ReentrantLock类实现了Lock接口,它拥有和synchornized相同的并发性和内存语义,可以显示加锁和解锁;
举例:卖票问题
class Window implements Runnable{
private int tickets=100;
//1.创建ReentrantLock对象
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while (true){
try {
对共享数据进行加锁 //2.调用lock()方法进行上锁;
lock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets>0){ System.out.println(Thread.currentThread().getName()+"="+tickets--);
}
else break;
}finally {
执行完之后进行解锁 //执行完之后解锁unlock()
lock.unlock();
}
}
}
打印结果:
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
synchornized与lock的异同
相同
- 都可以解决线程的安全问题
不同
- synchornized机制在执行完相应的同步代码后 自动释放锁;
- lock需要手动的启动同步(lock()方法), 结束时也需要手动去实现(unlock()方法)
- 使用lock锁,jvm将花费较少的时间来调度线程,性能更好,并且扩展性较好,提供更多的子类;