一、前言
这篇文章是介绍Java基础知识——多线程的,读完这篇文章,各位小伙伴们可以收获哪些呢?
- 程序、进程、线程三者的区别
- 创建线程的四种方式
- 线程的常用方法
- 线程的死锁、释放锁
- 线程的同步原理、生命周期等等
废话不多说,干货满满,赶快来看看吧~
二、基本概念
什么是程序?
- 程序是为完成特定任务、用某种语言编写的一组指令的集合。大白话来讲,就是我们写的代码
什么是进程?
- 进程是程序的一次执行过程,或是正在运行的一个程序。例如运行中的QQ、微信,运行中的MP3播放器等等,有它自身的产生、存在和消亡的过程
什么是线程?
- 线程是由进程创建的,是进程的一个实体,一个进程可以拥有多个线程,也可以有单个线程
三、创建线程
1、方式一
继承 Thread类
,看代码:
public class Thread_one {
public static void main(String[] args) throws InterruptedException {
Person person = new Person();
person.start();
//当启动线程时,子线程和主线程会交替执行
//主线程结束,子线程不一定结束
//person.run()就是一个普通的方法,没有真正的启动一个线程
for (int i = 0; i < 20; i++) {
System.out.println("main线程执行");
Thread.sleep(1000);
}
}
}
class Person extends Thread {
@Override
public void run() {
int times = 0;
while (true) {
try {
System.out.println("大家好,我是卷心菜~"
+ (++times)
+ "号" + "线程名是——"
+ Thread.currentThread().getName());
Thread.sleep(2000);
if (times == 50) break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
该代码的功能是:启动一个子线程,在控制台打印50次 “大家好,我是卷心菜”
,主线程在控制台打印 “main线程执行”
,运行的部分结果如下:
我们来重点剖析一下这种创建方式,因为后面三种方式本质上区别不大。
当我们 Person类
继承 Thread类
以后,就要重写 run()
,方法体内写上自己需要的代码逻辑。语句 Thread.sleep(2000);
意思是,每打印一句话在控制台后,子线程就要“休息”2秒,在这“休息”的时间,主线程就开始执行自己的语句;同样,在主线程“休息”的时间,子线程也开始执行自己的语句。
总之,子线程和主线程会交替执行,多次运行可以发现,主线程结束,子线程不一定结束。这里需要特别注意。
爱思考的小伙伴们可能就要问了:我们在Person类中重写了run(),但是怎么使用线程的时候,调用的是start(),那run()谁来执行的呢?别着急,让我们进去源码看一看。
我们从源码中可以发现,start()调用了start0(),而start0()是一个本地方法,由JVM调用,只是我们看不到而已。
2、方式二
方式一明白了,再看看第二种方式,代码如下:
public class Thread_two {
public static void main(String[] args) {
Animal animal = new Animal();
Thread thread = new Thread(animal);
thread.start();
}
}
class Animal implements Runnable {
int times = 0;
@Override
public void run() {
while (true) {
System.out.println("大家好,我是Tomcat~");
try {
Thread.sleep(2000);
++times;
if (times == 10) break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
此代码逻辑也很好理解,这里就不过多讲述了。我们来分析一下:为什么要使用类实现接口的方式来创建线程?
学过基础的小伙伴们都知道,我们Java是单继承的。因此,在某些情况下一个类可能已经继承了某个类,这时再用继承Thread类方法来创建线程就不可以了,所以我们有了Runnable接口。
3、方式三
再来看看方式三,代码如下:
public class Thread_six {
public static void main(String[] args) throws Exception {
D d = new D();
FutureTask futureTask = new FutureTask(d);
Thread thread = new Thread(futureTask);
thread.start();
Object o = futureTask.get();
System.out.println(o);
}
}
class D implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 5; i++) {
System.out.println(i + 1);
sum += i;
}
return sum;
}
}
来看看运行结果:
与 Runnable类
相比,类 Callable
功能更强大些,具体有如下优点:
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
4、方式四
最后来看看第四种方式,使用线程池,也是最推荐使用的方式,代码如下:
public class Thread_seven {
public static void main(String[] args) {
ExecutorService service =
Executors.newFixedThreadPool(10);
// service 可以理解为池子
service.execute(new F()); //适用于Runnable
// service.submit(); //适用于Callable
service.shutdown();//关闭线程池
}
}
class F implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i + 1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果没有任何问题:
四、常用方法
1、第一组
- public Thread(Runnable target):返回对当前正在执行的线程对象的引用
- public synchronized void start():开启线程
- public final synchronized void setName(String name):设置线程名
- public final void setPriority(int newPriority):设置线程的优先级别
- public static native void sleep(long millis):设置线程的睡眠时间
- public void interrupt():中断线程,但并没有结束线程,一般用于中断正在休眠的线程
这么多的方法,用代码实践一下:
public class Thread_five {
public static void main(String[] args) {
A a = new A();
Thread thread = new Thread(a);
thread.setName("棒棒糖线程");
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
thread.interrupt();
for (int i = 0; i < 20; i++) {
System.out.println("主线程开始行动");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class A implements Runnable {
@Override
public void run() {
while (true) {
try {
for (int i = 0; i < 10; i++) {
System.out.println("我要吃" + (i + 1) +
"棵棒棒糖,所处的线程是:"
+ Thread.currentThread().getName());
}
System.out.println("吃多了,要消化20秒");
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
带大家看一下线程的优先级:
MAX_PRIORITY:10 MIN _PRIORITY:1 NORM_PRIORITY:5
截取一部分运行结果:
解析一下:由于把子线程的优先级设置为最低,主线程拿到了优先执行的语句,输出 "主线程开始行动"
,然后在主线程睡眠时间时,子线程开始执行,当执行完:吃多了,要消化20秒时,本应该等待20秒,但是interrupt()打断了子线程,所以就会出现上图的情况。
如果把语句 thread.interrupt();
删去,就会出现下图的结果:
2、第二组
- public final void join():当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
- public static native void yield():暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
直接上代码演示:
public class Thread_one {
public static void main(String[] args) throws InterruptedException {
T t = new T();
Thread thread = new Thread(t);
thread.start();
for (int i = 0; i < 15; i++) {
Thread.sleep(100);
System.out.println("主线程吃了" + (i+ 1) + "颗棒棒糖");
if (i == 4) {
System.out.println("让子线程先吃吧");
thread.join();
// thread.yield();
System.out.println("子线程吃完了,该我了");
}
}
}
}
class T implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程吃了" + (i + 1) + "颗棒棒糖");
}
}
}
运行结果:
由代码和运行结果图可以看出:因为语句 thread.join();
的出现,导致主线程必须等到子线程结束后才能继续执行自己的代码,这就是 join()
的作用。
五、守护线程
Java中的线程分为两类:一种是守护线程,一种是用户线程。 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用 thread.setDaemon(true)
可以把一个用户线程变成一个守护线程。
下面用代码演示一下:
public class Thread_two {
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread thread = new Thread(a);
//下面的方法要在start方法使用前使用,目的是当我们希望main方法结束后,子线程也可以结束;即把子线程设为守护线性
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 10; i++) {
Thread.sleep(100);
System.out.println("我在疯狂打游戏~");
}
}
}
class A implements Runnable{
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
System.out.println("室友们在努力内卷学习~");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
即当语句 我在疯狂打游戏
结束之后,守护线程的执行语句 室友们在努力内卷学习
就结束了,不在执行。
六、生命周期
要想实现多线程,必须在主线程中创建新的线程对象。我们可以使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五 种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束 代码演示一下:
public class Thread_three {
public static void main(String[] args) throws InterruptedException {
B b = new B();
Thread thread = new Thread(b);
System.out.println(thread.getName() + "状态为" + thread.getState());
thread.start();
//只要子线程状态不是终止状态,就继续查看子线程的状态
while (Thread.State.TERMINATED != thread.getState()) {
System.out.println(thread.getName() + "状态" + thread.getState());
Thread.sleep(500);
}
System.out.println(thread.getName() + "状态" + thread.getState());
}
}
class B implements Runnable {
@Override
public void run() {
while (true) {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
System.out.println("我说了" + (i + 1) + "次Hi");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
}
}
}
运行结果:
七、线程的同步
下面案例都以模仿卖票入手,解决卖超的情形
1、方式一
- 同步方法
public class Thread_four {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
thread1.start();
thread2.start();
}
}
class Ticket implements Runnable{
private int tickets = 30;
@Override
public synchronized void run() {
while (true) {
try {
if (tickets <= 0) {
System.out.println("票没了,售票结束!");
break;
}
System.out.println("窗口" + Thread.currentThread().getName()
+ "出售了一张票,还剩下"
+ --tickets + "张票");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
2、方式二
- 同步代码块
class Ticket implements Runnable {
private int tickets = 30;
@Override
public void run() {
synchronized (this) {
while (true) {
try {
if (tickets <= 0) {
System.out.println("票没了,售票结束!");
break;
}
System.out.println("窗口" + Thread.currentThread().getName()
+ "出售了一张票,还剩下"
+ --tickets + "张票");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
八、锁
1、死锁
public class Thread_five {
public static void main(String[] args) {
DeadLock deadLock1 = new DeadLock(true);
DeadLock deadLock2 = new DeadLock(false);
Thread thread1 = new Thread(deadLock1);
Thread thread2 = new Thread(deadLock2);
thread1.start();
thread2.start();
}
}
class DeadLock implements Runnable {
static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用 static
static Object o2 = new Object();
boolean flag;
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//下面业务逻辑的分析 //1. 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
// 2. 如果线程 A 得不到 o2 对象锁,就会 Blocked
// 3. 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
// 4. 如果线程 B 得不到 o1 对象锁,就会 Blocked
if (flag) {
synchronized (o1) {//对象互斥锁, 下面就是同步代码
System.out.println(Thread.currentThread().getName() + " 进入 1");
synchronized (o2) { // 这里获得 li 对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入 2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " 进入 3");
synchronized (o1) { // 这里获得 li 对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入 4");
}
}
}
}
}
运行结果:
2、释放锁
因为不好演示,所以就介绍一下释放锁的各种情况:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、return
- 当前线程在同步代码块、同步方法中出现了未处理的ERROR、Exception,导致异常结束
最后再介绍不会释放锁的情况:
- 线程执行同步代码块或同步方法时,程序调用sleep()、yield()暂停当前线程的执行,不会释放锁
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程也不会释放锁
总结
乐莫乐兮新相知,很高兴各位小伙伴可以坚持看完这篇文章。 一起加油,一起进步!