转载

Java 多线程编程核心技术 (一):Java 多线程技能

本文授权转载自公众号薛勤的博客,主要介绍了并发基础概念与 API。

1、进程和线程

一个程序就是一个进程,而一个程序中的多个任务则被称为线程。

进程是表示资源分配的基本单位,线程是进程中执行运算的最小单位,亦是调度运行的基本单位。

举个例子:

打开你的计算机上的任务管理器,会显示出当前机器的所有进程,QQ,360 等,当 QQ 运行时,就有很多子任务在同时运行。比如,当你边打字发送表情,边好友视频时这些不同的功能都可以同时运行,其中每一项任务都可以理解成“线程”在工作。

2、使用多线程

在 Java 的 JDK 开发包中,已经自带了对多线程技术的支持,可以很方便地进行多线程编程。实现多线程编程的方式有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。使用继承 Thread 类创建线程,最大的局限就是不能多继承,所以为了支持多继承,完全可以实现 Runnable 接口的方式。需要说明的是,这两种方式在工作时的性质都是一样的,没有本质的区别。如下所示:

1. 继承 Thread 类

复制代码

publicclassMyThreadextendsThread{

@Override
public void run() {
//...
}

public static void main(String[] args) {
MyThreadthread =newMyThread();
thread.start();
}
}

2. 实现 Runnable 接口

复制代码

publicstaticvoidmain(String[] args)throwsInterruptedException{
newThread(newRunnable() {
@Override
publicvoidrun(){
//...
}
}).start();
}

Thread.java 类中的 start() 方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的 run() 方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,也就是使线程得到运行,多线程是异步的,线程在代码中启动的顺序不是线程被调用的顺序。

Thread 构造方法

  • Thread() :分配新的 Thread 对象。

  • Thread(Runnable target) :分配新的 Thread 对象。

  • Thread(Runnable target, String name) :分配新的 Thread 对象。

  • Thread(String name) :分配新的 Thread 对象。

  • Thread(ThreadGroup group, Runnable target) :分配新的 Thread 对象。

  • Thread(ThreadGroup group, Runnable target, String name) :分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,并作为 group 所引用的线程组的一员。

  • Thread(ThreadGroup group, Runnable target, String name, long stackSize) :分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,作为 group 所引用的线程组的一员,并具有指定的堆栈大小。

  • Thread(ThreadGroup group, String name) :分配新的 Thread 对象。

3、实例变量与线程安全

自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。当每个线程都有各自的实例变量时,就是变量不共享。共享数据的情况就是多个线程可以访问同一个变量。来看下面的示例:

复制代码

publicclassMyThreadimplementsRunnable{
privateintcount=5;

@Override
publicvoidrun() {
count--;
System.out.println(" 线程 "+Thread.currentThread().getName()+" 计算 count = "+count);
}
}

以上代码定义了一个线程类,实现 count 变量减一的效果。运行类 Runjava 代码如下:

复制代码

publicclassRuu{

publicstaticvoid main(String[] args) throws InterruptedException {
MyThread myThread =newMyThread();
Thread a =newThread(myThread,"A");
Thread b =newThread(myThread,"B");
Thread c =newThread(myThread,"C");
Thread d =newThread(myThread,"D");
Thread e =newThread(myThread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}

打印结果如下:

线程 C 计算 count = 3

线程 B 计算 count = 3

线程 A 计算 count = 2

线程 D 计算 count = 1

线程 E 计算 count = 0

线程 C,B 的打印结果都是 3,说明 C 和 B 同时对 count 进行了处理,产生了“非线程安全问题”。而我们想要的得到的打印结果却不是重复的,而是依次递减的。

在某些 JVM 中,i–的操作要分成如下 3 步:

  • 取得原有变量的值。

  • 计算 i-1。

  • 对 i 进行赋值。

在这三个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。

解决方法就是使用 synchronized 同步关键字 使各个线程排队执行 run() 方法。修改后的 run() 方法:

复制代码

publicclassMyThreadimplementsRunnable{
privateintcount=5;

@Override
synchronizedpublicvoidrun() {
count--;
System.out.println(" 线程 "+Thread.currentThread().getName()+" 计算 count = "+count);
}
}

打印结果:

线程 B 计算 count = 4

线程 C 计算 count = 3

线程 A 计算 count = 2

线程 E 计算 count = 1

线程 D 计算 count = 0

关于 System.out.println() 方法

先来看 System.out.println() 方法源码:

复制代码

publicvoidprintln(Stringx) {
synchronized(this) {
print(x);
newLine();
}
}

虽然 println() 方法内部使用 synchronized 关键字,但如下所示的代码在执行时还是有可能出现非线程安全问题的。

复制代码

System.out.println(" 线程 "+Thread.currentThread().getName()+" 计算 count = "+count--);

原因在于 println() 方法内部同步,但 i-- 操作却是在进入 println() 之前发生的,所以有发生非线程安全问题的概率。

4、多线程方法

1. currentThread() 方法

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

复制代码

Thread.currentThread().getName()

2. isAlive() 方法

方法 isAlive() 的功能是判断当前的线程是否处于活动状态。

复制代码

thread.isAlive();

3. sleep() 方法

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

复制代码

Thread.sleep()

4. getId() 方法

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

复制代码

thread.getId()

5、停止线程

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

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

1)使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程停止。

2)使用 stop() 方法强行终止线程,但是不推荐使用这个方法,因为该方法已经作废过期,使用后可能产生不可预料的结果。

3)使用 interrupt() 方法中断线程。

1. 暴力法停止线程

调用 stop() 方法时会抛出 java.lang.ThreadDeath 异常,但在通常的情况下,此异常不需要显示地捕捉。

复制代码

try{
myThread.stop();
}catch(ThreadDeath e) {
e.printStackTrace();
}

方法 stop() 已经被作废,因为如果强制让线程停止线程则有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的情况。示例如下:

复制代码

publicclass UserPass {
privateStringusername ="aa";
privateStringpassword ="AA";

publicStringgetUsername() {
returnusername;
}

publicvoidsetUsername(Stringusername) {
this.username = username;
}

publicStringgetPassword() {
returnpassword;
}

publicvoidsetPassword(Stringpassword) {
this.password = password;
}

synchronizedpublicvoidprintln(Stringusername,Stringpassword){
this.username = username;
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
this.password = password;
}

publicstaticvoidmain(String[] args) throws InterruptedException {
UserPass userPass =newUserPass();
Thread thread =newThread(newRunnable() {
@Override
publicvoidrun() {
userPass.println("bb","BB");
}
});
thread.start();
Thread.sleep(500);
thread.stop();
System.out.println(userPass.getUsername()+" "+userPass.getPassword());
}

}

运行结果:

bb AA

2. 异常法停止线程

使用 interrupt() 方法并不会真正的停止线程,调用 interrupt() 方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。

那我们如何判断该线程是否被打上了停止标记,Thread 类提供了两种方法。

复制代码

interrupted()测试当前线程是否已经中断。
isInterrupted()测试线程是否已经中断。

interrupted() 方法 不止可以判断当前线程是否已经中断,而且可以会清除该线程的中断状态。而对于 isInterrupted() 方法,只会判断当前线程是否已经中断,不会清除线程的中断状态。

仅靠上面的两个方法可以通过 while(!this.isInterrupted()){}对代码进行控制,但如果循环外还有其它语句,程序还是会继续运行的。这时可以抛出异常从而使线程彻底停止。示例如下:

复制代码

publicclassMyThreadextendsThread{
@Override
publicvoidrun(){
try{
for(inti=0; i<50000; i++){
if(this.isInterrupted()) {
System.out.println(" 已经是停止状态了!");
thrownewInterruptedException();
}
System.out.println(i);
}
System.out.println(" 不抛出异常,我会被执行的哦!");
}catch(Exception e) {
// e.printStackTrace();
}
}

publicstaticvoidmain(String[] args)throwsInterruptedException{
MyThread myThread =newMyThread();
myThread.start();
Thread.sleep(100);
myThread.interrupt();
}

}

打印结果:

2490

2491

2492

2493

已经是停止状态了!

注意

如果线程在 sleep() 状态下被停止,也就是线程对象的 run() 方法含有 sleep() 方法,在此期间又执行了 thread.interrupt() 方法,则会抛出 java.lang.InterruptedException: sleep interrupted 异常,提示休眠被中断。

3.return 法停止线程

return 法很简单,只需要把异常法中的抛出异常更改为 return 即可。代码如下:

复制代码

publicclassMyThreadextendsThread{
@Override
public void run() {

for(int i=0; i<50000; i++){
if(this.isInterrupted()) {
System.out.println(" 已经是停止状态了!");
return;// 替换此处
}
System.out.println(i);
}
System.out.println(" 不进行 return,我会被执行的哦!");

}
}

不过还是建议使用“抛异常”来实现线程的停止,因为在 catch 块中可以对异常的信息进行相关的处理,而且使用异常能更好、更方便的控制程序的运行流程,不至于代码中出现多个 return,造成污染。

6、暂停线程

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

这俩方法已经和 stop() 一样都被弃用了,因为如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。示例如下:

复制代码

publicclassMyThreadextendsThread{
privateInteger i =0;

@Override
publicvoidrun(){
while(true) {
i++;
System.out.println(i);
}
}

publicIntegergetI(){
returni;
}

publicstaticvoidmain(String[] args)throwsInterruptedException{
MyThread myThread =newMyThread();
myThread.start();
Thread.sleep(100);
myThread.suspend();
System.out.println("main end");
}

}

打印结果:

3398

3399

3400

3401

执行上段程序永远不会打印 main end。出现这样的原因是,当程序运行到 println() 方法内部停止时,PrintStream 对象同步锁未被释放。方法 println() 源代码如下:

复制代码

publicvoidprintln(Stringx) {
synchronized(this) {
print(x);
newLine();
}
}

这导致当前 PrintStream 对象的 println() 方法一直呈“暂停”状态,并且锁未被 myThread 线程释放,而主线程中的代码 System.out.println(“main end”) 还在傻傻的排队等待,导致迟迟不能运行打印。

使用 suspend() 和 resume() 方法也容易因为线程的暂停而导致数据不同步的情况,示例如下:

复制代码

publicclassUserPass2{
privateString username ="aa";
privateString password ="AA";

publicStringgetUsername(){
returnusername;
}


publicStringgetPassword(){
returnpassword;
}


publicvoidsetValue(String username, String password){
this.username = username;
if(Thread.currentThread().getName().equals("a")) {
Thread.currentThread().suspend();
}
this.password = password;
}

publicstaticvoidmain(String[] args)throwsInterruptedException{
UserPass2 userPass =newUserPass2();
newThread(newRunnable() {
@Override
publicvoidrun(){
userPass.setValue("bb","BB");
}
},"a").start();
newThread(newRunnable() {
@Override
publicvoidrun(){
System.out.println(userPass.getUsername()+" "+userPass.getPassword());
}
},"b").start();

}

}

打印结果:

bb AA

7、yield() 方法

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

复制代码

publicstaticvoidyield() 暂停当前正在执行的线程对象,并执行其他线程。

8、线程的优先级

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

设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。

设置线程优先级使用 setPriority() 方法,此方法的 JDK 源码如下:

复制代码

publicfinal void setPriority(intnewPriority) {
ThreadGroup g;
checkAccess();
if(newPriority> MAX_PRIORITY ||newPriority< MIN_PRIORITY) {
thrownewIllegalArgumentException();
}
if((g = getThreadGroup()) !=null) {
if(newPriority> g.getMaxPriority()) {
newPriority= g.getMaxPriority();
}
setPriority0(priority =newPriority);
}
}

在 Java 中,线程优先级划分为 1 ~ 10 这 10 个等级,如果小于 1 或大于 10,则 JDK 抛出异常。

从 JDK 定义的 3 个优先级常量可知,线程优先级默认为 5。

复制代码

publicfinalstaticintMIN_PRIORITY =1;

publicfinalstaticintNORM_PRIORITY =5;

publicfinalstaticintMAX_PRIORITY =10;

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

线程优先级具有规则性,线程的优先级与在代码中执行 start() 方法的顺序无关,与优先级大小有关。

线程优先级具有随机性,CPU 尽量使线程优先级较高的先执行完,但无法百分百肯定。也就是说,线程优先级较高的不一定比线程优先级较低的先执行。

9、守护线程

在 Java 中有两种线程,一种是用户线程,一种守护线程。

什么是守护线程?守护线程是一种特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有了存在的必要了,自动销毁。可以简单地说:任何一个守护线程都是非守护线程的保姆。

如何设置守护线程?通过 Thread.setDaemon(false) 设置为用户线程,通过 Thread.setDaemon(true) 设置为守护线程。如果不设置属性,默认为用户线程。

复制代码

thread.setDaemon(true);

示例如下:

复制代码

publicclassMyThreadextendsThread{
privateinti =0;
@Override
publicvoidrun(){
try{
while(true){
i++;
System.out.println("i="+i);
Thread.sleep(1000);
}
}catch(InterruptedException e) {
e.printStackTrace();
}
}

publicstaticvoidmain(String[] args)throwsInterruptedException{
MyThread thread =newMyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println(" 我离开后 thread 对象也就不再打印了 ");
}
}

打印结果:

i=1

i=2

i=3

i=4

i=5

我离开后 thread 对象也就不再打印了

原文  https://www.infoq.cn/article/Jtv2XL3a0HvRE2xwrNFs
正文到此结束
Loading...