Java中的多线程

一、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