- 运行线程
运行线程一般有两种方法。第一种,可以对Thread类派生子类,覆盖其run()方法。第二种,实现Runnable接口,将 Runnable对象传递给Thread构造函数。这两种方法关键在于run()方法,它的签名如下:
public void run()
应该把线程要做的所有工作都放在这个方法中。这个方法可以调用其他方法;
- 返回线程中的信息
在多线程环境下,最难掌握的事情之一是如何返回线程中的信息。从结束中获取信息,这是多线程中对常见的错误理解之一。
方法一:使用存取方法返回结果的线程
举例:
/**
* 使用存取方法返回结果的“搬砖”线程
* @author WangChunhe
*
*/
public class BanZhuanThread extends Thread{
private String finish = null;
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
for(int i = 0 ; i<10; i++) {
StringBuilder sb = new StringBuilder("搬砖去:" ).append("第").append(i).append( "块砖");
System. out.println(sb);
if (i == 9) {
finish = "搬完砖了" ;
}
}
}
public String getFinish() {
return finish ;
}
}
/**
* 使用存取方法获得“搬砖”线程输出的主程序
* @author WangChunhe
*
*/
public class BanZhuanThreadTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
BanZhuanThread bz = new BanZhuanThread();
//开始搬砖
bz.start();
System. out.println(bz.getFinish());
}
}
执行结果
这个结果是还没开始搬砖就就调用了 bz.getFinish( ) 方法了,显然结果就是“null”了。原因是在单线程程序里,bz.start( )只是调用同一线程内的run( ) 方法。bz.start( ) 开始搬砖可能会在main( ) 方法方运行到bz.getFinish( ) 调用之前结束,也可能不会。如果没有搬砖结束,bz.getFinish( )则会返回null。
有“幸运”种情况就是在调用bz.getFinish( )之前,让main( ) 方法做一些事情,比如说从1数到500(其他的事情也可以说白了就 是让“搬砖”线程多些时间搬砖),主线程改写如下:
/**
* 使用存取方法获得“搬砖”线程输出的主程序
* @author WangChunhe
*
*/
public class BanZhuanThreadTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
BanZhuanThread bz = new BanZhuanThread();
//开始搬砖
bz.start();
//让主线从1数到500
for(int i = 0; i<400; i++){
System. out.println(i);
}
System. out.println(bz.getFinish());
}
}
执行结果:
首图
尾图
方法二:轮询
让获取放在结果字段设置之前的返回一个标志值(或者可鞥抛出一个异常)。然后主线程定时询问获取方法,看是否标志之外 的值。这个搬砖的例子,这表示重复地测试finish是否为空,如果不为空才使用它(搬砖线程代码内容不变)。
/**
* 使用轮询方法获得“搬砖”线程输出的主程序
* @author WangChunhe
*
*/
public class BanZhuanThreadTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
BanZhuanThread bz = new BanZhuanThread();
//开始搬砖
bz.start();
while(true ){
String Str = bz.getFinish();
//反复测试Str 是否为空
if(Str != null ){
System. out.println(Str);
break;
}
}
}
}
执行结果:
这种方案能起作用,但是它做了大量不需要做的工作。就是反复的询问“Str”是否为空。
- 回调
上面的轮询是让主程序中重复询问“搬砖”线程中的finish。回调则是让线程告知主程序何时结束。它通过调用主类中的方法来做到这一点(该线程就是在这主类中启动的)。因为线程在结束是要回头调用其创建者。这样,主程序就可以在等待线程结束时进行休息,也不会占用运行线程的时间。
/**
* “搬砖”线程
* @author WangChunhe
*
*/
public class BanZhuanThread extends Thread{
public String finish = null;
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
for(int i = 0 ; i<10; i++) {
StringBuilder sb = new StringBuilder("搬砖去:" ).append("第").append(i).append( "块砖");
System. out.println(sb);
if (i == 9) {
finish = "搬完砖了" ;
}
}
BanZhuanThreadTest. callBack(finish);
}
}
/**
* 主线程
* @author WangChunhe
*
*/
public class BanZhuanThreadTest {
//回调方法
public static void callBack(String s) {
System. out.println(s);
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
BanZhuanThread bz = new BanZhuanThread();
//开始搬砖
bz.start();
}
}
执行结果:
- 同步
线程通过共享内存、文件句柄、socket和其他资源,使得程序更高效。只要两个线程不同时使用相同的资源,多线程就比多进程高效得多,在多进程中,每个进程多要为各个资源保存自己的副本。多线程程序的不利方面是,如果两个线程同时访问相同的资源,其中一个就必须等待另一个结束。如果其中一个没有等待,资源就可能被破坏,这时候就需要同步的手段来防止这种情况的发生。
/**
* 搬砖线程
*
* @author WangChunhe
*
*/
public class MovingBricks implements Runnable {
// 默认5个砖头
private Integer bricks = 5;
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 10; i++) {
if (bricks > 0) {
System. out.println("# " + Thread.currentThread().getName()
+ " 搬了第" + (bricks--) + "个砖");
}
}
}
}
package com.bruce.com;
/**
*
* @author WangChunhe
*
*/
public class Test {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
MovingBricks mb = new MovingBricks();
new Thread(mb).start();
new Thread(mb).start();
new Thread(mb).start();
}
}
执行结果:
在上面的例子中,3个线程共享 mb 对象,到时3个线程同时搬了第5个砖。正常情况下,第5个砖应该是由3个线程中的一个去搬就可以。防止以上的情况发生,应该将bricks进行同步。例子如下
package com.bruce.com;
/**
* 搬砖线程
*
* @author WangChunhe
*
*/
public class MovingBricks implements Runnable {
// 默认5个砖头
private Integer bricks = 5;
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 10; i++) {
//使用synchronized进行同步,一旦线程开始显示进入这个同步快,所有其他线程必须等待该线程执行完同步块,
//才能进入。
synchronized (bricks ) {
if (bricks > 0) {
System. out.println("# " + Thread.currentThread().getName()
+ " 搬了第" + (bricks--) + "个砖");
}
}
}
}
}
package com.bruce.com;
/**
*
* @author WangChunhe
*
*/
public class Test {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
MovingBricks mb = new MovingBricks();
synchronized (mb) {
new Thread(mb).start();
new Thread(mb).start();
new Thread(mb).start();
}
}
}
执行结果:
对于同步的问题,不是仅仅向添加Synchrogazed修饰符并不是一劳永逸的解决方案。其原因以下三点:
1、同步使得很多VM的性能下降(虽然很多最新的VM已经在这方面有所改进),可能会使代码速度降低三分之一或更多。
2、同步大大增加了死锁的机会。
3、并不是对象本身需要防止同步修改或访问,对该方法所属类的实例进行同步,可能无法保护真正需要保护的对象。
同步的代替方式
对于由线程调度引起的不一致行为问题,同步并不总是最好的解决方案。还有又很多可以完全避免使用同步。
1、在任何情况下,使用局部变量代替字段。局部变量没有同步问题。每次进入一个方法时,虚拟机将为该方法创建全新的局部变量集 合。这些变量在方法外部是不可见,在方法退出时将被销毁。这样,一个局部变量就不会用于两个不同的线程。每个线程都有其自己的局部变量集。
2、构造函数一般不需要要担心线程担心安全问题。在构造函数返回前,没有线程有此对象的引用,所以不坑会有两个线程拥有此对象的引用。(最有可能的问题是构造函数依赖于另一个线程中对象,它可能在构造函数运行时改变,但这种情况并不常见。如果构造函数以某种方式把它的正在创建的对象的引用传递给不同线程,也会有潜在的问题,但是这种情况也不常见。)
3、将非线程安全的类作用线程的类的一个私有字段。只要包含类仅以线程安全的方式访问此非安全类,并且只要永远不让这个私有字段的引用泄露到另一个对象中,那么这个类就是安全。