文章目录
进程与线程
通过查看“Windows任务管理器”中的列表,完全可以将运行在内存中的exe文件理解成进程,进程是受操作系统管理的基本运行单元。线程可以理解成是在进程中独立运行的子任务。
使用多线程技术后,可以在同一时间运行更多不同种类的任务。
多线程是异步的,所以千万不要把 Eclipse里代码的顺序当成线程执行的顺序,线程调用的时机是随机的。执行start()方法的顺序不代表线程启动的顺序。
使用多线程
一个进程在运行时候至少有一个线程,比如Java中的main函数。多线程的创建:
- 继承Thread类
- 实现Runnable接口
- JDK 5.0新增:实现Callable接口
继承自Thread类
- 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法。
- 创建MyThread类的对象。
- 调用线程对象的start()方法启动线程(启动后还是执行run方法的)。
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
package multiply.com.test;
public class MyThread extends Thread{
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
}
package multiply.com.test;
public class Run {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("运行结束!");
}
}
运行结束!
MyThread
为什么不直接调用了run方法,而是调用start启动线程。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
把主线程任务放在子线程之前了:这样主线程一直是先跑完的,相当于是一个单线程的效果了。
package multiply.com.test;
public class MyThread extends Thread{
private int i;
public MyThread(int i)
{
super();
this.i = i;
}
@Override
public void run() {
super.run();
System.out.println(i);
}
}
package multiply.com.test;
public class Run {
public static void main(String[] args) {
MyThread myThread1 = new MyThread(1);
MyThread myThread2 = new MyThread(2);
MyThread myThread3 = new MyThread(3);
MyThread myThread4 = new MyThread(4);
MyThread myThread5 = new MyThread(5);
myThread1.start();
myThread2.start();
myThread3.start();
myThread4.start();
myThread5.start();
}
}
1
2
5
4
3
实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
- 创建MyRunnable任务对象。
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程。
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
package multiply.com.test;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("运行中");
}
}
package multiply.com.test;
public class Run {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("运行结束");
}
}
运行结束
运行中
构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。
实现Runnable接口(匿名内部类形式):
- 可以创建Runnable的匿名内部类对象。
- 交给Thread处理。
- 调用线程对象的start()启动线程。
package com.test;
public class ThreadDemo2Other {
public static void main(String[] args) {
//创建Runnable的匿名内部类对象
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程1执行输出:" + i);
}
}
};
Thread t = new Thread(target);
t.start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程2执行输出:" + i);
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("子线程3执行输出:" + i);
}
}).start();
for (int i = 0; i < 3; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
多线程的实现方案三:利用Callable、FutureTask接口实现。
- 得到任务对象:定义类实现Callable接口,重写call方法,封装要做的事情;用FutureTask把Callable对象封装成线程任务对象。
- 线程任务对象交给Thread处理。
- 调用Thread的start方法启动线程,执行任务
- 程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
package com.test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call = new MyCallable(100);
// 4、把Callable任务对象 交给 FutureTask 对象
// FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
// FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
FutureTask<String> f1 = new FutureTask<>(call);
// 5、交给线程处理
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 1、定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
// 2、重写call方法(任务方法)
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
实例变量与线程安全
不共享数据的情况:一共创建了3个线程,每个线程都有各自的 count变量。
package multiply.com.test;
public class MyThread extends Thread {
private int count = 3;
public MyThread(String s)
{
super();
this.setName(s);//设置当前线程的名字
}
@Override
public void run() {
super.run();
while(count>0)
{
count--;
System.out.println("由" + Thread.currentThread().getName() + "计算,count=" + count);
}
}
}
package multiply.com.test;
public class Run {
public static void main(String[] args) {
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}
由C计算,count=2
由C计算,count=1
由A计算,count=2
由B计算,count=2
由A计算,count=1
由C计算,count=0
由A计算,count=0
由B计算,count=1
由B计算,count=0
共享数据的情况:同一个对象,不同的名称。
package multiply.com.test;
public class MyThread extends Thread {
private int count = 5;
@Override
public void run() {
super.run();
count--;
System.out.println("由" + Thread.currentThread().getName() + "计算,count=" + count);
}
}
package multiply.com.test;
public class Run {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread a = new Thread(myThread, "A");
Thread b = new Thread(myThread, "B");
Thread c = new Thread(myThread, "C");
Thread d = new Thread(myThread, "D");
Thread e = new Thread(myThread, "E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
由A计算,count=3
由D计算,count=0
由E计算,count=0
由B计算,count=2
由C计算,count=2
多个线程同时访问,一定会出现非线程安全问题,这里就要用到线程同步,让减一操作连续。
package multiply.com.test;
public class MyThread extends Thread {
private int count = 5;
@Override
synchronized public void run() {
super.run();
count--;
System.out.println("由" + Thread.currentThread().getName() + "计算,count=" + count);
}
}
由A计算,count=4
由D计算,count=3
由C计算,count=2
由E计算,count=1
由B计算,count=0
synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
留意i–与System.out.println()的异常
package multiply.com.test;
public class MyThread extends Thread {
private int count = 5;
@Override
synchronized public void run() {
super.run();
System.out.println("由" + Thread.currentThread().getName() + "计算,count=" + count--);
}
}
本实验的测试目的是:虽然println()方法在内部是同步的,但i–的操作却是在进入println()之前发生的,所以有发生非线程安全问题的概率。
currentThread()方法
package multiply.com.test;
public class MyThread extends Thread {
public MyThread()
{
System.out.println("构造方法的打印:"+Thread.currentThread().getName());
System.out.println("this.getName():"+this.getName());
}
@Override
public void run() {
System.out.println("run方法的打印:"+Thread.currentThread().getName());
System.out.println("this.getName():"+this.getName());
}
}
package multiply.com.test;
public class Run {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//myThread.start();
myThread.run();
}
}
构造方法的打印:main
this.getName():Thread-0
run方法的打印:main
this.getName():Thread-0
package multiply.com.test;
public class Run {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
//myThread.run();
}
}
构造方法的打印:main
this.getName():Thread-0
run方法的打印:Thread-0
this.getName():Thread-0
MyThread.java类的构造函数是破main线程调用的,而run方法是被名称为Thread-0的线程调用的,run方法是自动调用的方法。