Java多线程编程核心技术-1-多线程技术基础

使用多线程

Java的JDK实现多线程编程主要有两种方式:一种是继承Thread类,另一种是实现Runnable接口。

继承Thread类

直接继承Thread类,然后重写其run方法就可以得到一个新的线程类。

1
2
3
4
5
6
7
8
public class MyThread extends Thread{

@override
public void run(){
super.run();
System.out.println("MyThread");
}
}

主方法中启动线程:

1
2
3
4
5
6
7
public class Main{
public static void main(String[] args){
MyThread mythread = new MyThread();
mythread.start();
System.out.println("main");
}
}

结果:

1
2
main
MyThread

由于新线程的启动需要更多时间,所以main被先打印出来。

实现Runnable接口

由于Java不支持多继承,使用继承就会造成问题,如果本来还需要继承其他的父类,那么为了实现多线程来占用一个继承就得不尝试了,因此使用Runnable接口更加合理。实际上Thread也是实现了Runable接口的类。

1
public class Thread implements Runnable

创建新的线程类

同样的,实现该接口后重写其中的run方法,即可实现一个线程。

1
2
3
4
5
6
7
8
package myrunnable;

public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable运行中!");
}
}

运行该线程

1
2
3
4
5
6
7
8
public class Run {
public static void main(String[] args) {
Runnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
System.out.println("运行结束!");
}
}

实例变量共享造成的非线程安全问题与解决方案

每个线程类可以有多个实体对象。

  • 每个实体对象都可以单独调用start方法来运行,
  • 或者通过Thread类来构造多个新的线程对象。

此时前一种情况,每个实体对象的属性是独立的,即每个线程都拥有一份独立的属性。

总结规律就是:只有在创建不同线程的线程对象是同一个的时候,其中的成员变量才会被共享。

例如:

独立变量

线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread extends Thread {

private int count=5;

@Override
public void run() {
super.run();
count--;
//此示例不要用while语句,会造成其他线程得不到运行的机会
//因为第一个执行while语句的线程会将count值减到0
//一直由一个线程进行减法运算
System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
}
}

执行类

1
2
3
4
5
6
7
8
9
10
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();
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
由A计算,count=4
由B计算,count=4
由c计算,count=4
由B计算,count=3
由A计算,count=3
由A计算,count=2
由A计算,count=1
由A计算,count=O
由B计算,count=2
由c计算,count=3
由B计算,count=1
由c计算,count=2
由B计算,count=O
由c计算,count=1
由c计算,count=o

由于每个线程独立享有属性变量,在这里即为count属性,所以每个线程都会独立操作对应count,从头5减到0。

共享变量

线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread extends Thread {

private int count=5;

@Override
public void run() {
super.run();
count--;
//此示例不要用while语句,会造成其他线程得不到运行的机会
//因为第一个执行while语句的线程会将count值减到0
//一直由一个线程进行减法运算
System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
}
}

执行类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}
}

结果:

1
2
3
4
5
由A计算,count=3
由B计算,count=3
由c计算,count=2
由D计算,count=1
由E计算,count=0

此时由于这5个线程是由一个实体创建而来,所以其变量是私有的,所以每个线程都是访问的同一个变量。这样大概率会出现线程安全问题,比如上面的A, B。共享变量的值都为3,说明A, B同时对count进行了处理。

出现这个问题的原因主要是在JVM中,count++会被分成三步:

  • 取得原有的count值,
  • 计算count-1,
  • 对count进行赋值。

其解决方法由几种,后面文章会详细介绍,首先这里可以使用synchronized关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread extends Thread {

private int count=5;

@Override
synchronized public void run() {
super.run();
count--;
//此示例不要用while语句,会造成其他线程得不到运行的机会
//因为第一个执行while语句的线程会将count值减到0
//一直由一个线程进行减法运算
System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
}
}

简单来说就是:被synchronized关键字标记的方法,不同线程会同步进入,所以不会出现线程安全问题。

这里要理解的应该是共享变量的概念,而不是如何解决同步问题,这里也是多线程的核心问题之一。

start方法与run方法

start方法

start方法并不保证其会按照启动顺序来执行,会由JVM来为其创建新的线程,因为其会启动新的线程,不是一个立即执行操作,而不同的线程启动时间可能随系统的状态不同而改变,因此后面的线程是有可能比前面的线程更先执行。

run方法

run方法一般不直接调用,因为如果直接调用,就会在当前线程直接执行。而并不是创建新线程,然后执行。

相关方法

currentThread()方法

currentThread()方法可返回代码段正在被哪个线程调用。

例如

1
2
3
4
5
public class Run1 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}

结果

1
main

在新建的线程中,可以使用this来替代Thread,因为其本身就是Runnable

如:

1
2
3
4
5
6
7
8
9
10
11
public class CountOperate extends Thread {
@Override
public void run() {
System.out.println("run---begin");
System.out.println("Thread.currentThread().getName()="
+ Thread.currentThread().getName());
System.out.println("this.getName()=" + this.getName());
System.out.println("run---end");
}

}

isAlive()方法

isAlive()方法的功能是判断当前的线程是否存活。

该方法同样是Thread类的静态方法。

例如:

非线程类中

1
2
3
4
5
public class Run1 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}

线程类中

1
2
3
4
5
6
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("run=" + this.isAlive());
}
}

这里的活动状态是指处于正在运行或准备开始运行的状态。

sleep()方法

sleep(long millis)方法

sleep()方法的作用是在指定的时间(毫秒)内让当前“正在执行的线程”休眠(暂停执行),这个“正在执行的线程”是指this.currentThread()返回的线程。

例如:

线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyThread2 extends Thread {
@Override
public void run() {
try {
System.out.println("run threadName="
+ this.currentThread().getName() + " begin ="
+ System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("run threadName="
+ this.currentThread().getName() + " end ="
+ System.currentTimeMillis());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

执行类

1
2
3
4
5
6
7
8
public class Run2 {
public static void main(String[] args) {
MyThread2 mythread = new MyThread2();
System.out.println("begin =" + System.currentTimeMillis());
mythread.start();
System.out.println("end =" + System.currentTimeMillis());
}
}

结果:

1
2
3
4
5
6
begin =1396255271828
end=1396255271828
run threacNarne=Thread-0
begin =1396255271828
run threadName=Thread-O
end =1396255273828

值得注意的是sleep方法并不会释放其持有的锁。

sleep(long millis, int nanos)方法

sleep(long millis,int nanos)方法的作用是在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序的精度和准确性的影响。

StackTraceElement[] getStackTrace()方法

StackTraceElement[]getStackTrace()方法的作用是返回一个表示该线程堆栈跟踪元素数组。

注意该方法是Thread.currentThread()的方法。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 eachElement = array[i];
System.out.println("className=" + eachElement.getClassName() + " methodName=" + eachElement.getMethodName() + " fileName=" + eachElement.getFileName() + " lineNumber="+ eachElement.getLineNumber());
}
}

}

public static void main(String[] args) {
Test1 test1 = new Test1();
test1.a();
}
}

结果

1
2
3
4
5
6
7
className=java.lang.Thread methodName=getStackTrace fileName=Thread.java lineNumber=1559
className=test1.Test1 methodName=e fileName=Test1.java lineNumber=22
className=test1.Test1 methodName=d fileName=Test1.java lineNumber=18
className=test1.Test1 methodName=c fileName=Test1.java lineNumber=14
className=test1.Test1 methodName=b fileName=Test1.java lineNumber=10
className=test1.Test1 methodName=a fileName=Test1.java lineNumber=6
className=test1.Test1 methodName=main fileName=Test1.java lineNumber=36

dumpStack()方法

static void dumpStack()方法的作用是将当前线程的堆栈跟踪信息输出至标准错误流

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Test1 {

public void a() {
b();
}

public void b() {
c();
}

public void c() {
d();
}

public void d() {
e();
}

public void e() {
int age = 0;
age = 100;
if (age == 100) {
Thread.dumpStack();
}
}

public static void main(String[] args) {
Test1 test1 = new Test1();
test1.a();
}
}

结果

1
2
3
4
java.lang.Exception: Stack trace
at java.lang.Thread.dumpStack(Thread.java :1336)at test6.Test1.e(Test1.java: 25)
at test6.Test1.d(Test1.java: 18)at test6.Test1.c(Test1.java: 14)at test6.Test1.b(Test1.java:10)at test6.Test1.a(Test1.java:6)
at test6.Test1.main(Test1.java: 31)

getId方法

getId()方法用于取得线程的唯一标识。

例如

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
Thread runThread = Thread.currentThread();
System.out.println(runThread.getName() + " " + runThread.getId());
}
}

结果

1
main 1

从运行结果来看,当前执行代码的线程名称为main,线程id值为1。

停止线程

停止线程是多线程开发的一个很重要的技术点,停止线程在Java语言中并不像break语句那样干脆,还需要一些技巧性的处理。

在Java中有3种方法可以使正在运行的线程终止运行:

  • 使用退出标志使线程正常退出。
  • 使用stop()方法强行终止线程,但是这个方法不推荐使用,因为stop()和suspend()、resume()一样,都是作废过期的方法,使用它们可能发生不可预料的结果。
  • 使用interrupt()方法中断线程。

interrupt()

interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt()方法仅仅是在当前线程中做了一个停止的标记,并不是真正停止线程。

interrupt()并不会停止

线程类:

1
2
3
4
5
6
7
8
9
public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 5; i++) {
System.out.println("i=" + (i + 1));
}
}
}

执行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package test;

import exthread.MyThread;

public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
System.out.println("zzzzzzzz");
}
}

结果:

1
2
3
4
5
i=0
i=1
i=2
i=3
i=4

可以看到线程并没有停止。

实际上我们需要自己判断interrupt状态,然后自己来推出线程。

使用interrupt(),interrupted(),this.isInterrupted()来停止线程

  • interrupted():测试currentThread()是否已经中断。执行后具有清除状态标志值为false的功能。
  • this.isInterrupted():测试this关键字所在类的对象是否已经中断。不清除状态标志。

示例

线程类

1
2
3
4
5
6
7
8
9
10
11
12
public class MyThread extends Thread {
@Override
public void run() {
super.run();
int i = 0;
while(!this.isInterrupted()){
i++;
System.out.println("i=" + i);
}
System.out.println("结束!线程中断!");
}
}

执行类

1
2
3
4
5
6
7
8
9
10
11
12
13
package test;

import exthread.MyThread;

public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.out.println("zzzzzzzz");
}
}

结果:

1
2
3
4
5
6
i = 82345
i = 82345
i = 82345
i = 82345
zzzzzzzz
结束!线程中断!

这里我们每一次都检测interrupt状态,如果中断了就不进行循环了。

异常法

上面我们是直接循环判断,出错就跳出循环。而如果使用异常,则可以直接跳出该线程。

例如

线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了!我要退出了!");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
System.out.println("我在for下面");
} catch (InterruptedException e) {
System.out.println("进MyThread.java类run方法中的catch了!");
e.printStackTrace();
}
}
}

运行类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import exthread.MyThread;

public class Run {

public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
}

}

结果:

1
2
3
4
5
6
7
8
9
10
11
i=183973i=183974
i=183975
i=183976
i=183977
i=183978
己经是停止状态了!我要退出了!
end !
进MyThread.java类run方法中的catch了!
java. lang . InterruptedException
at exthread. MyThread.run ( MyThread.java:11)

这里就是检测到外部传入中断信息就直接抛出错误。不再运行下面的程序。

在sleep中使用interrupt()

首先明确:在sleep中使用interrupt()会直接报出InterruptedException错误。

例如:

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {
for(int i=0;i<100000;i++){
System.out.println("i="+(i+1));
}
System.out.println("run begin");
Thread.sleep(200000);
System.out.println("run end");
} catch (InterruptedException e) {
System.out.println("先停止,再遇到了sleep!进入catch!");
e.printStackTrace();
}
}
}

运行类:

1
2
3
4
5
6
7
8
public class Run {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
System.out.println("end!");
}
}

结果:

1
2
3
4
end!
i=1
i=2
i=3

stop强制停止线程

使用stop()方法可以强行停止线程,即暴力停止线程。并且会释放该线程所持有的所有锁。并且会抛出java.lang.ThreadDeath

例如:

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyThread extends Thread {
private int i = 0;

@Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import testpackage.MyThread;

public class Run {

public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(4000);
thread.stop();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

结果:

1
2
3
4
i=1
i=2
i=3
i=4

由运行结果可以看出,线程被暴力停止了

注意:stop()方法已经是作废的方法,因为如果暴力性地强制让线程停止,则一些清理性的工作可能得不到完成,或者数据添加不完整。

例如,由于直接杀死线程并且释放锁,但是如果被锁的变量正被操作了一半,此时释放锁,其他线程得到的就是一个不完整的脏数据。所以该方法会被废除。

暂停线程

暂停线程意味着此线程还可以恢复运行,在Java多线程中,可以使用suspend()方法暂停线程,使用resume()方法来恢复线程的执行。

不过这两个方法实际上也是被废除了。所以一般也并不这么用。

suspend()方法与resume()方法

使用

调用suspend()方法会离级停止该线程。但不会释放所持有的锁。

示例

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread extends Thread {

@Override
public void run() {
int i = 0;
while (true) {
i++;
System.out.println(i);
}
}

}

执行类:

1
2
3
4
5
6
7
8
9
10
11
12
import mythread.MyThread;
public class Run {

public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.suspend();
Thread.sleep(5000);
thread.resume();
}
}

结果:执行后会立即执行,然后在主线程sleep的2s内,thread会一直i++。然后在2s后停止,随后主线程sleep5s,即thread暂停5s,接下来线程继续运行。

缺点

独占

如果suspend()方法与resume()方法使用不当,极易造成公共同步对象被独占,其他线程无法访问公共同步对象的结果。

例如:线程A持有了Lock1锁,线程B在等待Lock1,但线程A被意外永久suspend了。那么线程B就陷入了死锁。

数据不完整

在使用suspend()方法与resume()方法时也容易出现线程暂停,进而导致数据不完整的情况。

这种立即停止,不考虑线程的状态,一般都会面临该问题。比如赋值操作进行到一半,一部分变量被赋值了,另一部分没有被赋值。那取出的数据就会出现不统一的情况。

yield()方法

yield()方法的作用是放弃当前的CPU资源,让其他任务去占用CPU执行时间,放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

注意:该方法应该直接通过Thread静态调用,而不能通过this来调用。

线程优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务,其实就是让高优先级的线程获得更多的CPU时间片。

在jdk中使用3个常量来预置定义优先级的值。分别是:

1
2
3
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

优先级的继承特征

在Java中,线程的优先级具有继承性,例如,A线程启动B线程,则B线程的优先级与A线程是一样的。

优先级的规律性

高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。

优先级的随机性

优先级高的线程往往优先执行完,但这个结果不是绝对的,因为线程的优先级还具有“随机性”,即优先级较高的线程不一定每一次都先执行完。

守护线程

Java中有两种线程:

  • 用户线程,也称非守护线程;
  • 另一种是守护线程。

守护线程是一种特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。

典型的守护线程是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。

创建一个守护线程的方法是使用thread1.setDaemon(),调用这个方法的线程就是thread1的守护线程。

例如:在main方法中执行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("我离开thread对象也不再打印了,也就是停止了!");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

则main方法主线程即为thread的守护线程。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :