• 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 线程。

  • 相关阅读:
    mysql 优化20点
    java function
    设计模式 概览
    Linux安装java1.8并配置环境变量
    windows下一次执行多个sql文件
    mybatis查询结果为空时的返回值问题
    Java中Json与String互转
    SSM Service自动注入失败
    本地安装Mysql5.7过程中出现的一系列问题
    解决本地工具无法连接服务器上的mysql的问题
  • 原文地址:https://www.cnblogs.com/hellokitty2/p/15383759.html
Copyright © 2020-2023  润新知