Java的JDK开发包已经自带了对多线程技术的支持,可以方便地进行多线程编程。实现多线程编程的方式主要有两种:一种是继承Thread类,一种是实现Runnable接口。
在学习如何创建新的线程前,先来看看Thread类的声明结构,代码如下:
public class Thread implements Runnable
从上面的源码可以发现,Thread类实现了Runnable接口,它们之间具有多态关系。其实使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为Java语言的特点就是单继承,所以为了支持多继承,可以通过实现Runnable的方式。
【示例1】创建自定义的线程类
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("my thread");
}
}
在main()方法中,执行start方法。
public class RunMyThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("运行结束!");
}
}
上面的代码使用start()方法来启动一个线程。线程启动后会自动调用线程对象中的run()方法,run()方法里面的代码就是线程对象要执行的任务,是线程执行任务的入口。从上面的运行结果来看,run()方法的执行时间晚于main()方法的执行时间,因为start()方法比较耗时,也增加了先输出main()方法的概率。
start()耗时多的原因是内部执行了多个步骤:
1、通过JVM告诉操作系统创建Thread.
2、操作系统开辟内存并使用Windows SDK中的createThread()函数创建Thread线程对象。
3、操作系统对Thread对象进行调度,已确定执行时机。
4、Thread在操作系统中被执行成功。
使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序无关。另外,线程是一个子任务,CPU是以不确定的方式或者随机的时间来调用线程的run方法。
【示例】创建自定义线程类:
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 10000; i++) {
System.out.println("run = " + Thread.currentThread().getName());
}
}
}
public class RunMyThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("myThread");
myThread.start();
for (int i = 0; i < 10000; i++) {
System.out.println("main = " + Thread.currentThread().getName());
}
System.out.println("运行结束!");
}
}
多线程随机输出的原因是CPU将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替执行并输出,导致输出结果成乱序。
时间片就是CPU分配给各个线程的时间。每个线程被分配一个时间片,在当前的时间片内执行线程的任务。需要注意的是,当CPU在不同的线程上进行切换时是需要耗时的,所以并不是创建的线程越多,执行的效率就越快。想法,线程数过多反而会降低代码的执行效率。
如果调用“run()”,而不是调用“start()” ,就不是异步执行,而是同步执行,那么此线程对象并不交给线程规划器来处理,而是由main线程来调用run方法,也就是必须等run方法执行完才可以执行后面的代码。
【示例】创建一个实现Runnable接口的MyRunnable类:
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("运行中");
}
}
public class RunRunnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println("运行结束");
}
}
使用继承Thread类的方式来开发多线程是有局限性的,因为Java是单继承,不支持多继承,为了改变这种限制,可以使用实现Runnable接口的方式来实现多线程。
使用Runnable接口方式实现多线程可以把”线程“和”任务“分离,Thread代表线程,Runnable代表可以运行的任务。
自定义线程类中的实例变量针对其他线程有共享和不共享之分,这在多个线程之间交互时是很重要的。
1、不共享数据的情况
【示例1.5.1】
public class MyThread1 extends Thread {
private int count = 5;
public MyThread1(String name){
super();
this.setName(name);
}
@Override
public void run(){
super.run();
while(count > 0){
count --;
System.out.println("由" + Thread.currentThread().getName() + "计算 count = " + count );
}
}
}
public class MyThreadMain {
public static void main(String[] args) {
MyThread1 thread1 = new MyThread1("a");
MyThread1 thread2 = new MyThread1("b");
MyThread1 thread3 = new MyThread1("c");
thread1.start();
thread2.start();
thread3.start();
}
}
分析:一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值,这样的情况就是变量不共享。
2、共享数据的情况
共享数据的情况就是多个线程可以访问一个变量。
【示例1.5.2】
public class MyThread2 extends Thread{
private int count = 5;
@Override
public void run(){
super.run();
count --;
System.out.println("由" + Thread.currentThread().getName() + "计算 count = " + count );
}
}
public class MyThread2Main {
public static void main(String[] args) {
MyThread2 mythread = new MyThread2();
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 B C输出的值都是2,说明A B C同时对count进行处理,产生了分线程安全问题。
出现非线程安全的情况是因为某些jvm中,count--的操作要分解成如下三步(执行这三个步骤的过程中会被其他线程所打断)
1、取得原来count的值。
2、计算count - 1
3、对count 进行重新赋值。
在这三个步骤中,如果有多个线程同时访问,那么很大概率会出现非线程安全问题。
针对上面出现线程不安全的情况下,在run方法前加上synchronized关键词,就不会出现非线程安全的情况了。
public class MyThread2 extends Thread{
private int count = 5;
@Override
synchronized public void run(){
super.run();
count --;
System.out.println("由" + Thread.currentThread().getName() + "计算 count = " + count );
}
}
分析:在run方法的前面假如synchronized关键字,使多个线程在执行run方法时,一排队的方式进行处理。一个线程在调用run方法前,需要先判断run方法有没有上锁,如果上锁,说明有其他线程正在执行run方法,必须等其他线程执行结束后才可以执行run方法,这样也就实现了排队调用run方法的目的,实现了顺序对count变量-1的效果。虽然count -- 仍然有3个步骤,但在执行这三个步骤时并没有被打断,呈”原子性“,所以运行结果是正确的。
使用synchronized关键字修饰的方法称为”同步方法“,可用来对方法内部的全部代码执行加锁,而加锁的这段代码称为”互斥区“或”临界区“。
当一个线程想要执行同步方法里的代码时,它会先尝试拿这把锁,如果能够拿到,那么该线程就会执行synchronized里面的代码,如果拿不到,那么这个线程就会不断尝试去拿这把锁,直至拿到为止。
currentThread()可以返回代码段正在被哪个线程调用。
判断线程对象是否存活。
public class MyThread1 extends Thread{
@Override
public void run(){
System.out.println("run = " + this.isAlive());
}
}
public class MyThread1Main {
public static void main(String[] args) {
MyThread1 t = new MyThread1();
System.out.println("begin = " + t.isAlive());
t.start();
System.out.println("end = " + t.isAlive());
}
}
isAlive()的作用是测试线程是否处于活动状态。那么什么事活动状态呢?即线程已经启动尚未终止。如果线程处于正在运行或准备开始运行的状态,就认为线程是”存活“的。需要说明一下,对于代码:
System.out.println("end = " + t.isAlive());
虽然输出的是true,但此值是不确定的。输出true值时因为t线程还未执行完毕。
public StackTraceElement[] getStackTrace()方法的作用是返回一个表示该线程的堆栈跟踪元素数组。如果该线程尚未启动或已经终止,则该方法将返回一个零长度数组。如果返回的数组长度不为零,则第一个元素代表堆栈顶,它是该数组中最新的方法调用。最后一个元素代表堆栈底,是该数组中最旧的方法调用。
【示例2.2】
public class Test1 {
public void a(){
b();
}
public void b(){
c();
}
public void c(){
d();
}
public void d(){
e();
}
public void e(){
StackTraceElement[] array = Thread.currentThread().getStackTrace();
if(array != null){
for (int i = 0; i < array.length; i++) {
StackTraceElement stackTraceElement = array[i];
System.out.println("className = " + stackTraceElement.getClassName() +
"methodName = " + stackTraceElement.getMethodName() +
"fileName = " + stackTraceElement.getFileName() +
"lineNumber = " + stackTraceElement.getLineNumber());
}
}
}
public static void main(String[] args) {
Test1 test1 = new Test1();
test1.a();
}
}
停止线程多线程开发的一个重要的技术点。在大多数情况下,使用Thread.interrupt()方法停止一个线程。
~未完待续,感谢阅读