因为多线程这一块基本没怎么学过,因此想跟着《Java多线程编程核心技术》这本书把多线程从头到尾学一遍,一边写一边跟着做下笔记,加油吧?
一、进程和线程
进程
如果打开一个任务管理器,那么里面所有的 exe 程序都可以理解成一个进程。Adobe 是一个进程,Chrome 也是一个进程
线程
线程可以理解成在进程中独立运行的子任务。比如,QQ.exe 运行时就有很多子任务在同时运行,比如,好友视频线程、下载文件线程、传输数据线程,这些不同的任务都可以同时运行,每一项任务可以理解成是线程在工作
好处
可以最大限度地利用CPU地空闲时间来处理其他任务,比如可以让操作系统处理正在由打印机打印地数据,一边使用Word编辑文档,而CPU在这些任务之间不停地切换,由于切换地速度非常快,给使用者地感受就是这些任务似乎是在同时进行
对于前者,任务2只能等待任务1完成任务之后运行,单任务的特点就是排队执行,也就是同步,后一条命令必须等待前一条命令执行完才可以执行,效率很低
对于后者,CPU可以在任务1和2之间来回切换,任务2不用等待任务1执行完再去执行,这也是异步
二、两种方式创建多线程
2.1 继承 Thread 类
创建一个类,继承 Thread 类,并且重写 run 方法,在 run 方法中存放线程需要执行的任务
public class ThreadTest extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "在运行"
+ " " + i);
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
//调用 Thread 类中的 start 方法,即 run 方法
threadTest.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "在运行"
+ " " + i);
}
}
}
结果
main在运行 0
Thread-0在运行 0
main在运行 1
Thread-0在运行 1
main在运行 2
Thread-0在运行 2
main在运行 3
Thread-0在运行 3
main在运行 4
Thread-0在运行 4
注意,每次调用 Thread 类中的实例方法 start,其实就是调用 run 方法,也就是 Thread 子类重写之后的 run 方法
Thread 中的 start 方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的 run() 方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,使线程得到运行,启动线程,具有异步执行的效果。
如果直接调用 thread.run() 就不是异步执行了,而是同步执行,那么此线程对象并不交给“线程规划器”来处理,而是直接由 main 主线程来调用 run() 方法,必须按照顺序执行
2.2 实现Runnable接口实现多线程
如果要创建的线程类已经有一个父亲了,这个时候就不能再继承 Thread 类了,因为 Java 不能支持多继承,因此只能实现 Runnable 接口来完成
public class RunnableTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("run 在运行");
}
}
public static void main(String[] args) {
//产生 Runnable 接口的子类的实例化对象
RunnableTest test = new RunnableTest();
//将对象传入 Thread 的构造方法
Thread thread = new Thread(test);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main 在运行");
}
}
}
结果:
main 在运行
main 在运行
main 在运行
main 在运行
main 在运行
run 在运行
run 在运行
run 在运行
run 在运行
run 在运行
三、共享数据
我们使用 Thread 类进行测试
public class MyThread4 extends Thread{
private int count = 5;
public MyThread4(String name) {
this.setName(name);
}
@Override
public void run() {
while (count > 0) {
count--;
System.out.println("由 " + this.currentThread().getName() +
"计算, count=" + count);
}
}
public static void main(String[] args) {
//一个创建 3 个线程,每个线程各自进行自己的操作
MyThread4 a = new MyThread4("AA");
MyThread4 b = new MyThread4("BB");
MyThread4 c = new MyThread4("CC");
//调用各个线程的 run 方法
a.start();
b.start();
c.start();
}
}
结果:从结果可以看到,一共创建了 3 个线程,每个线程都有各自的 count 变量,run 方法减少的是自身的 count 值,与其他线程的无关
由 BB计算, count=4
由 AA计算, count=4
由 CC计算, count=4
由 AA计算, count=3
由 BB计算, count=3
由 BB计算, count=2
由 BB计算, count=1
由 BB计算, count=0
由 AA计算, count=2
由 AA计算, count=1
由 AA计算, count=0
由 CC计算, count=3
由 CC计算, count=2
由 CC计算, count=1
由 CC计算, count=0
可见,这样的情况是变量不共享的
我们可以对上面的代码进行改进,同样使用 Thread 的构造函数,但是第一个变量是 Runnable 类型
public class MyThread5 extends Thread{
private int count = 5;
@Override
public void run() {
//这里的 count 是公共的变量,每个线程进来都会改变这个变量
count--;
System.out.println("由 " + this.currentThread().getName() +
"计算, count=" + count);
}
public static void main(String[] args) {
MyThread5 t1 = new MyThread5();
Thread a = new Thread(t1, "AA");
Thread b = new Thread(t1, "BB");
Thread c = new Thread(t1, "CC");
Thread d = new Thread(t1, "DD");
Thread e = new Thread(t1, "EE");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
结果:
由 AA计算, count=4
由 BB计算, count=3
由 CC计算, count=2
由 DD计算, count=1
由 EE计算, count=0
但其实经常会出现打印的数据是重复的情况,即两个 count 是同一个数值,即产生了非线程安全的问题
我们可以使用 synchronized 关键字来解决,这个后面再说
public class MyThread6_1 implements Runnable {
private int count = 5;
@Override
public void run() {
count--;
System.out.println("由 " + Thread.currentThread().getName() +
"计算, count=" + count);
}
public static void main(String[] args) {
MyThread6_1 thread = new MyThread6_1();
Thread t1 = new Thread(thread);
Thread t2 = new Thread(thread);
Thread t3 = new Thread(thread);
Thread t4 = new Thread(thread);
Thread t5 = new Thread(thread);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
结果是:
由 Thread-0 计算, count=4
由 Thread-1 计算, count=3
由 Thread-2 计算, count=2
由 Thread-3 计算, count=1
由 Thread-4 计算, count=0
四、线程的状态
任何线程都有 5 种状态,即创建、就绪、运行、阻塞、终止
1.新建状态
在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其他资源,但还是处于不可运行的状态。新建一个线程对象可采用线程构造方法来实现。比如 Thread thread = new Thread()
2.就绪状态
新建线程对象后,调用该线程的 start 方法可以启动线程,当线程启动后,线程进入就绪状态,此时线程将进入线程队列排队,等待CPU服务,这表明它已经具备运行条件
3.运行状态
当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态,此时自动调用该线程对象的 run 方法
4.堵塞状态
一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时,将让出CPU并暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用 sleep,suspend,wait 方法,线程都将进入阻塞状态。阻塞时,只有当引起阻塞的原因被消除,才可以进入就绪状态
5.死亡状态
线程调用 stop 方法或者 run 方法执行结束后,线程便处于死亡状态,处于死亡状态的线程不具有继续运行的能力
五、参考
《Java多线程核心技术》
《Java基础教程》