正在学习《第一行代码Java》简略写一下学习笔记,记录自己的学习过程。书中指出三种多线程实现方法:继承Thread、使用Runnable接口、使用Callable接口,下面是使用和对比。
class MyThread extends Thread {
@Override
public void run(){
//线程运行部分
}
}
继承Thread类,通过start()方法调用
public class ThreadTest {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
}
}
class MyThread implements Runnable {
@Override
public void run(){
//线程运行部分
}
}
接口中没有start()方法启动,所以通过Thread的构造方法Thread(Runnable target)启动
public class ThreadTest {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
new Thread(mt).start();
}
}
Callable接口相较于Runnable,使用call()替代run()方法,多加了线程返回值,具体优劣后文会有描述。
public class MyThread implements Callable<Params> {
@Override
public Params call() throws Exception {
//线程运行部分
return Params;
}
}
Callable接口需要通过FutureTask使用,实现代码如下:
public class ThreadTest {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
FutureTask<Params> task = new FutureTask<Params>(mt);
new Thread(task).start();
Params result = task.get();
//Params result = task.get(long timeout,TimeUnit timeunit);
}
}
通过get()方法可以获得Callable的返回值,也可以调用get(long timeout,TimeUnit timeunit)方法来进行超时设置。
直接继承Thread方法比较直接且简单,但是由于Java的多继承限制,有一定局限性,使用Runnable接口可以解决多继承的问题,并且,使用Runnable可以较为简单的解决数据共享问题,举个例子:
class MyThread implements Runnable {
private int chips = 5;
@Override
public void run(){
for(int i = 0;i < 20;i++){
if(this.chips>0){
System.out.println("还剩"+this.chips--+"片没吃");
}
}
}
}
实现多线程数据共享
public class ThreadTest {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
运行结果:
还剩5片没吃
还剩2片没吃
还剩1片没吃
还剩3片没吃
还剩4片没吃
可以看到一共吃了5片,三个线程的数据是共享的,但是顺序不对,出现了数据错乱问题,这个问题可以通过同步解决可以参考Synchronized原理详解。
经过同步(synchronized)调整如下:
class MyThread implements Runnable {
private int chips = 5;
@Override
public void run(){
for(int i = 0;i < 20;i++){
synchronized(this){
if(this.chips>0){
System.out.println("还剩"+this.chips--+"片没吃");
}
}
}
}
}
实例化运行部分不用更改,结果如下:
还剩5片没吃
还剩4片没吃
还剩3片没吃
还剩2片没吃
还剩1片没吃
可见数据错乱已解决,关于synchronized的简要用法和理解在后续会提到。
由于继承了Thread的线程无法多次调用start(),虽然也可以使用上述同样的方法使用new Thread(mt).start启动线程,但是由于本身就具有start()方法而显得并不合理,就如书上所描述的那样:
两个人在沙漠里走,都只剩下最后一口水,结果A对B说,把你的水给我喝,我的不喝了。
可见相较于继承Thread方法,Runnable接口可以更好地实现数据共享,但不是唯一。
Callable相比于Runnable多了线程返回值,但是这个返回值的获取是通过阻塞来实现的。通过调用FutureTask的get方法,阻塞获取线程的返回值。个人认为这个方法只适用用于较少的特定需求,并不是一个可以常用的好方法。