线程的简介
几乎每种操作系统都支持进程的概念。进程就是在某种程度上相互隔离的、独立运行的程序。
线程化是允许多个活动共存于一个进程中的工具。大多数现代的操作系统都支持线程,而且线程的概念以各种形式已存在了好多年。Java 是第一个在语言本身中显式地包含线程的主流编程语言,它没有把线程化看作是底层操作系统的工具。
有时候,线程也称作轻量级进程。就象进程一样,线程在程序中是独立的、并发的执行路径,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态。
进程可以支持多个线程,它们看似同时执行,但互相之间并不同步。一个进程中的多个线程共享相同的内存地址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。尽管这让线程之间共享信息变得更容易,但您必须小心,确保它们不会妨碍同一进程里的其它线程。
Java 线程工具和 API 看似简单。但是,编写有效使用线程的复杂程序并不十分容易。因为有多个线程共存在相同的内存空间中并共享相同的变量,所以必须小心,确保线程不会互相干扰。
线程的建立
在java中线程的建立有两种方式:一种时通过继承Thread类,一种是实现Runnable接口。下面就这两种方式的展开讨论。
继承Thread类建立线程
创建一个自定义的线程类MyThread,继承自Thread,重写run方法。在run()方法中添加要执行的代码,代码如下
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("this is a Thread ,MyThread");
}
}
运行类代码如下;
public class t1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread myThread = new MyThread();
myThread.start();
System.out.println("run over!~");
}
}
运行结果如图:
图2-1 运行结果
这是线程常见的一种写法,通过继承Thread类的方法。从本次的运行结果来看 在使用多线程技术时,代码的运行结果与代码的执行顺序或者代用顺序是无关的。补充一点,如果多次调用start()方法,就会出现Exception in thread "main" java.lang.IllegalThreadStateException的异常
实现Runnable接口创建线程
如果想要创建的线程类已经有一个父类了,这个时候就不能通过继承Thread类了,因为java不支持多继承,在这个时候就需要通过Runnable接口来创建线程类。
创建一个线程类MyRunnable,实现接口Runnable,重写run()方法,在run()方法中添加要执行的代码,代码如下:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("运行中");
}
}
运行类代码:
public class runnableTest {
public static void main(String[] args){
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("运行结果");
}
}
运行结果如图:
图2-2 运行结果
使用继承Thread类方式来开发多线程应用程序在设计上是有局限的,因为java是单根继承,不支持多继承,所以为了改变这种局限性可以使用Runnable接口方式的=来实现多线程技术。
Thread.java类也实现了Runnable接口。那也就意味这构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象。这样就完全可以将一个Thread对象中的run()方法交于其他的线程进行调用。
线程中常见方法
方法名 | 调用实例 | 作用 | 备注 |
---|---|---|---|
currentThread() | Thread.currentTnread().getName() | 返回代码段正在被哪个线程调用的信息 | ~ |
isAlive() | mythread.isAlive(); | 判断当前线程是否处于活跃状态 | 当线程活跃状态(即线程已经启动且尚未终止)返回true,否则返货false |
sleep() | Thread.sleep(1000); | 指定时间内(以毫秒为单位)当前“正在被执行的线程”休眠 | 需要捕获InterruptedException异常 |
getId() | mythread.getId(); | 获取线程的唯一标识 | ~ |
start() | mythread.start() | 启动线程 | ~ |
run() | mythread.run() | 运行线程 | ~ |
ps(表中mythread表示实例化的线程类,即MyThread mythread = new MyThread();)
线程的安全
线程调用的随机性
在前面就提到过线程的执行顺序是与调用的顺序是无关的,下面就为展现线程具有的随机特性,创建一个线程类继承自Thread,在run()方法中数据变量i的值:
public class MyThread extends Thread{
private int i;
public MyThread(int i){
super();
this.i = i;
}
@Override
public void run() {
System.out.println(i);
}
}
在运行类进行调用
public class MyThreadTest {
public static void main(String[] args) {
MyThread t1 = new MyThread(1);
MyThread t2 = new MyThread(2);
MyThread t3 = new MyThread(3);
MyThread t4 = new MyThread(4);
MyThread t5 = new MyThread(5);
MyThread t6 = new MyThread(6);
MyThread t7 = new MyThread(7);
MyThread t8 = new MyThread(8);
MyThread t9 = new MyThread(9);
MyThread t10 = new MyThread(10);
MyThread t11 = new MyThread(11);
MyThread t12 = new MyThread(12);
MyThread t13 = new MyThread(13);
MyThread t14 = new MyThread(14);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
t8.start();
t9.start();
t10.start();
t11.start();
t12.start();
t13.start();
}
}
程序运行后的记过如图:
通过这个例子可以很清楚的看到执行start()方法的顺序并不代表线程启动的顺序。
实例变量
自定义线程类中的实例变量针对其他线程有共享与不共享之分,这在多线程之间进行交互是一个很重要的技术点。
(1) 不共享数据
不共享数据比较简单,就是每个线程的数据不进行交互,每个线程中的数据只能在本线程中进行改变,可以通过一个简答的例子进行说明:
创建线程类:
public class MyThread extends Thread{
private int count =5;
public MyThread(String name){
super();
this.setName(name);//设置线程名字
}
@Override
public void run() {
super.run();
while(count>0){
count--;
System.out.println("由"+this.currentThread().getName()+"计算,count="+count);
}
}
}
运行类的代码如下,创建三个线程,每个线程都有各自的count变量,自己减少自己的count变量的值,这样的情况就是变量和不共享。
public class RunTest {
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();
}
}
运行结果如图:
(2)共享数据的情况
共享数据的情况就是多个线程可以访问同一个变量,下面通过一个实例来看下数据共享的情况:
创建线程类:
public class MyThread1 extends Thread{
private int count =5;
@Override
public void run() {
super.run();
this.count--;
//在这里不能用for语句,因为使用同步之后其他线程就得不到运行的机会了。
System.out.println("由"+this.currentThread().getName()+"计算,count="+count);
}
}
运行类代码如下:
public class RunTest2 {
public static void main(String[] args){
MyThread1 myThread = new MyThread1();
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();
}
}
运行结果如图:
可以看到,这个程序我们可以得到两种不同的结果,这是非常致命的,这些不稳定的因素可能就会造成程序间接性的bug。因此我们避免这种情况发生是非常由必要的。下面就这种情况的发生来进行分析。
(1)是我们预想要看到的结果,而从(2)结果可以看出,线程A和线程B同时对count进行了处理,产生了“非线程安全”问题。与我们想要的递减结果是不同的,
在某些JVM中,i--的操作要分为如下3步,
1)获取原来i值
2)计算j-1
3)对i进行赋值
在这3个步骤中,如果由多个线程同时访问,那么一定会线程非线程安全问题。
那么如何避免这种情况呢?最简单的办法就是在run方法前面加入synchronized关键字,使得多个线程在执行run()方法时。以排队的方式进行处理。当一个线程调用run钱,先判断run方法有没有上锁,如果上锁,说明有其他的线程正在调用run方法,必须等其他线程对run方法调用结束才可以执行run方法。这样也就实现了排队调用run方法的目的,也就达到了按顺序对count变量减1的效果了。更多关于synchronized关键子的讲解会在后文中讲解。总之,在这个例子中我们在run方法前面加入synchronized关键字后,就值会出现(1)的结果。
线程的启动、暂停与停止
线程的启动
start():他的作用是启动一个新的线程,新的线程会执行相应的run()方法。start()不能被重复的调用。
run() : run()就和普通的成员方法一样么可以被重复的调用,单独的调用run()方法的话,会在当前的线程中执行run(),而不会启动新的线程!
下面通过两个例子来区别以下线程启动的两种方式:
线程类的代码testStrartThread.java
public class testStrartThread extends Thread {
public testStrartThread(String usename){
super(usename);
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+" is running");
}
}
测试代码:demo.java
public class Demo {
public static void main(String[] args) {
Thread testStrartThread = new testStrartThread("test run");
System.out.println(Thread.currentThread().getName()+" call testStratThread run");
testStrartThread.run();
Thread testStrartThread1 = new testStrartThread("test start");
System.out.println(Thread.currentThread().getName()+" call testStratThread run");
testStrartThread1.start();
}
}
下面是运行的结果,
结果说明:
(1)testStrartThread.run()是在主线程main中调用的,该run()方法直接运行在主线程mian上。
(2)testStrartThread1.start()会启动“testStrartThread”,“线程testStrartThread”启动之后,会启动run()方法;此时的run()方法是运行在“线程testStrartThread”上。
线程的暂停
suspend和resume可以有效的实现线程的暂停和回复的效果。可以通过以下的方法进行调用:
Thread myThread = new MyThread();
myThread.start();
myThread.suspend()://暂停线程
myThread.sleep(5000);
myThread.resume();//恢复线程
但是在使用的过程中,你会发现suspend()和resume()都被划上了删除线,在这里我们简单的探讨下为什么会被摒弃这两种方法。
独占
在使用suspen和resume方法时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。下面以一个例子来进行说明:
线程类:
public class synchronizedObject {
synchronized public void printString(){
System.out.println(" begin");
if(Thread.currentThread().getName().equals("a")){
System.out.println("a thread is suspend forever!");
Thread.currentThread().suspend();
}
System.out.println(" end");
}
}
测试demo类
public class deom {
public static void main(String[] args) {
try {
final synchronizedObject object = new synchronizedObject();
Thread thread = new Thread() {
@Override
public void run() {
object.printString();
}
};
thread.setName("a");
thread.start();
Thread.sleep(1000);
Thread thread2 = new Thread() {
@Override
public void run() {
System.out.println("thread2 is start but can`t into printString methord ");
object.printString();
}
};
thread2.setName("b");
thread2.start();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
最后的结果如图:
在线程suspend方法时,如果同实例一样使用,就会导致线程的阻塞。
不同步
在使用suspend和resum方法时也容易出现因为线程的暂停导致数据不同步的情况。实例代码如下:
public class myobject {
String username= "1";
String password= "111";
public void setValue(String u,String p){
this.username = u;
if(Thread.currentThread().getName().equals("a")){
System.out.println("stop thead a !");
Thread.currentThread().suspend();
}
this.password = p;
}
public void printString(){
System.out.println("username :"+this.username+" password: "+this.password);
}
}
测试类:
public class run {
public static void main(String[] args) {
try {
final myobject my = new myobject();
Thread thread1 = new Thread() {
public void run() {
my.setValue("a", "aa");
}
};
thread1.setName("a");
thread1.start();
Thread.sleep(500);
Thread thread2 = new Thread() {
public void run() {
my.printString();
}
};
thread2.start();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果如图:
通过这个简单的例子,我们可以看出来在使用suspend方法时还是有很多的坑的,需要我们仔细去分辨,所以摒弃了suspend方法。
yield方法
yiled()方法的作用是放弃当前的cpu资源,将它让给其他的任务去占用cpu执行时间。但是放弃的时间不确定,有可能刚刚放弃,马上又获得了cup时间片。调用示例:
public class MyThread extends Thread{
public void run(){
for(int i=0;i<10;i++){
system.out.printf("i= "+i+" current");
Thread.yield();//放弃当前的时间片
}
}
}
停止线程
停止线程四在多线程开发过程中很重要的技术点,掌握次技术可以对线程的停止进行有效的处理。停止线程在java语言中并不像break语句那么简单干脆,需要一些技巧性的处理。
大多数停止线程的操作使用Thread.interrupt()方法,尽管方法名称是“停止,中止”的意思,但是这个方法不会终止一个正在运行的线程,这需要加入一个判断才能完成线程的停止。关于此知识点后面会有相应的介绍。
在java中有以下三种方法可以终止正在进行的线程:
- 1.使用退出标志,使线程正常退出,也就是当run方法执行完成后线程自动终止。
- 2.使用stop方法强行终止线程,但是不推荐使用这种方法,因为使用他们会产生不可预料的结果。
- 3.使用inerrupt方法终止线程。
在介绍如何停止线程的知识点之前,先来看一下如何判断线程的状态是不是停止的。在java的JDK中,Thread.java类里提供了两种方法。
1) this.interrupted():测试当前线程是否已经中断。执行后具有将状态标志清除为false的功能。官方文档对interrupted方法的解释:
测试当前线程是否已经终端。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次返回false(在第一次第啊用已清除了其中断状态之后,且第二次第啊用检验完中断状态钱,当前线程再次中断的情况除外)。
2)this.isInterrupted():测试线程Thread对象是否已经是终端状态,但不清除状态标志。
利用异常退出线程
实例代码如下:
myThread代码如下:
public class MyThread extends Thread {
public void run() {
super.run();
try {
for (int i = 0; i < 20000; i++) {
if (this.interrupted()) {
System.out.println("it had stop,will exit!");
throw new InterruptedException();//抛出一个异常,直接退出线程
}
System.out.println("i= " + i);
}
System.out.println("this thread is`t stop");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
测试类的代码如下:
public class Run {
public static void main(String[] args) {
try {
MyThread myThread = new MyThread();
myThread.start();
Thread.sleep(20);
myThread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
}
}
运行结果如下:
如果只是使用interrupted方法来停止线程,本此测试程序将无法停止MyThread的线程(证明就是即使停止线程,for循环的System.out.println("this thread is`t stop");语句将会被执行),可以自己亲自尝试一下。
使用return停止线程
将方法interrupted与return相结合也能实现停止线程的效果,使用方法同异常法大同小异,只需要将前面例子中的MyThread线程类改成下面的代码:
public class MyThread extends Thread {
public void run() {
super.run();
for (int i = 0; i < 20000; i++) {
if (this.interrupted()) {
System.out.println("it had stop,will exit!");
return;
}
System.out.println("i= " + i);
}
System.out.println("this thread is`t stop");
}
}
运行类与上例一样。
运行效果如图:
可以看出来,结合return来使用也可以达到停止线程的效果。但是还是建议用抛异常的方法来实现线程的停止,因为在catch块中还可以将异常向上抛出,使线程停止的方法可以得到传播。
stop暴力停止方法
使用stop方法停止线程非常简单,形如:"this.stop();"即可停止线程,在这里不多多讲它的使用,着重讲的是该方法的副作用和使用时要注意的事项 。
调用stop()方法时会抛出java.lang.ThreadDeath异常,但在通常情况下,此异常不需要显示的捕获。方法stop已经被作废,因为如果使用强制让线程停止则可能使一些清理性工作得不到完成。另外一种情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的问题。
最后注意几点:
在sleep状态下停止线程,会进入到catch语句,并且清除停止状态位,使之变成false。
若在停止线程后在进入sleep状态,则在会运行完线程后进入到中断的catch语句中。
线程的优先级
在操作系统中,线程可以划分优先级,优先级高的线程可以得到的cpu资源较多,也就是cpu优先执行优先级较高的线程对象中任务。
设置线程优先级有助于“线程规划器”确定在下一次选择那一个线程来优先执行。设置线程优先级使用:setPriority()方法。在java的JDK中,线程的优先级划分为1-10这10个等级,如果不在此范围内,则会抛出异常。
设置线程优先级:
this.setPriority(6);
下面对线程优先级做几点说明:
继承性
在java中,线程的优先级具有继承性,比如a线程启动b线程,则b线程的优先级与a是一样的。在次就不局里说明了。
规则性
高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。另外,当线程优先级差距很大的时候,谁先执行完和代码的执行顺序无关。
随机性
优先级高的线程不一定每一次都先执行完。不要把线程的优先级与运行结果的顺序作为衡量的标准,优先级较高的线程并不一定每一都先执行完run方法中的任务,也就是说,线程的优先级与打印的顺序无关,不要将这两者相关联,他们的关系具有不确定性和随机性。(在优先级别差不多的情况下体现的比较明显)。
守护线程
在java线程中,有两种线程,一种是用户线程,一种是守护线程。
用户线程就是前面介绍的基本线程,守护线程是一种特殊的线程,他的特性是有“陪伴”的含义。当进程中不存在与非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。下面通过一个demo来理解守护线程:
创建一个线程类MyThread
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 class Runtest {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);//通过这个语句设置为守护线程
thread.start();
Thread.sleep(3000);
System.out.println("我离开thread对象就不在打印,也就是停止了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果如图所示:
这里,MyThread就是守护线程,守护我们的主线程,在RunTest中让主线程休眠了3s,所以MyThread线程执行了3s,1s打印一次,所以出现上面的结果。当主线程休眠结束,MyThread的守护线程也自动结束了,所以它也不在打印任何值了。