一、进程与线程的概念
1.进程:
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志
进程具有的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进行一起并发执行
- 独立性:进程是系统进行资源分配和调度的一个独立单位
- 结构性:进程由程序,数据和进程控制块三部分组成
2.线程:
在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。
3.进程与线程的区别:
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多
二、创建线程及其常用方法
(1)获取线程名称:
- 使用Thread类中的getName()
String getName();返回线程名称 - 获取当前正在执行的线程
static Thread currentThread()返回当前正在执行的线程对象的引用
public class MyThread extends Thread{
@Override
public void run() {
//第一种:直接获取线程名称
String name=getName();
System.out.println(name);//Thread-0
//第二种:获取当前正在执行的线程对象的引用
Thread t=Thread.currentThread();
System.out.println(t);//Thread[Thread-0,5,main]
//再用getName获取线程名
String name1=t.getName();
System.out.println(name1);//Thread-0
//或用链式编程一步到位
System.out.println(Thread.currentThread().getName());//Thread-0
}
}
public class Main {
public static void main(String[] args) {
MyThread thread=new MyThread();
thread.start();
System.out.println(Thread.currentThread().getName());//main
//也可直接获取主线程名称
}
}
(2)设置线程名称
- 使用Thread类中的setName()
void setNmae(String name); - 创建一个带参数的构造方法,参数传递线程的名称,调用父类参数构造方法,把线程名称传递给父类,让父类Thread起名
public class MyThread extends Thread{
public MyThread(){//创建无参构造方法
}
public MyThread(String name) {
super(name);//创建有参构造方法并传给父类
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
//开启多线程
MyThread thread=new MyThread();
thread.setName("张三");
thread.start();//张三
new MyThread("李四").start();//李四
}
}
(3)sleep休眠
使正在执行的线程以指定毫秒数暂停,计时结束后继续执行
public static void sleep(long millis)
public class Main {
public static void main(String[] args){
for(int i=0;i<10;i++){
//需要用try/catch或者throws进行异常处理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);//每过一秒打印一个数字
}
}
}
若是多个线程对象同时执行,且这几个线程对象执行的方法体是相同的,则执行的时间有先有后,睡眠的时间也有先有后,其中有适当的延迟操作
(4)中断线程
Thread类里提供了两种中断执行处理方法
判断线程是否被中断:public boolean isInterrupted
中断线程执行:public void interrupted
所有正在执行的线程是可以被中断的,中断线程必须要进行异常的处理
(5)强制执行
在强制执行时必须要获取强制执行对象才可以进行join()的调用
public class Main {
public static void main(String[] args) {
Thread mainthread = Thread.currentThread();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
if (i == 2) {
try {
mainthread.join();//强制执行主线程,执行完后才会执行次线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(500);//休眠便于观察结果
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("次线程" + i);
}
}).start();
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(500);//休眠便于观察结果
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程" + i);
}
}
}
/*输出结果为:
主线程0
次线程0
次线程1
主线程1
主线程2
主线程3
主线程4
次线程2
次线程3
次线程4 */
三、线程安全–>synchronized关键字
举个例子:StringBuilder与StringBuffer构造方法基本相同,但为什么要分为两个呢,那是因为StringBuffer中所有构造方法都加上了synchronized关键字,所以StringBuilder是非线程安全的,StringBuffer是线程安全的
- 非线程安全是指多线程操作同一个对象可能会出现问题。而线程安全则是多线程操作同一个对象不会有问题。
- 线程安全必须要使用很多synchronized关键字来同步控制,所以必然会导致性能的降低。所以虽然加上synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小使用范围,如果随意使用会很影响程序的性能。
synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则会一直处于等待状态。
加入同步锁之前:
public class Ticket{
public static void main(String[] args) {
MyThread run = new MyThread();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
class MyThread implements Runnable {
private int ticket = 50;
Object obj = new Object();
public void run() {
while (ticket > 0) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票");
ticket--;
}
}
}
}
/*输出:
Thread-2正在卖50张票
Thread-0正在卖50张票
Thread-1正在卖50张票
Thread-2正在卖47张票
Thread-0正在卖47张票
Thread-1正在卖47张票
Thread-0正在卖44张票
....为方便演示,已省略中间结果
Thread-2正在卖5张票
Thread-0正在卖2张票
Thread-2正在卖2张票
Thread-1正在卖2张票
Thread-0正在卖-1张票
*/
通过观察输出结果可以发现,同一张票可能会被多次售卖,还有可能出现-1结果
为防止该现象发生,可以加入同步锁:
public class Ticket
{
public static void main(String[] args) {
MyThread run=new MyThread();
Thread t0=new Thread(run);
Thread t1=new Thread(run);
Thread t2=new Thread(run);
t0.start();//开启3个线程
t1.start();
t2.start();
}
}
class MyThread implements Runnable {
private int ticket=50;//开始有30张票
Object obj=new Object();
public void run() {
while(ticket > 0){
synchronized (obj){//加入同步锁
if(ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖"+ticket+"张票");
ticket--;
}
}
}
}
}
/*输出:
Thread-0正在卖50张票
Thread-0正在卖49张票
Thread-0正在卖48张票
Thread-0正在卖47张票
Thread-0正在卖46张票
Thread-0正在卖45张票
.......此次省略中间结果
Thread-0正在卖5张票
Thread-0正在卖4张票
Thread-0正在卖3张票
Thread-0正在卖2张票
Thread-2正在卖1张票
*/
四、线程的状态
Java中的线程分为以下几个状态:
新建:
创建一个Java线程常见的有两种方式:
继承Thread类和实现Runnable接口这两种方式。
运行:
线程创建后仅仅占有了内存资源,在JVM管理的线程中还没有该线程,该线程必须调用start方法通知JVM,这样JVM
就会知道又有一个新的线程排队等候了。如果当前线程轮到了CPU的使用权限的话,当前线程就会继续执行。
阻塞状态:
阻塞状态与等待状态的区别是阻塞状态在等着获取一个排他锁,这个时间在另外一个线程放弃这个锁时发生,而等待状态是在等待时间或等待被唤醒动作时发生。在程序等待进入同步区时,线程进入阻塞状态。
休眠:
处于这种状态的线程也不会被CPU分配执行时间,不过无须等待被显示唤醒,而是在一定时间过后会由系统自动唤醒,以下方法会让线程陷入限期等待状态:sleep,设置了Timeout的wait和join等。
无限等待:
处于这种状态的线程不会被分配CPU执行时间,它们要等待其他线程显示唤醒。以下方法会让线程陷入无限等待状态:没有设置Timeout参数的wait()方法,没有设置Timeout参数的join()方法等。
死亡:
死亡的线程不再具有执行能力,有两种可能进入死亡状态
- .线程正常运行结束而引起的死亡,即run方法执行完毕。
- 线程被提前强制终止。
五、进程状态
就绪状态:
当进程已分配到除CPU以外的所有必要资源后,只要在获得CPU,便可立即执行,进程这时的状态就称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将他们排成一个队列,称为就绪队列。
执行状态:
进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态;再多处理机系统中,则有多个进程处于执行状态。
阻塞状态:
正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即程序的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态或封锁状态。
六、线程间通信
1.volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
2.使用Object类的wait() 和 notify() 方法
众所周知Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁
3.使用JUC工具类 CountDownLatch
jdk1.5之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的写,***CountDownLatch***基于AQS框架,相当于也是维护了一个线程间共享变量state
4.使用 ReentrantLock 结合 Condition
5.基本LockSupport实现线程间的阻塞和唤醒
LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。
参考自–>线程间通信的几种实现方式
七、进程间通信
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
参考自–> 进程间通讯的7种方式