Skip to main content
 首页 » 编程设计

Java中的多线程

2022年07月19日144lori

一、Java中线程实现

Java 中实现多线程的代码有三种方式,一种是继承 Thread 类,另一种是实现 Runnable 接口,在JDK1.5之后还有一个 Callable 接口,Runnable 接口方式利于资源共享的处理,Callable 接口的实现方式可以获取线程的返回值。

1. 方法1——继承 Thread 类

Thread 类是在 java.lang 包中定义的。一个类只要继承了 Thread 类就称为多线程操作类。在 Thread 的子类中必须明确覆写 Thread 类中的 run() 方法,此方法为线程主体。线程类定义如下:

class 类名 extends Thread { 
    属性... 
    方法... 
    public void run() { 
        线程主体 
    } 
}

启动线程是调用 Thread 类的 start() 方法,而不是 run() 方法。若直接调用 run() 方法就是一个普通方法调用,而不是多线程。并且 start() 方法只能调用一次,因为 start() 方法中有一个调用计数,多次调用会 throw new IllegalThreadStateException() 异常。

例子:

class MyThread extends Thread { 
    private String name; 
    public MyThread(String name) { 
        this.name = name; 
    } 
 
    public void run() { 
        for (int i = 0; i < 10; i++) { 
            System.out.println("name: " + name + " i=" + i); 
        } 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread("mt1"); 
        MyThread mt2 = new MyThread("mt2"); 
        //mt1.run(); //简单的方法调用 
        //mt2.run(); 
        mt1.start(); 
        mt2.start(); 
        //mt2.start(); //触发IllegalThreadStateException异常 
    } 
}

2. 方法2——实现 Runnable 接口

Java 中也可以通过实现 Runnable 接口的方式实现多线程,此接口定义为:

public interface Runnable { 
    public void run(); 
}

使用 Runnable 接口实现多线程的格式:

class 类名 implements Runnable { 
    属性... 
    方法... 
    public void run() { 
        线程主体 
    } 
}

Runnable 接口实际上还是依靠 Thread 实现多线程启动的,可以看 Thread 类的定义就知道使用方法了:

public class Thread extends Object implements Runnable { 
    private Runnable target; 
     
    public Thread(Runnable target, String name) { 
        init(null, target, name, 0); 
    } 
    private void init(ThreadGroup g, Runnable target, String name, long stackSize) { 
        ... 
        this.target = target; 
        ... 
    } 
    public void run() { 
        if (target != null) { 
            target.run(); 
        } 
    } 
}

如果传了 Runnable 类型的参数,最终执行的就是 Runnable 参数的 run() 方法。

举例1:

class MyThread implements Runnable { 
    private String name; 
    public MyThread(String name) { 
        this.name = name; 
    } 
 
    public void run() { 
        for (int i = 0; i < 10; i++) { 
            System.out.println("name: " + name + " i=" + i); 
        } 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread("mt1"); 
        MyThread mt2 = new MyThread("mt2"); 
        Thread t1 = new Thread(mt1); //传腹泻run()方法的Runnable的子类 
        Thread t2 = new Thread(mt2); 
        t1.start(); 
        t2.start(); 
    } 
}

通过 Runnable 接口实现多线程比起通过实现 Thread 类实现多线程的优势是便于多个同类对象资源共享时的处理。因为后者的运行的 run() 方法是来自参数对象的,因此多个线程传同一个参数对象的话其属性就只有一份资源。而前者需要定义多个对象然后调用其 run() 方法实现多线程,由于是多个对象,其属性就是多个资源了。开发过程中建议使用 Runnable 接口的实现方式实现多线程。


3. 方法3——利用 Callable 接口

通过 Runnable 接口实现的多线程会出现 run() 方法不能返回操作结果的问题,为了解决此问题,JDK1.5开始提供了一个新的接口 java.util.concurrent.Callable,定义如下:

public interface Callable<V> { 
    public V call() throws Exception; 
}

call() 方法在执行完后可以返回一个具体类型的数据。但是 Thread 类中没有定义任何构造方法来接收 Callable 接口对象实现对象,这导致多线程的启动又遇到了问题,JDK1.5之后开始提供一个 java.util.concurrent.FutureTask<V> 类来解决这个问题,其定义:

public class FutureTask<V> extends Object implements RunnableFuture<V>

FutureTask 实现了 RunnableFuture 接口,而后者又同时实现了 Future 和 Runnable 接口。如果想要接收线程执行的返回结果,调用 Future 接口中的 get() 方法即可。FutureTask 类常用方法如下:

public FutureTask(Callable<V> callable); //构造函数,接收 Callable 接口对象实例 
public FutureTask(Runnable runnable, V result); //接收 Runnable 接口实例,并指定返回结果类型 
public V get() throws InterruptedException, ExecutionException; //取得线程的执行结果,由 Future 接口定义

FutureTask 是 Runnable 接口的子类,并且其构造函数可以接收 Callable 实例,因此依然可以利用 Thread 类来实现多线程的启动。若想获取线程执行结果,则利用 Future 接口中的 get() 方法。

例子:

import java.util.concurrent.Callable; 
import java.util.concurrent.FutureTask; 
import java.util.concurrent.ExecutionException; 
 
class MyThread implements Callable<String> { 
    private int ticket = 5; 
 
    //@override 
    public String call() throws Exception { 
        for (int i = 0; i < 10; i++) { 
            if (ticket > 0) { 
                System.out.println("ticket left: " + ticket--); 
            } 
        } 
        return "sold out"; 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        MyThread mt2 = new MyThread(); 
 
        FutureTask<String> task1 = new FutureTask<String>(mt1); 
        FutureTask<String> task2 = new FutureTask<String>(mt2); 
 
        new Thread(task1).start(); 
        new Thread(task2).start(); 
 
        try { 
            System.out.println("task1 return: " + task1.get()); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } catch (ExecutionException e) { 
            e.printStackTrace(); 
        } 
        try { 
            System.out.println("task2 return: " + task2.get()); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } catch (ExecutionException e) { 
            e.printStackTrace(); 
        } 
    } 
}

 Runnable 接口是 Java 最早提供也是使用最广泛的,平时建议通过使用 Runnable 接口的方式实现多线程

二、线程操作相关方法

1. Thread 类中的主要方法

在 Java 实现多线程的程序中,虽然 Thread 类实现了 Runnable 接口,但是操作线程的主要方法并不在 Runnable 接口中,而是在 Thread 类中,下面列出 Thread 类中的主要方法:

public Thread(Runnable target) //构造方法,通过 Runnable 接口子类对象实例化 Thread 对象 
public Thread(Runnable target, String name) //构造方法,通过 Runnable 接口子类对象实例化 Thread 对象,并设置子线程名称 
public Thread(String name) //构造方法,实例化对象并设置子线程名称 
public static Thread currentThread() //返回目前正在执行的线程,静态方法,可以直接 Thread.currentThread()进行调用。 
public final String getName() //返回线程名称 
public final void setName(String name) //设定线程名称 
public final int getPriority() //返回线程优先级 
public final void setPriority(int newPriority) //设置线程优先级 
public boolean isInterrupted() //判断目前线程是否被中断,如果是返回true,否则返回false 
public final boolean isAlive() //判断线程是否在活动,如果是返回true,否则返回false 
public final void join() throws InterruptedException //等待线程死亡 
public final synchronized void join(long millis) throws InterruptedException //等待 millis ms后,线程死亡  ###### 
public void run() //线程函数主体 
public static void sleep(long millis) throws InterruptedException //使目前正在执行的线程休眠 millis ms 
public void start() //开始执行新线程 
public String toString() //返回代表线程的字符串 
public static void yield() //将目前正在执行的线程暂停,允许其他线程执行 
public final void setDaemon(boolean on) //将一个线程设置为后台运行

2. Thread 类中的方法使用

(1) getName/setName

线程名称一般是启动前设置,但是也允许为已经运行的线程设置名字,允许两个 Thread 对象有相同的名字。如果没有设置线程的名字,系统会自动为其分配,格式为 Thread-X,X是数字,从0开始。

class MyThread implements Runnable { 
    public void run() { 
        for (int i = 0; i < 3; i++) { 
            System.out.println(Thread.currentThread().getName() + " running " + i); //为啥直接使用getName()和Thread.getName()都报错 
        } 
        while(true); 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        new Thread(mt1).start(); 
 
        MyThread mt2 = new MyThread(); 
        new Thread(mt2).start(); 
 
        MyThread mt3 = new MyThread(); 
        new Thread(mt3, "mt3").start(); 
 
        mt1.run(); //直接调用run()也打印出了main,说明main也是一个线程 
    } 
} 
 
/* 
# java ThreadDemo  
Thread-0 running 0 
Thread-0 running 1 
Thread-0 running 2 
mt3 running 0 
mt3 running 1 
mt3 running 2 
main running 0 
main running 1 
main running 2 
Thread-1 running 0 
Thread-1 running 1 
Thread-1 running 2 
*/

而直接继承 Thread 类是可以直接调用的,Runnable 接口继承 Thread 类,这里 MyThread 类实现 Runnable 接口,与直接继承 Thread 类有何区别?

class MyThread extends Thread { 
    public void run() { 
        for (int i = 0; i < 100; i++) { 
            try { 
                Thread.sleep(1); 
            } catch(Exception e) {} 
            System.out.println("name: " + getName()); 
            if (i > 50) { 
                while(true); 
            } 
        } 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        mt1.start(); 
        try { 
            Thread.sleep(10); 
        } catch(Exception e) {} 
        mt1.setName("Hello"); 
    } 
}

这些线程的名字只是 Java 层的,cat /proc/<pid>/task/<tid>/comm 全部显示为java,此例中/proc/<pid>/task/下有17个线程,名字全为java。就算是调用了 setName() 也不会改变 cat 出来的名字。

(2) isAlive()判断线程是否启动

class MyThread implements Runnable { 
    public void run() { 
        System.out.println(Thread.currentThread().getName() + " running"); 
        while(true); 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        Thread t1 = new Thread(mt1); 
        System.out.println("isAlive: " + t1.isAlive()); 
        t1.start(); 
        System.out.println("isAlive: " + t1.isAlive()); 
    } 
} 
 
/* 
# java ThreadDemo  
isAlive: false 
isAlive: true 
Thread-0 running 
*/

注意,主线程先执行完,但是其它线程不会受到任何影响,也不会随着主线程的结束而结束。和C不同!

(3) sleep() 线程的休眠

class MyThread implements Runnable { 
    public void run() { 
        while(true) { 
            System.out.println(Thread.currentThread().getName() + " running"); 
        } 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        Thread t1 = new Thread(mt1); 
        t1.setDaemon(true); 
        t1.start(); 
    } 
}

但是没有设置成功,程序执行后直接退出。

(3) setPriority()/getPriority() 线程的优先级

class MyThread implements Runnable { 
    public void run() { 
        for (int i = 0; i < 100; i++) { 
            System.out.println(Thread.currentThread().getName() + " running " + i); 
        } 
        while(true); 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        Thread t1 = new Thread(mt1, "MIN_P"); 
        t1.setPriority(Thread.MIN_PRIORITY); 
        t1.start(); 
        System.out.println(t1.getName() + " priority: " + t1.getPriority()); 
 
        MyThread mt2 = new MyThread(); 
        Thread t2 = new Thread(mt2,"MAX_P"); 
        t2.setPriority(Thread.MAX_PRIORITY); 
        t2.start(); 
        System.out.println(t2.getName() + " priority: " + t2.getPriority()); 
 
        MyThread mt3 = new MyThread(); 
        Thread t3 = new Thread(mt3, "NOR_P"); 
        t3.setPriority(Thread.NORM_PRIORITY); 
        t3.start(); 
        System.out.println(t3.getName() + " priority: " + t3.getPriority()); 
    } 
} 
 
# java ThreadDemo  
MIN_P priority: 1 
... 
MAX_P priority: 10 
... 
NOR_P priority: 5 
...

top看CPU占用率为300%,的确是三个核被占满了,但是cat /proc/<pid>/task/<tid>/sched,所有线程的优先级还是120,看来又是Java 虚拟机自己封装了优先级,对操作系统是不可见的。

(4) yield()线程礼让

class MyThread extends Thread { 
    public void run() { 
        for (int i = 0; i < 100; i++) { 
            System.out.println(getName() + " running " + i); 
            if (i == 50) { 
                System.out.println(getName() + " yield"); 
                yield(); 
            } 
        } 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        mt1.start(); 
        MyThread mt2 = new MyThread(); 
        mt2.start(); 
    } 
}

yield() 对应内核的实现机制是将此任务设置为 ignore buddy,只是选中它运行时只跳过一次,若是下次任务切换再次选中,就继续运行了,所以上面测试用例yield()后下次选可能还是选自己。


(5) interrupt()中断线程

class MyThread extends Thread { 
    public void run() { 
        for (int i = 0; i < 100; i++) { 
            System.out.println(getName() + " running " + i); 
            try { 
                sleep(100); 
            } catch(Exception e) { 
                System.out.println(getName() + " get exception and return"); 
                return; 
            } 
        } 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt1 = new MyThread(); 
        mt1.start(); 
        try { 
            Thread.sleep(2000); 
        } catch(Exception e) { } 
        mt1.interrupt(); 
    } 
} 
 
/* 
# java ThreadDemo  
Thread-0 running 0 
... 
Thread-0 running 19 
Thread-0 get exception and return 
*/

可以看出 interrupt() 就是使自己受到一个异常。若 run() 中没有调用 sleep 并进行 catch 异常,线程是不会响应 interrupt() 调用的,正常执行完毕。


三、线程同步互斥问题

1. synchronized 关键字

临界区可以通过 同步代码块 或 同步方法 两种方式完成。代码块就是使用 {} 括起来的一段代码,根据其位置和声明的不同,又分为普通代码块、构造块、静态块 3种。若代码块上加 synchronized 关键字就称为同步代码块。
同步代码块:

//同步代码块: 
synchronized(同步对象) { 
    ... 
} 
//同步方法: 
synchronized 返回值类型 方法名(参数列表) { 
    ... 
}

Java 中定义方法的完整格式:

访问权限{public/default/protected/private}[final][static][synchronized] 
返回值类型 方法名称(参数列表)[throws Exception1, Exception2] { 
    函数体 
}

2. 等待与唤醒

Object类是所有类的父类,此类中有以下方法是对多线程进行支持的,notify()只唤醒一个,notifyAll()唤醒所有等待线程。

public final void wait() throws InterruptedException //线程等待 
public final void wait(long timeout) throws InterruptedException //线程等待,可指定最长等待时间,单位ms 
public final void wait(long timeout, int nanos) throws InterruptedException //线程等待,可指定最长等待多少ms和ns 
public final void notify() //唤醒一个等待线程 
public final void notifyAll() //唤醒全部等待线程

3. 一个生产者和消费者的例子

class Info { 
    private static boolean flag = true; //true can produce 
    private String content; 
 
    public synchronized String get() { 
        if (flag) { 
            try { 
                super.wait(); //Object's, the same as wait() 
            } catch(Exception e) {} 
        } else { 
            flag = true; 
            super.notify(); //Object's, the same as notify() 
        } 
        return this.content; 
    } 
    public synchronized void set(String content) { 
        if (flag) { 
            flag = false; 
            this.content = content; 
            System.out.println("set: " + this.content); 
            super.notify(); 
        } else { 
            try { 
                super.wait(); 
            } catch(Exception e) {} 
        } 
    } 
} 
 
class MyThread implements Runnable { 
    private boolean role; 
    private Info info; 
    public MyThread() { 
        this.info = new Info(); 
    } 
    public void run() { 
        if ("Provider".equals(Thread.currentThread().getName())) { 
            role = true; 
        } else { 
            role = false; 
        } 
        if (role) { 
            for (int i = 0; i < 100; i++) { 
                System.out.println("get: " + info.get()); 
            } 
        } else { 
            for (int i = 0; i < 100; i++) { 
                info.set("I am " + i); 
            } 
        } 
    } 
} 
 
public class ThreadDemo { 
    public static void main(String args[]) { 
        MyThread mt = new MyThread(); 
        new Thread(mt, "Consumer").start(); 
        new Thread(mt, "Provider").start(); 
    } 
} 
 
/* 
... 
set: I am 94 
get: I am 94 
set: I am 96 
get: I am 96 
set: I am 98 
get: I am 98 //为啥都是偶数,丢一个数据呢? 
*/

首先要保证两个线程共享 Info 实例对象才行,这样使用实现 Runnable 方式来实现线程好一些。

四、线程的生命周期

1. 线程中的 suspend() resume() stop() 方法已经被标记为 @Deprecated 注释,不建议使用。

2. 可以通过自己实现一个 stop() 然后在 run() 调用来实现 stop 线程。


本文参考链接:https://www.cnblogs.com/hellokitty2/p/15383759.html