作者:薛勤
来源:薛勤的博客
本文为《Java并发编程系列》第一章,主要介绍并发基础概念与API。
01 进程和线程
一个程序就是一个进程,而一个程序中的多个任务则被称为线程。
进程是表示资源分配的基本单位,线程是进程中执行运算的最小单位,亦是调度运行的基本单位。
举个例子:
打开你的计算机上的任务管理器,会显示出当前机器的所有进程,QQ,360等,当QQ运行时,就有很多子任务在同时运行。比如,当你边打字发送表情,边好友视频时这些不同的功能都可以同时运行,其中每一项任务都可以理解成“线程”在工作。
02 使用多线程
在Java的JDK开发包中,已经自带了对多线程技术的支持,可以很方便地进行多线程编程。实现多线程编程的方式有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。使用继承 Thread 类创建线程,最大的局限就是不能多继承,所以为了支持多继承,完全可以实现 Runnable 接口的方式。需要说明的是,这两种方式在工作时的性质都是一样的,没有本质的区别。如下所示:
1. 继承 Thread 类
public class MyThread extends Thread {
@Override
public void run() {
//...
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
2. 实现 Runnable 接口
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
//...
}
}).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 对象。 |
03 实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。当每个线程都有各自的实例变量时,就是变量不共享。共享数据的情况就是多个线程可以访问同一个变量。来看下面的示例:
public class MyThread implements Runnable {
private int count = 5;
@Override
public void run() {
count--;
System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count);
}
}
以上代码定义了一个线程类,实现count变量减一的效果。运行类Runjava代码如下:
public class Ruu {
public static void main(String[] args) throws InterruptedException {
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();
}
}
打印结果如下:
线程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()方法:
public class MyThread implements Runnable {
private int count = 5;
@Override
synchronized public void run() {
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()方法源码:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
虽然println()方法内部使用 synchronized 关键字,但如下所示的代码在执行时还是有可能出现非线程安全问题的。
System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count--);
原因在于println()方法内部同步,但 i-- 操作却是在进入 println()之前发生的,所以有发生非线程安全问题的概率。
04 多线程方法
1. currentThread()方法
currentThread()方法可返回代码段正在被哪个线程调用的信息。
Thread.currentThread().getName()
2. isAlive()方法
方法isAlive()的功能是判断当前的线程是否处于活动状态。
thread.isAlive();
3. sleep()方法
方法sleep()的作用是在指定的毫秒数内让当前"正在执行的线程"休眠(暂停执行)。这个"正在执行的线程"是指this.currentThread()返回的线程。
Thread.sleep()
4. getId()方法
getId()方法的作用是取得线程的唯一标识。
thread.getId()
05 停止线程
停止线程是在多线程开发时很重要的技术点。停止线程并不像break语句那样干脆,需要一些技巧性的处理。
在Java中有以下3种方法可以终止正在运行的线程:
1)使用退出标志,使线程正常退出,也就是当run()方法完成后线程停止。
2)使用stop()方法强行终止线程,但是不推荐使用这个方法,因为该方法已经作废过期,使用后可能产生不可预料的结果。
3)使用interrupt()方法中断线程。
1.暴力法停止线程
调用stop()方法时会抛出 java.lang.ThreadDeath 异常,但在通常的情况下,此异常不需要显示地捕捉。
try {
myThread.stop();
} catch (ThreadDeath e) {
e.printStackTrace();
}
方法stop()已经被作废,因为如果强制让线程停止线程则有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的情况。示例如下:
public class UserPass {
private String username = "aa";
private String password = "AA";
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
synchronized public void println(String username, String password){
this.username = username;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
}
public static void main(String[] args) throws InterruptedException {
UserPass userPass = new UserPass();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
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()){}对代码进行控制,但如果循环外还有其它语句,程序还是会继续运行的。这时可以抛出异常从而使线程彻底停止。示例如下:
public class MyThread extends Thread {
@Override
public void run() {
try {
for (int i=0; i<50000; i++){
if (this.isInterrupted()) {
System.out.println("已经是停止状态了!");
throw new InterruptedException();
}
System.out.println(i);
}
System.out.println("不抛出异常,我会被执行的哦!");
} catch (Exception e) {
// e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
MyThread myThread =new MyThread();
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即可。代码如下:
public class MyThread extends Thread {
@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,造成污染。
06 暂停线程
暂停线程意味着此线程还可以恢复运行。在Java多线程中,可以使用 suspend() 方法暂停线程,使用 resume()方法恢复线程的执行。
这俩方法已经和stop()一样都被弃用了,因为如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。示例如下:
public class MyThread extends Thread {
private Integer i = 0;
@Override
public void run() {
while (true) {
i++;
System.out.println(i);
}
}
public Integer getI() {
return i;
}
public static void main(String[] args) throws InterruptedException {
MyThread myThread =new MyThread();
myThread.start();
Thread.sleep(100);
myThread.suspend();
System.out.println("main end");
}
}
打印结果:
...
3398
3399
3400
3401
执行上段程序永远不会打印main end。出现这样的原因是,当程序运行到 println() 方法内部停止时,PrintStream对象同步锁未被释放。方法 println() 源代码如下:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
这导致当前PrintStream对象的println() 方法一直呈“暂停”状态,并且锁未被myThread线程释放,而主线程中的代码System.out.println("main end") 还在傻傻的排队等待,导致迟迟不能运行打印。
使用 suspend() 和 resume() 方法也容易因为线程的暂停而导致数据不同步的情况,示例如下:
public class UserPass2 {
private String username = "aa";
private String password = "AA";
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setValue(String username, String password){
this.username = username;
if (Thread.currentThread().getName().equals("a")) {
Thread.currentThread().suspend();
}
this.password = password;
}
public static void main(String[] args) throws InterruptedException {
UserPass2 userPass = new UserPass2();
new Thread(new Runnable() {
@Override
public void run() {
userPass.setValue("bb","BB");
}
},"a").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(userPass.getUsername()+" "+userPass.getPassword());
}
},"b").start();
}
}
打印结果:
bb AA
07 yield()方法
yield() 方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
08 线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程优先级使用setPriority()方法,此方法的JDK源码如下:
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
在Java中,线程优先级划分为1 ~ 10 这10个等级,如果小于1或大于10,则JDK抛出异常。
从JDK定义的3个优先级常量可知,线程优先级默认为5。
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
线程优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
线程优先级具有规则性,线程的优先级与在代码中执行start()方法的顺序无关,与优先级大小有关。
线程优先级具有随机性,CPU尽量使线程优先级较高的先执行完,但无法百分百肯定。也就是说,线程优先级较高的不一定比线程优先级较低的先执行。
09 守护线程
在Java中有两种线程,一种是用户线程,一种守护线程。
什么是守护线程?守护线程是一种特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有了存在的必要了,自动销毁。可以简单地说:任何一个守护线程都是非守护线程的保姆。
如何设置守护线程?通过Thread.setDaemon(false)设置为用户线程,通过Thread.setDaemon(true)设置为守护线程。如果不设置属性,默认为用户线程。
thread.setDaemon(true);
示例如下:
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) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("我离开后thread对象也就不再打印了");
}
}
打印结果:
i=1
i=2
i=3
i=4
i=5
我离开后thread对象也就不再打印了
参考与总结
《Java多线程编程核心技术》高洪岩 著
本文主要介绍了Thread类的API,算是为学习多线程更深层次知识打下一些基础,文章若有错误请在评论区指正。
推荐阅读
《Java多线程编程核心技术(第2版)》
推荐语:
技术畅销书全新升级,首版好评如潮,畅销3万余册。
案例式讲解,逐一分析和验证每个技术点,轻松学习、不枯燥。
精讲核心库和方法,精通多线程“武器库”,神清气爽,“自主可控”。
突破原子性、可视性、线程间通信、锁难点,解决高并发业务瓶颈。
你与世界
只差一个
公众号
点击“阅读原文”了解更多好书!