多线程基础
进程线程
- 进程是指运行中的程序的实例, 是程序的一次执行过程, 是系统进行资源分配和调度的一个独立单位
- 进程是动态的, 是程序的一次执行过程, 包含它自身的产生, 消亡等过程
- 线程由进程创建的, 是进程的一个实体
并发并行
- 单线程: 同一个时刻, 只允许执行一个线程
- 多线程: 同一时刻可以执行多个线程
- 并发: 并发是同一个时刻, 多个任务交替执行
- 并行: 并行是同一个时刻, 多个任务同时执行
- 并发和并行可能同时存在.
Runtime runtime = Runtime.getRuntime();
// 获取当前电脑的 CPU 核心数
System.out.println(runtime.availableProcessors());
// 32
继承 Thread 创建线程
public class Thread01 {
public static void main(String[] args) {
// 创建一个 cat 对象, 可以当作线程使用
Cat cat = new Cat();
cat.start();
// 这样执行一次就结束了, 因为线程里就执行一次
}
}
// 1. 当一个类继承了 Thread 类, 该类就可以当做线程使用
// 2. 我们一般会重写 run 方法, 写上自己的业务代码
// 3. run Thread 类, 实现了 Runnable 接口的 run 方法
class Cat extends Thread {
@Override
public void run() {
System.out.println("喵喵");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
稍微修改
class Cat extends Thread {
private int time =0;
@Override
public void run() {
while(true) {
time++;
System.out.println("喵喵" + time);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (time >= 5){break;}
}
}
}
多线程机制
可以通过 Thread.currentThread().getName()
获取当前线程的名字
- 程序启动以后会先开启一个主线程 main, 主线程又可以开启多个子线程
- 比如上面的例子, 主线程是 main, 子线程是 Thread-0
- 可以是用 JConsole 查看线程的状态
- 主线程执行完毕, 子线程还在执行, 程序不会结束
为什么是 start
为什么是 start 方法, 而不是 run 方法.
- start 方法是启动线程, 如果是run 方法, 就是普通的方法调用, 没有启动新的线程, 相当于是普通的方法调用
- start 方法会启动一个 start0 方法, 是一个 native 方法, 是由 JVM 调用的
- 真正实现多线程效果的是 start0 方法.
- start 方法调用 start0 方法后, 该线程不会立刻执行, 只是将线程变成了可运行状态, 具体什么时候执行, 取决于 CPU, 由 CPU 统一调度
Runnable 创建线程
- Java 是单继承的, 如果一个类已经继承了其他类, 就不能再继承 Thread 类
- Java 设计这提供了另一个方法创建线程, 通过实现 Runnable 接口来创建线程
package threaduse;
/**
* @Author: 大西瓜
* @Date: 2024/3/27 11:34
* @Description: 写介绍
* @Version: v1.0
*/
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
// 这时候不能调用 start, 但是也不能用 run 啊
// 因此可以用下面这种方法
Thread thread = new Thread(dog);
thread.start();
// 使用了一个设计模式: 代理模式
}
}
class Dog implements Runnable{
int count = 0;
@Override
public void run() {
while (true){
count ++;
System.out.println("aoaoaoao"+count+Thread.currentThread().getName());
if (count>=5){
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
多个子线程案例
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread = new Thread(t1);
Thread thread1 = new Thread(t2);
thread.start();
thread1.start();
}
}
class T1 implements Runnable {
int count = 0;
@Override
public void run() {
while(true){
count++;
System.out.println("T1 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count >= 10) {break;}
}
}
}
class T2 implements Runnable {
int count = 0;
@Override
public void run() {
while(true){
count++;
System.out.println("T2 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count >= 10) {break;}
}
}
}
- Runnable 接口的好处是可以共享资源, 可以让一个对象T1同时放到两个线程里, 共享T1对象的资源
- Runnable 接口避免了单继承的局限性
- 建议使用 Runnable 接口创建线程
多线程售票问题
public class SellTicket {
public static void main(String[] args) {
SellTicket01 sellTicket1 = new SellTicket01();
SellTicket01 sellTicket2 = new SellTicket01();
SellTicket01 sellTicket3 = new SellTicket01();
SellTicket01 sellTicket4 = new SellTicket01();
sellTicket1.start();
sellTicket2.start();
sellTicket3.start();
sellTicket4.start();
}
}
// 三个窗口售票100张
class SellTicket01 extends Thread{
private static int ticketNum = 100;
@Override
public void run() {
while (true){
if (ticketNum<=0){
System.out.println(Thread.currentThread().getName()+" 售票结束");
break;
}
ticketNum--;
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("窗口: "+Thread.currentThread().getName()+" 售出一张票, 剩余票数: "+ticketNum);
}
}
}
正常来说会超卖, 不知道为什么我的没有超卖, 但是这种超卖是正常的, 常识使用 Runnable 接口创建线程.
class SellTicket02 implements Runnable{
private int ticketNum = 100;
@Override
public void run() {
while (true){
if (ticketNum<=0){
System.out.println(Thread.currentThread().getName()+" 售票结束");
break;
}
ticketNum--;
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("窗口: "+Thread.currentThread().getName()+" 售出一张票, 剩余票数: "+ticketNum);
}
}
}
按理来说我这里也会超卖, 不知道为什么没超卖
通知线程退出
public class ThreadExit_ {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
Thread.sleep(5000);
// 如果希望main 线程去终止 t 线程, 需要可以修改 loop 变量
// 这种方式称为通知方式
t.setLoop(false);
}
}
class T extends Thread{
private boolean loop = true;
@Override
public void run() {
int count = 0;
while(loop) {
try {
Thread.sleep(50);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("运行中..."+(++count));
}
}
public void setLoop(Boolean loop){
this.loop = loop;
}
}
线程中断
线程还有一些常用方法
- setName:设置线程名称
- getName:获取线程名称
- start:启动线程
- run:调用对象的 run 方法
- setPriority:设置线程优先级
- getPriority:获取线程优先级
- sleep:线程休眠指定的毫秒数
- interrupt:中断线程
Priority的优先级最小为1, 一般为 5, 最大为10.
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException {
// 测试相关方法
T t = new T();
t.setName("大西瓜线程");
t.setPriority(Thread.MIN_PRIORITY);
t.start();
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+": hi~");
Thread.sleep(700);
}
t.interrupt();
}
}
class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
System.out.println("当前线程的名称: " + Thread.currentThread().getName());
}
try {
System.out.println(Thread.currentThread().getName() + " 休眠中...");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("发生 Interrupt");
}
}
}
}
这个 interrupt 实际上是中断子线程的休眠.
线程插队
常用方法2:
- yield:线程让步, 释放 CPU 执行权, 但是不会释放锁, 只是让出 CPU 时间片, 重新进入就绪状态
- join:插队, 让其他线程等待当前线程执行完毕, 也可以设置等待时间
yield 可能会不成功, 比如资源很多, join 是让别人插队, 只执行它.
守护线程
- 用户线程: 也叫工作线程, 当线程的任务执行完或通知方式结束
- 守护线程: 一般是为工作线程服务的, 当所有的用户线程结束, 守护线程自动结束
- 常见的守护线程: 垃圾回收线程
普通线程是主线程结束了, 但是子线程还在执行, 守护线程是主线程结束了, 守护线程也结束了.
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
// 如果我们希望, 当 main 线程结束后, 子线程自动退出
// 只需将子线程设置为守护线程即可
myDaemonThread.setDaemon(true);
// 先设置在执行
myDaemonThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main 线程存活...");
Thread.sleep(1000);
}
}
}
class MyDaemonThread extends Thread {
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子线程存活...");
}
}
}
只需要在 start 前简单设置一下即可.
线程7大状态
- NEW
- RUNNABLE
- Ready
- Running
- BLOCKED
- WAITING
- TIMED_WAITING
- TERMINATED
RUNNABLE 有两种状态, 一种是 Ready, 一种是 Running, 是可运行状态, 但是是否真实运行, 取决于 CPU 调度. 因此可以细分成两种状态. yield 就是切换到 Ready 状态.
public class ThreadState_ {
public static void main(String[] args) throws InterruptedException {
T t = new T();
System.out.println(t.getName() + "的状态为: " + t.getState());
t.start();
while (Thread.State.TERMINATED != t.getState()){
System.out.println(t.getName() + "的状态为: " + t.getState());
Thread.sleep(500);
}
System.out.println(t.getName() + "的状态为: " + t.getState());
}
}
class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
break;
}
}
}
大部分都是 TIME_WAITING, 因为 sleep 了 10 秒.
线程同步机制
线程同步机制:
- 在多线程编程里, 一些敏感数据不允许被多个线程同时访问, 此时就使用同步访问技术, 保证数据在任何同一时刻, 最多有一个线程访问, 以保证数据的完整性
- 线程同步, 即当有一个线程在对内存进行操作时, 其他线程都不可以对这个内存地址进行操作, 直到该线程完成操作, 其他线程才能对该内存地址进行操作.
同步具体方法-Synchronized:
- 同步代码块
synchronized(对象){// 需要被同步的代码;}
- synchronized 修饰方法
public synchronized void method(){};
整个方法为同步方法.
互斥锁
synchroniezd 是一种非公平锁.
- Java 语言中, 引入了对象互斥锁的概念, 来保证共享数据操作的完整性
- 每个对象都对应于一个可称为"互斥锁"的标记, 这个标记用于保证在任一时刻, 只能有一个线程访问该对象
- 关键字 synchronized 来与对象的互斥锁联系. 当某个对象用 synchronized 修饰时, 表明该对象在任一时刻只能由一个线程访问
- 同步的局限性: 导致程序的执行效率要降低
- 同步方法(非静态的)的锁可以是this, 也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁, 是当前类本身
// 这时候锁为当前类本身
public synchronized void method() {
}
// 如果是静态方法
// 锁是加载 Class 对象上, 比如 xxx.class
public synchronized static void method() {
}
// 因此如果是静态方法要在代码块里面, 需要锁住当前类
public static void method() {
synchronized (xxx.class) { // 而不能写成 synchronized (this)
}
}
多个线程的锁对象为同一个即可.
线程死锁
A 线程占用了 B 线程需要的一个资源, B 线程拿到了 A 线程需要的一个资源, 但是 A 线程需要 B 线程的资源, B 线程需要 A 线程的资源, 两个线程都在等待对方释放资源, 造成死锁.
释放锁
练习
1
- 在 main 方法中启动两个线程
- 第一个线程循环打印100个的整数
- 直到第二个线程从键盘读到了Q命令
public class Homework1 {
public static void main(String[] args) {
A a = new A();
B b = new B(a);
a.start();
b.start();
}
}
class A extends Thread{
private boolean flag = true;
@Override
public void run() {
int count = 0;
while (flag) {
count++;
System.out.println("count: "+ count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count >= 100){
break;
}
}
}
public void setFlag(boolean flag){
this.flag = flag;
}
}
class B extends Thread{
private A a;
private Scanner scanner = new Scanner(System.in);
public B(A a){
this.a = a;
}
@Override
public void run() {
while(true){
System.out.println("请输入");
char key = scanner.next().toUpperCase().charAt(0);
if (key == 'Q'){
a.setFlag(false);
break;
}
}
}
}
2
- 两个用户分别从同一个卡上取钱, 余额为 1000 元
- 每次取钱 100 元, 余额不足就不能取款
- 不能超取
public class Homework2 {
public static void main(String[] args) {
User user = new User();
User user1 = new User();
user.start();
user1.start();
}
}
class User extends Thread{
private static Integer balance = 1000;
@Override
public void run() {
while (true){
synchronized (User.class) {
if (balance <100){
break;
}
balance -= 100;
System.out.println(Thread.currentThread().getName() +" 取出 100元, 余额为: "+balance);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
或者一个User, 然后实现 Runnable 接口, 传入两个线程. 这样就不用静态变量了.