进程就是运行中的程序,程序是静态的,进程是动态的。它有一下三大特点:
- 动态性:进程是运行中的程序,要动态的占用内存,CPU和资源。
- 独立性:进程之间是相互独立的,彼此有自己的独立内存区域。
- 并发性:单核的CPU计算机在同一个时刻只有一个进程在被执行。CPU会分时轮询切换来为每个进程服务,因为切换的速度非常快,我们就感觉这些进程似乎是在同时进行,这就是并发性。
并行:即同一时刻有多个事件在发生或执行(同时执行,微观上是分时交替进行),它是随机的。
并发:同一时间段内有两个或多个事件发生或执行(交替执行)。
- 线程属于进程,是进程中的一个独立单元。线程本身不会独立存在
- 一个进程可以包含多个线程,这就是多线程。
- 线程创建的开销要小于进程。
- 线程也支持并发性。
线程作用:提高程序的效率,线程支持并发性能更多地获取CPU,可以解决很多特定的业务模型,是大型高并发技术的核心技术。
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
- 线程:同一线程的内部线程之间的堆空间是共享的,而栈空间是独立的,线程消耗的资源比进程小的多。
注意:(了解知识点)
1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
2:Java 程序的进程里面至少包含两个线程,主线程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
由图知道:一个进程包含多个线程,多个线程共享进程的堆和方法区资源,每个线程都有其私有的程序计数器和栈区域。
- 堆是一个进程中最大的内存,并且被所有线程共享,主要用来存放使用进程中使用
new
操作创建的对象实例。 - 方法区则是用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的。
- 程序计数器为线程私有,用来记录线程当前要执行的指令地址。
- 栈资源用来存储该线程的局部变量,这些变量为线程私有,此外还用来存放线程的调用栈帧。
多线程编程(Multithreaded Programming)的目的,就是“最大限度地利用cpu资源”,当某一线程的处理不需要占用cpu而只和io等资源打交道时,让需要占用Cpu的其他线程有其他机会获得cpu资源。
线程的创建方法有三种:
- 直接定义一个类去继承
Thread
类,然后重写run()
方法,最后创建线程对象并调用对象的start()
方法启动线程。 - 定义一个线程任务类实现
Runnable
接口,重写run()
方法,创建线程任务对象,吧线程对象包装成线程对象,调用线程对象的start()方法启动线程。 - 实现
Callabel
接口
使用java.lang.Thread
类来开启线程。
其构造方法如下:
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
其常用的方法如下:
public String getName()
:获取当前线程名称。public void start()
:开启此线程执行线程任务; Java虚拟机调用此线程的run方法。public void run()
:定义此线程要执行的任务代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行,之后会继续执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
案例如下:
//自定义线程类,继承自Thread类
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
public MyThread() {
//不指定线程的名字,线程有默认的名字Thread-0
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
//测试类
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 200; i++) {
System.out.println("main线程!"+i);
}
}
}
通过实现java.lang.Runnable
接口并并重写run方法。
**具体步骤**如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
案例代码如下:
//实现Runnable接口实现类
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。
Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
- 可以避免java中的单继承的局限性,线程任务类只实现了Runnabel接口,可以继续继承其它类,并且可以继续实现其他接口。
- 同一个线程任务可以被包装成多个线程对象。
- 适合多个相同的程序代码的线程去共享同一个资源。
- 增加程序的健壮性,实现解耦操作(代码和线程独立),代码可以被多个线程共享。
- 线程池只能放入实现Runable或Callable类的线程任务对象。
- 他们不能直接得到线程执行的结果(run()方法没有返回值)
注意:Thread类实际上也实现了Runable接口
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
//匿名内部类方法创建线程并启动
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<5 ;i++){
System.out.println(Thread.currentThread().getName()+"=====>"+i);
}
}
}).run();
该方法创建线程的步骤如下:
- 定义任务类实现
Callable
接口,并且申明线程执行的结果类型! - 重写线程任务类的
call
方法,该方法可以直接返回线程执行的结果。 - 创建一个Callable线程任务对象。
- 把该任务对象包装成一个未来任务对象。
- 把未来任务对象包装成一个线程对象。
- 调用线程对象的start()方法来启动线程。
什么是是未来任务对象?未来任务对象实际就是一个Runnable对象,进而可以被包装成线程对象。并且它可以在线程执行任务完成之后去得到线程执行的结果。
//1.定义任务类实现`Callable`接口,并且<font color='cornflowerblue'>申明线程执行的结果类型</font>!
class MyCallable implements Callable<String>{
//2.
@Override
public String call() throws Exception {
//写需求代码
int sum = 0;
for (int i=1; i<=10; i++){
System.out.println(Thread.currentThread().getName()+"==>"+i);
sum += i;
}
return Thread.currentThread().getName()+"执行的结果是:"+sum;
}
}
public static void main(String[] args) {
//3.创建一个Callable线程任务对象。
// Callable<String> ca = new MyCallable();
Callable ca = new MyCallable();
//4.把该任务对象<font color='red'>包装成一个未来任务对象。
FutureTask<String> futureTask = new FutureTask<>(ca);
//5.把未来任务对象包装成线程任务对象
Thread t = new Thread(futureTask);
//6.启动线程对象
t.start();
for (int i =0; i<9; i++){
System.out.println("主线程磨时间中!!!!");
}
//7.最后去获取线程执行的结果,如果线程没有结果,则让出CPU等线程执行完后再来取
try {
String result = futureTask.get();//获取call()方法返回的结果(正常/异常)
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
优点:
- 可以避免java中的单继承的局限性,线程任务类只实现了Callable接口,可以继续继承其它类,并且可以继续实现其他接口。
- 同一个线程任务可以被包装成多个线程对象。
- 适合多个相同的程序代码的线程去共享同一个资源。
- 增加程序的健壮性,实现解耦操作(代码和线程独立),代码可以被多个线程共享。
- 线程池可以放进实现Runable或Callable类的线程任务对象。
- 能直接得到线程执行的结果
缺点就是编程稍微复杂了点。
线程的安全问题来源:多个线程操作同一个共享资源的时候可能会出现线程安全问题。
举现实生活中的一个例子:
实例代码如下:
首先设定银行账户:
package 线程安全;
/**
* Create By Chenhui Peng on 2021/3/2.
*/
public class Account {
private String ID;
private double money;
//同一个账户里的取钱方法
public void drawMoney(int money){
//开始判断取钱逻辑
//1.判断谁来取钱, 拿到当前线程的名字
String name = Thread.currentThread().getName();
//2.判断当前账户对象的金额是否足够
if (this.money >= money) {
System.out.println(name+"来取钱,余额足够,取出"+money);
//3.更新余额
this.money -= money;
System.out.println(name+"来取钱后,余额为"+this.money);
}else {
System.out.println(name+"来取钱,余额不足!");
}
//
}
public Account(String ID, double money) {
this.ID = ID;
this.money = money;
}
public String getID() {
return ID;
}
public void setID(String ID) {
this.ID = ID;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
自定义简单的取钱线程:
package 线程安全;
/**
* Create By Chenhui Peng on 2021/3/2.
* 线程类 设定取钱线程
*/
public class DrawThread extends Thread {
//定义一个成员变量接收账户对象!
private Account acc;
public DrawThread(Account acc, String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
//开始取钱 设定要取10000
acc.drawMoney(10000);
}
}
创建两个线程开始取钱测试:
package 线程安全;
/**
* Create By Chenhui Peng on 2021/3/2.
*/
public class ThreadSafe {
public static void main(String[] args) {
//1.创建一个共享资源的对象账户!
Account account = new Account("ICBC-110", 10000);
//2.创建两个线程去账户对象中去取钱
Thread xaioMing = new DrawThread(account, "xiaoMing");
xaioMing.start();
Thread xiaoHong = new DrawThread(account, "xiaoHong");
xiaoHong.start();
//
}
}
结果却如下:
xiaoMing来取钱,余额足够,取出10000
xiaoHong来取钱,余额足够,取出10000
xiaoMing来取钱后,余额为0.0
xiaoHong来取钱后,余额为-10000.0
出现线程安全问题!!两个人同时对同一个账户对象取钱取钱。因此我们需要线程同步!
线程同步就是为了解决线程的额安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决,以保证每个线程都能正常执行原子操作。
线程同步的做法是**加锁**。即把要共享的资源加上锁,每次只能一个线程去访问,等该线程访问完毕后,其它线程才能进来访问。
可简单理解如下:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
步骤如下:
- 同步代码块。
- 同步方法。
- 锁机制。
做法:把出现线程安全问题的核心代码给上锁,每次只能一个线程来执行,等该线程执行完毕后再自动解锁,其它线程才可以进来执行。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,且锁的范围约精细越好。
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
锁对象:理论上可以为任意“唯一”对象,建议使用共享资源。
- 在实例方法中,建议使用this作为锁对象。
- 在静态方法中建议使用
类名.class
字节码作为锁对象。
示例代码:
package 线程安全_同步代码块;
/**
* Create By Chenhui Peng on 2021/3/2.
*/
public class Account {
private String ID;
private double money;
//同一个账户里的取钱方法
public void drawMoney(int money){
//开始判断取钱逻辑
//1.判断谁来取钱, 拿到当前线程的名字
String name = Thread.currentThread().getName();
//加锁
synchronized (this){
//2.判断当前账户对象的金额是否足够
if (this.money >= money) {
System.out.println(name+"来取钱,余额足够,取出"+money);
//3.更新余额
this.money -= money;
System.out.println(name+"来取钱后,余额为"+this.money);
}else {
System.out.println(name+"来取钱,余额不足!");
}
}
//
}
public Account(String ID, double money) {
this.ID = ID;
this.money = money;
}
public String getID() {
return ID;
}
public void setID(String ID) {
this.ID = ID;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
作用:把出现线程安全的核心方法给锁起来,每次只能有一个线程进入访问,其它线程必须在外面等待。
使用方法:直接给方法加上修饰符synchronized
注意:
该方法的底层原理和同步代码块是完全一样的,其底层也有锁对象,如果方法是实例方法:同步方法默认用this作为锁对象,如果方法是静态方法,则默认用类名.class作为锁的对象。
示例代码:
//同一个账户里的取钱方法
//在方法上加锁
public synchronized void drawMoney(int money){
//开始判断取钱逻辑
//1.判断谁来取钱, 拿到当前线程的名字
String name = Thread.currentThread().getName();
//2.判断当前账户对象的金额是否足够
if (this.money >= money) {
System.out.println(name+"来取钱,余额足够,取出"+money);
//3.更新余额
this.money -= money;
System.out.println(name+"来取钱后,余额为"+this.money);
}else {
System.out.println(name+"来取钱,余额不足!");
}
//
}
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
示例代码:
package 线程安全_Lock显示锁;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Create By Chenhui Peng on 2021/3/2.
*/
public class Account {
private String ID;
private double money;
//创建一把锁对象,由于账户对象对所有用户唯一,因此该锁对象也唯一。
private final Lock lock = new ReentrantLock();
//同一个账户里的取钱方法
public void drawMoney(int money){
//开始判断取钱逻辑
//1.判断谁来取钱, 拿到当前线程的名字
String name = Thread.currentThread().getName();
//先上锁,再取钱!!
lock.lock();
try {
//2.判断当前账户对象的金额是否足够
if (this.money >= money) {
System.out.println(name+"来取钱,余额足够,取出"+money);
//3.更新余额
this.money -= money;
System.out.println(name+"来取钱后,余额为"+this.money);
}else {
System.out.println(name+"来取钱,余额不足!");
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
//
}
public Account(String ID, double money) {
this.ID = ID;
this.money = money;
}
public String getID() {
return ID;
}
public void setID(String ID) {
this.ID = ID;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
线程安全的优缺点:
- 线程安全,就会导致性能差(只有一个线程能正常访问所需资源,其它线程必须等着)。
- 线程不安全则性能好。(若开发需求简单不会碰到多线程安全问题则使用不安全设计)
补充:
StringBuilder线程不安全的
StringBuffer线程安全的(已淘汰)