进程与线程
相关概念
进程
进程就是程序的一次执行过程,或者是正在执行的程序。是一个动态的过程,有它自身的产生、存在和消亡过程
线程
线程是进程创建的,是进程的一个实体,一个进程可以有多个线程
单线程
同一个时刻只允许执行一个线程
多线程
同一个时刻可以执行多个线程,比如 qq 打开多个聊天窗口
并发
同一个时刻,多个任务交替执行,比如单核 cpu 实现多任务
并行
同一个时刻,多个任务同时执行,比如多核 cpu 实现多个任务
线程使用
使用线程的两种途径
- 继承 Thread
- 实现 Runnable
继承 Thread 使用
当一个类继承了 Thread 类,它就可以当作线程使用,不过需要重写 run 方法,在其中写入自己的业务逻辑
而Thread 类是实现了 Runnable 接口的 run 方法,源码:
@Override
public void run() {
if (target != null) {
target.run();
}
}
使用步骤:
- 实现一个类,继承 Thread
- 重写 run() 方法
- 在 main 方法中新建一个该类的对象,调用 start() 方法
如:
public class MeowThread {
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat();
cat.start(); // 启动线程 Thread-0,最终会调用 run 方法
// 当 main 线程,也就是 main 方法启动子线程 Thread-0 后,主线程不会阻塞
// 即不会等待 Thread-0 执行完毕后才执行
// 此时,主线程和子线程交替执行
for (int i = 0; i < 1000; i++) {
System.out.println("主线程 i= " + i + ",线程名称:" + Thread.currentThread().getName());
// 主线程休眠 0.3s
Thread.sleep(300);
}
}
}
/**
* 当一个类继承了 Thread 类,它就可以当作线程使用
* 需要重写 run 方法,在其中写入自己的业务逻辑
* Thread 类是实现了 Runnable 接口的 run 方法
* @Override
* public void run() {
* if (target != null) {
* target.run();
* }
* }
*/
class Cat extends Thread {
int times = 0; // 记录线程运行次数
@Override
public void run() { // 重写 run 方法,写自己的业务逻辑
while (true) {
// 每隔 0.3 秒,输出一次
System.out.println("喵喵,我是小猫咪 " + (++times) + "次,线程名称:" + Thread.currentThread().getName());
// 让进程休眠一秒
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (times == 1000) { // 运行 20 次后退出
break;
}
}
}
}
可以看到:
- 主线程与子线程交替执行
- 可以在运行的时候在控制台输入 Jconsole,选择代码对应的进程,再点击进程查看资源使用情况
- 中间使用了 start() 方法,而不是直接调用 run() 方法,因为如果是后者,就相当于是 main 方法调用了 run() 方法,并不会开启子线程,就会变成串行化的执行,即 run() 方法执行完毕后才会执行之后的代码
start() 方法源码执行过程:
- 调用 start() 后,会进入到另一个方法 start0()
- start0() 是本地方法,由 JVM 调用,底层由 c/c++ 实现。它是真正实现多线程的方法
- start() 方法调用 start0() 后,线程并不一定会立即执行,只是变成了就绪态,什么时候执行具体看 CPU 什么时候调度
实现 Runnable 接口
- Java 是单继承机制,如果一个类已经继承了某个类,那就无法继承 Thread 类,只能通过实现 Runnable 接口来创建线程
使用步骤:
- 创建一个实现了 Runnable 接口的类
- 实现 run() 方法,在其中写入自己的功能
- 在 main 方法中新建一个该类的对象 obj
- 再在 main 方法中新建一个 Thread 类的对象,并且创建时传入上一步创建的对象 obj
- 调用 Thread 类的对象的 start() 方法
这里使用了设计模式中的代理模式,简单地说就是把一个实现了 Runnable 接口的类的对象 obj 传入 Thread 类的对象 thread 中,在后者的对象 thread 调用 start() 方法时,最终调用的 run() 方法会通过动态绑定机制调用 obj 的 run() 方法
例子:
public class ThreadTest02 {
public static void main(String[] args) {
Dog dog = new Dog();
// dog.start(); 该方法不能用,因为 Dog 类中没有这个方法
// 于是可以通过创建 thread 对象把 dog 对象(实现 Runnable 类的对象)放进去
// 这里底层使用了设计模式--代理模式
Thread thread = new Thread(dog);
thread.start();
}
}
/**
* 通过实现 Runnable 接口创建线程
*/
class Dog implements Runnable {
int count = 0; // 计数
@Override
public void run() {
while (true) {
System.out.println("小狗汪汪汪" + (++count) + "次, 线程名称:" + Thread.currentThread().getName());
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
而且 Runnable 可以达到多个线程共享资源:
public class ThreadTest04 {
public static void main(String[] args) {
Resource resource = new Resource();
Thread thread = new Thread(resource);
Thread thread1 = new Thread(resource);
// 两个线程用了一个对象,消耗了同一份资源 resource
thread.start();
thread1.start();
}
}
class Resource implements Runnable {
int resource = 1000;
@Override
public void run() {
while (resource > 0) {
System.out.println("进程:" + Thread.currentThread().getName() + " 消耗资源,剩余资源" + (--resource));
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
两种方式对比
- 从 Java 设计来看,两种方式本质上没有区别
- 实现 Runnable 接口的方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制
线程终止
- 执行完毕自动终止
- 通知方式,使用变量控制 run 退出的方式
例子:
public class ThreadExit {
public static void main(String[] args) throws InterruptedException {
ExitThread exitThread = new ExitThread();
exitThread.start();
// 如果要在 main 方法中控制线程结束,可以通过修改 loop 变量实现
Thread.sleep(5000);
exitThread.setLoop(false);
}
}
class ExitThread extends Thread {
private int count = 0;
private boolean loop = true;
public boolean isLoop() {
return loop;
}
public void setLoop(boolean loop) {
this.loop = loop;
}
@Override
public void run() {
while (loop) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("running......");
}
}
}
线程常用方法
第一组:
- start()
- run()
- getName()
- setName()
- setPriority() 设置优先级
- getPriority() 更改优先级
- sleep()
- interrupt() 中断线程
例子:
public class ThreadInterrupted {
public static void main(String[] args) throws InterruptedException {
Interrupted interrupted = new Interrupted();
interrupted.setName("Yasuo");
interrupted.setPriority(Thread.MIN_PRIORITY);
interrupted.start();
// 主线程休眠 2 秒后,中断子线程
Thread.sleep(2000);
interrupted.interrupt();
}
}
class Interrupted extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i + " times is running");
}
try {
System.out.println(Thread.currentThread().getName() + "休眠中");
Thread.sleep(10000);
} catch (InterruptedException e) {
// 睡眠中被中断
System.out.println(Thread.currentThread().getName() + "被中断");
}
}
}
第二组:
- yield :让出线程占用的 cpu,但是由于让出时间不确定,因而让出不一定成功。比如当 CPU 资源较多时,足够两个线程使用,那么就不会出现礼让
- join :线程插队。一旦插入成功,那么就会先执行插入的线程的所有任务。比如有两个线程 t1, t2,在 t1 执行的时候调用了方法 t2.join() ,那么 cpu 就会去执行 t2,直至 t2 执行完才会继续执行 t1
例子:
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
JoinThread joinThread = new JoinThread();
joinThread.start();
for (int i = 0; i < 20; i++) {
Thread.sleep(300);
System.out.println("主线程 " + Thread.currentThread().getName() + " 打游戏 " + (i + 1));
// 主线程执行 5 次后,将子线程插队
if (i == 4) {
System.out.println("主线程玩够了,让给子线程");
// 从此处开始,子线程执行完毕才继续执行主线程
joinThread.join();
}
}
}
}
class JoinThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程 " + Thread.currentThread().getName() + " 打游戏 " + (i + 1));
}
}
}
用户线程和守护线程
- 用户线程:也叫工作线程。一般是任务执行完毕结束,或者以被通知的形式结束
- 守护线程:为工作线程服务,当所有用户线程结束后,守护线程自动结束
- 常见的守护线程:垃圾回收机制
可以通过方法 setDaemon() 将线程设置为守护线程
例子:
public class ThreadDaemon {
public static void main(String[] args) throws InterruptedException {
DaemonThread daemonThread = new DaemonThread();
// 若想让 main 线程结束后,子线程自动结束
// 就需要将子线程设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("车子在路上过~~~~~~~~~~");
Thread.sleep(500);
}
}
}
class DaemonThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("杰瑞狗在地里跑~~~~~~~~~");
}
}
}
Java 线程的生命周期
- NEW : 创建后但未启动的线程处于该状态
- RUNNABLE : 正在执行的线程处于该状态 ,其中又包含 Ready 和 Running 两种状态。Ready 就是就绪态,Running 就是运行态。比如处于运行态的线程可以通过调用 yield() 方或者是时间片使用完毕法进入就绪态,而就绪态到运行态则取决于操作系统内核的调度方法
- BLOCKED : 被阻塞等待的线程处于该状态
- WAITING : 正在等待另一个线程执行特定动作的线程处于该状态,比如一个线程调用了 join() 方法,就会进入该状态
- TIMED_WAITRING :正在等待另一个线程执行特定动作达到指定等待时间的线程处于该状态,比如一个线程调用了 sleep() 方法,就会进入该状态
- TEMINATED : 已退出的线程处于该状态
可以通过方法 getState() 获取线程状态
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
State state = new State();
// new 之后,start 之前的状态
System.out.println(state.getName() + " 状态 " + state.getState()); // NEW
state.start();
// start 后的状态
while (Thread.State.TERMINATED != state.getState()) {
System.out.println(state.getName() + " 状态 " + state.getState()); // TIMED_WAITING / RUNNABLE
Thread.sleep(300);
}
// 中止之后的状态
System.out.println(state.getName() + " 状态 " + state.getState()); // TERMINATED
}
}
class State extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
System.out.println("hi " + i);
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
}
线程同步
- 在多线程场景下,有些敏感资源不允许同时被多个线程访问。这个时候就需要线程同步,来确保数据任何时刻只会被一个 线程访问
- 也可以理解为,使用线程同步后,当一个线程对这个内存进行操作时,其他线程都不允许对这个内存再做操作,直至该线程操作完毕,其他线程才能进行操作
线程同步的方法
- 同步代码块
synchronized (对象) { // 得到对象的锁,才能操作同步代码
// 需要被同步的代码
}
- synchronized 还可以放在方法声明中,表示整个方法为同步方法
public synchronized void func(String s) {
// 方法体
}
例子:
public class SellTicket {
public static void main(String[] args) {
SellTicker03 sellTicker03 = new SellTicker03();
Thread thread = new Thread(sellTicker03);
Thread thread2 = new Thread(sellTicker03);
Thread thread3 = new Thread(sellTicker03);
thread.start();
thread2.start();
thread3.start();
}
}
class SellTicker03 implements Runnable {
private int count = 100; // 票数
private boolean loop = true; // 控制线程结束
@Override
public void run() {
while (loop) {
sell(); // 使用线程同步的售票方法
}
}
// 售票方法
// 使用 synchronized 实现线程同步
public synchronized void sell() { // 同步方法,在同一时刻,只能有一个线程执行 sell
if (count <= 0) {
System.out.println("售票结束");
loop = false; // 线程停止循环
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + " 售票,剩余票数" + (--count));
}
}
线程同步原理
- 线程同步就像是给被同步的数据或者内存加了一把互斥锁,每当有一个对象访问被同步内容时,被同步的部分就被加锁,其他线程就无法访问
- synchronized 可以加在方法上,这时锁就是加在 this 上,也就是调用该方法的对象上。在上个例子中,三个Thread对象都使用了一个 SellTicked03 对象,因而这样枷锁可以实现互斥
- synchronized 也可以加在代码块上
- synchronized 加载静态方法上,这时相当于给这个类加锁
- 加锁的粒度越小,性能越高
代码块的加锁方法:
synchronized(this) { // 这里的括号内可以是 this,也可以是其他对象(但需要是同一个对象 )
// 代码块内容
}
比如给上个例子,换一个加锁方式
class SellTicker03 implements Runnable {
private int count = 100; // 票数
private boolean loop = true; // 控制线程结束
private Object obj = new Object();
@Override
public void run() {
while (loop) {
sell(); // 使用线程同步的售票方法
}
}
// 售票方法
public void sell() { // 同步方法,在同一时刻,只能有一个线程执行 sell
// 代码块加锁 this
synchronized(this) {
if (count <= 0) {
System.out.println("售票结束");
loop = false; // 线程停止循环
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + " 售票,剩余票数" + (--count));
}
/*
// 代码块加锁 obj 效果和上面一样
synchronized(obj) {
if (count <= 0) {
System.out.println("售票结束");
loop = false; // 线程停止循环
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + " 售票,剩余票数" + (--count));
}
*/
}
synchronized 加在静态方法和在静态方法中使用同步代码块的写法:
// 如果对静态方法使用 synchronized 关键字,就相当于给本类加锁
public synchronized static void m1() {
}
// 静态方法中实现同步代码块
public static void m2() {
synchronized (SellTicker03.class) { // 括号内需要为本类类名
System.out.println("m22222222");
}
}
注意;
- 同步方法如果没有 static 修饰,默认锁对象为: this
-同步方法如果使用 static 修饰,默认锁对象为:当前类.class
实现互斥锁的步骤:
- 分析需要上锁的代码
- 选择同步代码块或者同步方法
- 多个线程的锁对象要相同
线程的死锁
死锁就是所有线程都处于竞争资源而导致的堵塞状态,会导致所有线程都无法继续执行
例子:
public class ThreadDeadLock {
public static void main(String[] args) {
DeadLock deadLock = new DeadLock(true);
DeadLock deadLock1 = new DeadLock(false);
deadLock.setName("A");
deadLock1.setName("B");
deadLock.start();
deadLock1.start();
}
}
class DeadLock extends Thread {
boolean flag;
static Object o1 = new Object();
static Object o2 = new Object();
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (o1) { // 对象互斥锁,下面是同步代码
System.out.println(Thread.currentThread().getName() + " 进入 1");
synchronized (o2) { // 对象互斥锁,下面是同步代码
System.out.println(Thread.currentThread().getName() + " 进入 2");
}
}
} else {
synchronized (o2) { // 对象互斥锁,下面是同步代码
System.out.println(Thread.currentThread().getName() + " 进入 3");
synchronized (o1) { // 对象互斥锁,下面是同步代码
System.out.println(Thread.currentThread().getName() + " 进入 4");
}
}
}
}
}
在上述例子中,程序运行后有可能会进入死锁状态:
- 当线程 A 进入 run() 方法,会先拿到对象 o1 的锁,如果在 A 还未拿到 o2 的锁时,线程 B 也进入了 run() 方法,也就会提前拿到 o2 的锁。
- 那么接下来 A 会等待 o2 锁,B 会等待 o1 锁,进入了一个无限等待的过程
- 这就是一个死锁
释放锁
以下操作会释放锁
- 当前线程同步方法、同步代码块执行完毕。就像打游戏打完了
- 当前线程在同步方法、同步代码块中遇到 break、return。就像打游戏打了一半被叫去吃饭
- 当前线程在同步方法、同步代码块中发生异常导致程序结束。就像打游戏打了一半手柄坏了
- 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。就像打游戏打到一半,这时需要排队进一个服务器,过一会才能再打
以下情况不会释放锁
- 当前线程在同步方法、同步代码块中调用了 sleep()、yield() 方法暂停当前线程执行,不会释放锁。就像打游戏打着打着睡着了,但是其实游戏没下线
- 线程执行同步代码块中,其它线程调用了该线程的 suspend() 方法将该线程挂起。就像打游戏打着打着突然掉线了,并不会离开,会准备继续打