1、多线程的基本概念
线程指进程中的一个执行场景,也是执行流程,那么进程和线程有什么区别呢?
- 每个进程是一个应用程序,都有独立的内存空间
- 同一个进程中的线程共享其进程中的内存和资源(共享的内存是堆内存和方法区内存,栈内存不共享,每个线程有自己的栈内存)
1.1、什么是进程?
一个进程对应一个应用程序。例如:在windows操作系统启动word就表示启动了一个进程。在java开发环境下启动JVM,就表示启动了一个进程。现代计算机是支持多进程的,在同一个操作系统中,可以同时启动多个进程。
1.2、多进程有什么作用?
单进程计算机只能做一件事情。
玩电脑,一边玩游戏(游戏进程)一边听音乐(音乐进程)。
对于单核计算机来讲,在同一个时间点上做一件事情。由于计算机将在“游戏进程”和“音乐进程”之间频繁的切换执行,切换速度极高,人类感觉游戏和音乐在同时进行。
多进程的作用不是提高执行速度,而是提高CPU的使用率。进程和进程之间的内存是独立的。
1.3、什么是线程
线程是一个进程中的执行场景,一个进程可以启动多个线程。
1.4、多线程有什么作用
多线程不是为了提高执行速度,而是提高应用程序的使用率。例如让一个应用被多个客户端使用。
可以给现实世界中的人一种错觉:感觉多个线程在同时并发执行
1.5、java程序的运行原理
java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也等于启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用木某个类的main方法。所以main方法运行在主线程中。
2、线程的创建和启动
java虚拟机的主线程入口是main方法,用户可以自己创建线程,方式有两种:
- 继承Thread类
- 实现Runnable接口(推荐使用Runnable接口)
2.1、通过继承Thread类实现线程
简例:
/**
* 通过继承Thread实现多线程的方式:
* 第一步:继承java.lang.Thread;
* 第二步:重写run方法。
* 三个知识点:
* 如何定义线程?
* 如何创建线程?
* 如何启动线程?
*/
public class ThreadTest01 {
public static void main(String[] args) {
// 创建线程
Thread t = new Processor();
// 启动线程
t.start();// 这段代码执行瞬间结束。告诉JVM再分配一个新的栈给t线程。
// run不需要程序员手动调用,系统线程启动之后自动调用run方法
// t.run(); // 只是普通的方法调用,这样做程序只有一个线程,run方法结束后,下面的程序才能继续执行。
// 这段代码在主线程中运行
for (int i = 0; i < 10; i++) {
System.out.println("main-->" + i);
}
// 有了多线程之后,main方法结束之时主线程中没有方法栈帧了
// 但是其他线程或者其他栈中海油栈帧
// main方法结束,程序可能还在运行
}
}
// 定义一个线程类
class Processor extends Thread {
// 重写run方法
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("run-->" + i);
}
}
}
2.2、通过实现Runnable实现线程
简例:
/**
* java中通过 Runnable接口实现线程
* 第一步:写一个类实现java.lang.Runnable;接口
* 第二步:实现run方法
*
*/
public class ThreadTest02 {
public static void main(String[] args) {
// 创建线程
Thread t = new Thread(new Processor2());
// 启动
t.start();
}
}
// 推荐使用这种方式。因为一个类实现接口之外保留了类的继承
class Processor2 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("run-->" + i);
}
}
}
2.3、UML图描述线程的生命周期
线程的生命周期有五个状态:新建、就绪、运行、阻塞、消亡。下图为5个状态的关系
2.4、线程的调度与控制
通常我们计算机只有一个CPU,CPU在某一时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行命令。在单CPU的机器上线程不是并行运行的,只有在多个CPU上线程才可以并行运行。java虚拟机要负责线程的调度,取得CPU的使用权,目前有两种调度模式:分时调度模式和抢占式调度模式,java使用抢占式调度模式。
- 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模式:优先级高的线程获得的CPU时间片相对多一些,如果线程的优先级相同,则随机选择一个。
常用方法简例
/**
* 线程常用方法:
* 1、Thread.currentThread() 获取当前线程对象
* 2、t1.setName("t1") 给线程起名字
* 3、t.getName() 获取线程的名字
* 4、t1.setPriority(10) 更改优先级
* 5、t1.getPriority() 获取优先级
* 6、Thread.sleep(毫秒) 阻塞当前线程,腾出CPU给其它线程让位,是一个静态方法,阻塞的是当前线程,和创建对象无关
* 7、t2.interrupt() 打断线程休眠,使用异常处理机制打断。建议使用变量的设定来正确终止线程
* 8、Thread.yield() 让位,给同优先级的线程,让位的时间不固定,是一个静态方法。
* 9、Thread.join() 线程合并,成员方法。将线程与当前线程合并。等待合并过来的线程死亡。
* 线程的优先级
* 优先级高的获取的CUP时间片相对多一些
* 优先级:1-10
* 最低:1
* 最高:10
* 默认:5
*/
public class ThreadTest03 {
public static void main(String[] args) {
// 优先级常量
System.out.println(Thread.MAX_PRIORITY);// 10
System.out.println(Thread.NORM_PRIORITY);// 5
System.out.println(Thread.MIN_PRIORITY);// 1
// 获取当前线程对象
Thread t = Thread.currentThread();// 此处t保存的内存地址指向的线程是“主线程对象”
// 获取线程的名字getName()
System.out.println(t.getName());// main
Thread t1 = new Thread(new Processor03());
t1.setName("t1");// 设置线程的名字setName()
Thread t2 = new Thread(new Processor03());
t2.setName("t2");
// 获取优先级
System.out.println(t1.getPriority());// 5
System.out.println(t2.getPriority());// 5
// 设置优先级
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
class Processor03 implements Runnable {
// Thread中的run()方法不抛出异常,所以重写run()方法后,在run()方法的声明位置上不能使用throws
// 所以run()方法中的异常只能用try...catch...
public void run() {
// Thread t = Thread.currentThread();// 此处t保存的内存地址指向的线程是“t1线程对象”
// System.out.println(t.getName());// Thread-0-->t1\Thread-1-->t2。线程默认名字规律Thread-*,可以改变名字。
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
使用变量的设定来正确终止线程
/**
* 使用变量的设定来正确终止线程
*
*/
public class ThreadTest04 {
public static void main(String[] args) throws InterruptedException {
Processor04 p = new Processor04();
Thread t = new Thread(p);
t.setName("t1");
t.start();
// 主线程等待5秒
Thread.sleep(5000);
// 5秒后终止p线程
// Thread.interrupted();// 打断当前的sleep,抛出异常,后续方法人会执行
p.flag = false;
}
}
class Processor04 implements Runnable {
boolean flag = true;
public void run() {
for (int i = 0; i < 10; i++) {
if (flag) {
try {Thread.sleep(1000);} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + "--->" + i);
} else {
return;
}
}
}
}
3、线程同步机制
为什么引入同步概念?
以下取款实例说明为什么引入同步。
异步的取款代码:
/**
* t1和t2
* 异步编程模式:t1线程和t2线程各自执行自己的,两个之间谁也不等谁。
* 同步编程模式:两个线程执行,t1线程必须等t2线程执行结束后才执行。
*
* 什么时候要用同步呢?为什么要引入同步呢?
* 1、为了数据安全。尽管应用程序的使用率降低,但为了保证数据是安全的,必须加入线程同步机制。
*
* 2、什么条件下使用线程同步?
* 第一、必须是多线程环境
* 第二、多线程环境共享同一个数据
* 第三、共享的数据涉及到修改操作。
*
* 以下程序演示取款操作。演示程序中不使用线程同步机制,多线程对同一个账户进行操作,会出现的问题!
*/
public class ThreadTest06 {
public static void main(String[] args) {
// 创建一个公共的账户
Account act = new Account("actno-1", 5000.0);
// 创建线程对同一个账户取款
Thread t1 = new Thread(new Processor06(act));
Thread t2 = new Thread(new Processor06(act));
t1.start();
t2.start();
}
}
// 取款线程
class Processor06 implements Runnable {
// 账户
Account act;
// 构造方法
Processor06 (Account act) {
this.act = act;
}
public void run() {
act.withdraw(1000.0);
// 得到错误的结果
System.out.println("取款1000.0成功,余额:" + act.getBalance());// 取款1000.0成功,余额:4000.0
// 取款1000.0成功,余额:4000.0
}
}
// 账户实体
class Account {
private String actno;
private double balance;
public Account() {}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
// 对外提供一个取款方法
public void withdraw(double money) {
double after = balance - money;
// 延时
try {Thread.sleep(1000);} catch (InterruptedException e) {}
//更新
this.setBalance(after);
}
}
使用同步机制的取款代码synchronize:
/**
* 改变以上代码,以下程序使用线程同步机制保证数据的安全
*/
public class ThreadTest07 {
public static void main(String[] args) {
// 创建一个公共的账户
Account2 act = new Account2("actno-1", 5000.0);
// 创建线程对同一个账户取款
Thread t1 = new Thread(new Processor07(act));
Thread t2 = new Thread(new Processor07(act));
t1.start();
t2.start();
}
}
// 取款线程
class Processor07 implements Runnable {
// 账户
Account2 act;
// 构造方法
Processor07 (Account2 act) {
this.act = act;
}
public void run() {
act.withdraw(1000.0);
// 得到想要的结果,但是牺牲了部分时间。
System.out.println("取款1000.0成功,余额:" + act.getBalance());// 取款1000.0成功,余额:4000.0
// 取款1000.0成功,余额:3000.0
}
}
// 账户实体
class Account2 {
private String actno;
private double balance;
public Account2() {}
public Account2(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
// 对外提供一个取款方法
public void withdraw(double money) {
// 把需要同步的代码,放到同步语句块中。参数填共享对象。
/**
* 原理:t1和t2线程
* t1线程执行到此处,遇到synchronize关键字,就会找this的对象锁,
* 如果找到this对象锁,则进入同步语句块中执行程序。当同步语句块中的代码执行结束后,
* t1归还this对象锁
*
* 在t1线程执行同步语句块的过程中,吐过t2线程也过来执行同步语句块遇到synchronize关键字,
* 所以也去找this的对象锁,但是对象锁被t1线程持有,只能等待this对象的锁归还后再执行。
* 如果synchronize关键字加载方法上,同样找的是this对象。
*
* 当加载synchronize的方法是静态方法时使用的是类锁,类锁也只有一个,如果两个方法都添加synchronize
* 就算不共享对象,后执行的方法也要等类锁归还后执行,和对象锁无关。
*
*/
synchronized (this) {
double after = balance - money;
// 延时
try {Thread.sleep(1000);} catch (InterruptedException e) {}
//更新
this.setBalance(after);
}
}
}
死锁简单例子:
/**
* 对象锁:死锁
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(new T1(o1, o2));
Thread t2 = new Thread(new T2(o1, o2));
t1.start();
t2.start();
}
}
class T1 implements Runnable {
Object o1;
Object o2;
T1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized(o1) {
try {Thread.sleep(1000);} catch (InterruptedException e) {}
synchronized(o2) {// 此时o2被另一个线程锁住,需要等待释放对象锁
}
}
}
}
class T2 implements Runnable {
Object o1;
Object o2;
T2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized(o2) {
try {Thread.sleep(1000);} catch (InterruptedException e) {}
synchronized(o1) {// 此时o1被另一个线程锁住,需要等待释放对象锁
}
}
}
}
4、守护线程
从线程分类上可以分为:用户线程(以上都是用户线程),另一个是守护线程。守护线程伴随用户线程,只有所有的用户线程结束生命周期,守护线程才会结束生命周期,例如java中的垃圾回首器就是一个守护线程,只有应用程序中所有的线程结束,它才结束。
实例:
/**
* 守护线程
* 其他所有的用户线程结束,则守护线程退出!
* 守护线程一般都是无限执行的
* t1.setDaemon(true) 用户线程修改为守护线程
*/
public class ThreadTest08 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Processor08();
t1.setName("t1");
// 将t1这个用户线程修改为守护线程
t1.setDaemon(true);
t1.start();
// 主线程main执行结束后,没有其它用户线程,则守护线程将自动退出
for (int i = 0; i <10; i++) {
System.out.println(Thread.currentThread().getName() + "----->" + i);
Thread.sleep(1000);
}
}
}
class Processor08 extends Thread{
public void run() {
int i = 0;
while(true) {
System.out.println(Thread.currentThread().getName() + "----->" + i);
i++;
try {Thread.sleep(500);} catch (InterruptedException e) {}
}
}
}