Java多线程基础
1.多线程简介
在了解多线程之前我们要先知道什么是进程和线程:
- 进程:进程是系统进行调度和分配资源的基本单位,通俗的讲就是一个正在运行和后台运行的程序。我们可以通过任务管理器看到系统有哪些进程在运行。
- 线程: 线程是CPU调度的最小单位,一个进程可能由多个或一个线程组成。比如我们在看视频是同时还能发弹幕。
核心概念:
- 线程就是独立的执行路径
- 在Java程序运行时,即使我们自己没有创建线程,JVM也会创建多个线程,如主线程(main())、GC线程。
- main()被称为主线程,是正程序的入口,用于执行整个程序
- 在一个线程中,如果开辟了多个线程,线程的运行是由调度器安排调度的,调度器是与操作系统紧密相关的。
- 对于同一份资源操作时,会出现资源抢夺问题,需要加入并发控制。
- 线程会带来额外的开销,如CPU调度时间、并发控制开销等。
- 每个线程在自己的工作内存中交互,内存控制不当会造成数据不一致问题。
2.线程实现方法
创建线程有多种方式,最基本的三种: Thread类、Runnable接口、Callable接口
1.通过继承Thread类:
重写Thread类中的run(),run()方法中执行我们自己的逻辑
package com.zw.tread.base;
/**
* @author zhouwei
* @version 1.00
* @className CreateThreadTest
* @describe 创建线程测试类
* @since 2020/4/19
*/
public class ThreadTest extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("我是创建的线程"+ Thread.currentThread().getName()+ "--->i==" + i);
}
}
public static void main(String[] args) {
// 创建线程 --- 线程创建
ThreadTest threadTest = new ThreadTest();
threadTest.setName("superMan");
// 线程进入就绪状态
threadTest.start();
//主线程中的for循环
for (int n = 0; n < 5; n++) {
System.out.println("我是主线程"+"--->n==" + n);
}
}
}
运行结果如下图,可以看出线程之间是交替执行的,每次执行结果可能都不一样,线程的运行是由调度器安排调度的,并且是被CPU轮流执行的:
image.png
2.通过实现Runnable接口创建线程
package com.zw.tread.base;
/**
* @author zhouwei
* @version 1.00
* @className RunnableTest
* @describe Runnable接口创建线程
* @since 2020/4/19
*/
public class RunnableTest implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("我是创建的线程"+ Thread.currentThread().getName()+ "--->i==" + i);
}
}
public static void main(String[] args) {
// 创建Runnable接口实现类
RunnableTest runnableTest = new RunnableTest();
// 创建线程
Thread thread = new Thread(runnableTest);
// 调用线程
thread.start();
// 执行主线程for循环
for (int n = 0; n < 5; n++) {
System.out.println("我是主线程"+"--->n==" + n);
}
}
}
运行结果:
image.png
3.线程状态
下面简单的介绍一下线程的五种状态:
- 创建状态(new): 线程被创建后,就进入了创建状态。例如:Thread thread = new Thread()。
- 就绪状态(Runnable): 就绪状态也被称为“可执行状态”。线程对象被创建后,调用其start(),该线程就进入就绪状态了,例如: thread.start()。线程进入就绪状态后就随时可能被CPU调用。
- 运行状态(Running):当线程获得CPU资源后,线程就进入运行状态,进入运行状态后,才会执行我们所写的逻辑代码。这里需要注意线程只能从就绪状态进入运行状态
-
阻塞状态(Blocked):阻塞状态是线程由于某种原因失去了CPU资源使用权,暂时停止运行。阻塞情况分为三种:
- 等待阻塞:通过调用线程wait()方法,让线程等待某种工作的完成在继续执行。
- 同步阻塞:线程获取synchronized同步锁失败,线程会进入同步阻塞状态。
- 其他阻塞:其他阻塞产生的情况比较多,通过调用线程的sleep()、join()方法或者发出IO请求时,线程会进入阻塞状态,只有当sleep()超时、join等待线程终止或超时、IO处理完毕是,线程重新进入就绪状态。
- 死亡状态(Dead):线程执行完成或因为异常退出run()方法。
4.线程同步
1.线程并发问题:
在多线程编程中,有些共享数据是不允许多个线程同时访问的,如果同时访问可能会出现数据同步问题。
下面通过买票例子,展现多线程并发问题。
/**
* @author zhouwei
* @version 1.00
* @className ThreadDemo01
* @describe 线程并发问题,例子买票功能
* @since 2020/4/20
*/
public class ThreadDemo01 implements Runnable {
/**
* 门票总个数
*/
private int ticketNum = 30;
@Override
public void run() {
while (true){
if(ticketNum <= 0){
System.out.println("票买完了。。。");
break;
}
System.out.println("线程" + Thread.currentThread().getName() + "买了一张票,剩余"+ ticketNum--+"张");
try {
// 模仿延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadDemo01 demo01 = new ThreadDemo01();
new Thread(demo01,"AA").start();
new Thread(demo01,"BB").start();
new Thread(demo01,"CC").start();
}
}
运行结果如果下图,从我们可以看出多线程并发问题,剩余票数出现混乱问题:
image.png
5.线程通讯问题
简介:线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
下面介绍通过Object类中的wait()和notify方法实现线程通信:
例子:定义一个仓库类(Depot),有两个方法一个是生产商品(produce),一个是消费商品(consume),启动两个线程,一个线程值生产,一个线程值消费。
public class WaitAndNotifyTest03 {
static class Depot {
/**
* 商品数量
*/
private Integer goodNum = 0;
private synchronized void produce(){
// 如果商品数量大于0,则等待
while (goodNum > 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int num = new Double(Math.random() * 100).intValue();
goodNum =+ num;
System.out.println("我生产了商品" + num + "个");
// 通知消费者消费
notify();
}
private synchronized void consume(){
// 如果商品数量大于0,则等待
while (goodNum <= 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int n = goodNum;
goodNum =- goodNum;
// 通知消费者消费
System.out.println("我消费了商品" + n + "个");
notify();
}
}
public static void main(String[] args) {
Depot depot = new Depot();
new Thread(() ->{
for(int i=0;i<10;i++){
depot.produce();
}
}).start();
new Thread(()->{
for(int i=0;i<10;i++){
depot.consume();
}
}).start();
}
}
6.线程其他方法
下面介绍一下Thread类的一些常用方法:
- setName(String): 该方法可以给线程设置名称,方便查询
- setPriority(Integer) : 该方法可以给线程设置优先级,但是线程的执行顺序还是有cpu决定的。
- setDeamon(boolean): 该方法设置线程是否由后台运行,false表示是,true表示不是。
- interrupt(): 中断线程,并不会真正中断线程,只是修改中断状态
- isInterrupted(): 获取线程是否被中断
- Interrupted(): 获取线程是否被中断,然后清除状态
- supspend()暂停、resume()恢复、stop()停止,这些方法已经过期不建议使用,原因如 supspend()调用这个方法线程并不会释放资源,而是占用资源进入休眠状态,可能会引发死锁。stop()方法同样没有给线程释放资源的机会就结束线程了。
- sleep(Integer):线程休眠,并不会释放锁
- yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入到就绪状态。即让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
- join():A线程调用B线程的join()方法,将会使A等待B执行,直到B线程终止。如果传入time参数,将会使A等待B执行time的时间,如果time时间到达,将会切换进A线程,继续执行A线程。
网友评论