java线程完全不懂?进来看看基础

228 阅读6分钟

什么是进程

进程是操作系统分配资源的基本单位,可视为正在运行的程序

什么是线程

线程是操作系统进行调度的基本单位,一个进程中可以存在多个线程,能够访问所属进程的共享资源

线程与进程的区别

  1. 一个程序至少有一个进程,一个进程至少有一个线程
  2. 进程间想要交换资源相对困难,而线程间可以共同访问共享资源

线程的基本方法

方法描述
run()线程执行的内容
start()线程启动方法
currentThread()返回当前正在执行线程对象的引用
setName()设置线程名称
getName()获取线程名称
setPriority()设置线程优先级
getPriority()获取线程优先级
setDaemon()设置线程为守护线程
isAlive()判断线程是否启动
interrupte中断另一个线程的执行状态
join()可以使一个线程强制运行,运行期间其他线程无法运行,必须等待此线程执行完毕
Thread.sleep()将当前正在执行的线程休眠
Thread.yield将当前正在执行的线程休眠,让其他线程执行

线程的状态

  1. 新建New 没有调用start方法
  2. 可运行Runnable 调用start方法
  3. 运行Running 线程获得cpu时间片
  4. 阻塞Blocked
    • 等待阻塞:线程调用wait方法
    • 同步阻塞:对象获取同步锁时,同步锁被占用
    • 其他阻塞:线程调用sleep或者join方法
  5. 终止Terminated 执行体被执行完毕

创建线程

三种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口

通过继承Thread类

  1. 定义Thread类的子类,重写run()方法。run()方法被称为执行体。
  2. 创建Thread子类的实例,即创建线程对象
  3. 调用线程对象start()方法来启动线程
public class ExtendThreadDemo {
    static class myThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程测试");
        }
    }

    public static void main(String[] args) {
        myThread mt = new myThread();
        mt.start();
    }
    
}

实现Runnable接口

  1. 定义Runnable接口的实现类,重写run()方法作为执行体
  2. 创建Runnble实现类的实例,作为参数创建Thread对象
  3. 调用线程对象的start()方法来启动线程
static class MyThread implements Runnable {
        int sleepTime;
        public MyThread(int sleepTime) {
            this.sleepTime = sleepTime;
        }
        @Override
        public void run() {
            System.out.println("当前线程名称:"+Thread.currentThread().getName());
            try {
                Thread.sleep(sleepTime);
                System.out.println(Thread.currentThread().getName()+"睡眠"+sleepTime/1000+"秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread(1000), "线程1");
        Thread t2 = new Thread(new MyThread(5000), "线程2");
        Thread t3 = new Thread(new MyThread(10000), "线程3");
        t1.start();
        t2.start();
        t3.start();
    }

实现Callable接口

jdk1.5以后提供了Callable和Future。通过它们可以在线程执行结束后,返回执行结果。

通过实现Callable接口创建线程的步骤

  1. 创建Callable接口实现类,重写call()方法作为执行体
  2. 创建Callable接口实现类的实例,使用Futuretask包装Callable接口实现类
  3. 将Futuretask对象作为Thread构造函数的参数,并启动线程
  4. 调用Futuretask对象的get方法来获取线程执行后的结果
public class ImplementCallableThread {
    static class MyCallable implements Callable {

        @Override
        public Object call() throws Exception {
            System.out.println("看看会返回什么");
            return this;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask ft = new FutureTask(new MyCallable());
        Thread t = new Thread(ft);
        t.start();
        System.out.println(ft.get());
    }

}

FAQ

run与start方法的区别

  1. run方法作为线程的执行体
  2. start方法则会调用run方法
    核心源码
    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
public synchronized void start() {
    ······
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        }
    }
    ······

由注释可以知道,该方法native为方法start0调用run方法

线程休眠

Thread.sleep方法可以使得正在执行的线程进入休眠状态 Thread.sleep(int 休眠时间[毫秒])

代码

public class ImplementRunnableDemo {
    static class MyThread implements Runnable {
        int sleepTime;
        public MyThread(int sleepTime) {
            this.sleepTime = sleepTime;
        }
        @Override
        public void run() {
            System.out.println("当前线程名称:"+Thread.currentThread().getName());
            try {
                Thread.sleep(sleepTime);
                System.out.println("睡眠"+sleepTime/1000+"秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread(1000), "线程1");
        Thread t2 = new Thread(new MyThread(5000), "线程2");
        Thread t3 = new Thread(new MyThread(10000), "线程3");
        t1.start();
        t2.start();
        t3.start();
    }
}

结果

当前线程名称:线程1
当前线程名称:线程3
当前线程名称:线程2
线程1睡眠1秒
线程2睡眠5秒
线程3睡眠10秒

可以看到虽然是线程3比线程2先运行,但是由于睡眠进入睡眠状态,线程2进入运行,线程2只睡眠5秒钟,所以先唤醒。

线程礼让

Thread.yield方法,调用时切换其他线程来执行。

代码

public class ThreadYield {
    static class MyThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i <= 4; i++) {
                System.out.println(Thread.currentThread().getName() + "输出" + i);
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();
        Thread t1 = new Thread(mt1,"线程1");
        Thread t2 = new Thread(mt2,"线程2");
        Thread t3 = new Thread(mt3,"线程3");
        t1.start();
        t2.start();
    }
}

结果

线程1输出0
线程2输出0
线程1输出1
线程2输出1
线程1输出2
线程2输出2
线程1输出3
线程2输出3
线程1输出4
线程2输出4

终止线程

使用stop方法来终止线程。

Thread中的stop方法被废弃理由如下: 使用stop方法会直接停止线程,导致它持有的所有锁被释放,如果有写入操作,很有可能导致每次执行的结果不一样 当一个线程运行时,另一个线程可以条用interrupt方法中断其运行状态。

使用interrupt方法来终止线程

代码

public class ThreadInterruptDemo {

    public static void main(String[] args) {
        MyThread mt = new MyThread(); // 实例化Runnable子类对象
        Thread t = new Thread(mt, "线程"); // 实例化Thread对象
        t.start(); // 启动线程
        t.interrupt(); // 中断线程执行
        for(int i = -10000;i <= -1; i++) {
            System.out.println(i);
        }
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            System.out.println("1、进入run()方法");
            for(int i = 0;i <= 10000; i++) {
                System.out.println(i);
            }
            try {
                System.out.println("开始休眠");
                Thread.sleep(10000); // 线程休眠10秒
                System.out.println("2、已经完成了休眠");
            } catch (InterruptedException e) {
                System.out.println("3、休眠被终止");
                return; // 返回调用处
            }
            System.out.println("4、run()方法正常结束");
        }
    }
}

通过以上代码得到的结果 可以得到以下的结论:

  1. interrupt方法无法中断某些处理逻辑,如上出代码的循环逻辑
  2. interrupt方法可以中断休眠状态的线程

如何解决interrupt方法无法中断循环逻辑?

  1. 定义标志位,在run方法中使用标志位控制线程结束
  2. 使用interrupt方法和Thread.interrupted方法配合

方法1

    /*使用标志位中断线程*/
    public static void main(String[] args) throws InterruptedException {
        MyThread mt = new MyThread(); // 实例化Runnable子类对象
        Thread t = new Thread(mt, "线程"); // 实例化Thread对象
        t.start(); // 启动线程
        t.sleep(10);
        mt.setFlag(true);

    }

    static class MyThread implements Runnable {
        boolean flag = false;
        public void setFlag(boolean flag) {
            this.flag = flag;
        }
        @Override
        public void run() {
            System.out.println("1、进入run()方法");
            int i = 0;
            for (i = 0; i <= 10000; i++) {
                if(flag) break;
                System.out.println(i);
            }
            if(i != 10001) {
                System.out.println("线程被中断");
            }
            System.out.println("4、run()方法正常结束");
        }
    }

结果

584
585
586
587
588
线程被中断
4run()方法正常结束

方法2

    //结合interrupt和interrupted中断线程
    public static void main(String[] args) throws InterruptedException {
        MyThread mt = new MyThread(); // 实例化Runnable子类对象
        Thread t = new Thread(mt, "线程"); // 实例化Thread对象
        t.start(); // 启动线程
        t.sleep(10);
        t.interrupt(); // 中断线程执行
    }

    static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("1、进入run()方法");
            for(int i = 0;i <= 10000; i++) {
                if(Thread.interrupted()) {
                    break;
                }
                System.out.println(i);
            }
            try {
                System.out.println("开始休眠");
                if(Thread.interrupted()) {
                    throw new InterruptedException();
                }
                Thread.sleep(10000); // 线程休眠10秒
                System.out.println("2、已经完成了休眠");
            } catch (InterruptedException e) {
                System.out.println("3、休眠被终止");
                return; // 返回调用处
            }
            System.out.println("4、run()方法正常结束");
        }
    }

结果

682
683
684
开始休眠
2、已经完成了休眠
4run()方法正常结束

线程类别

Java中有两类线程

  1. 用户线程(User Thread)
  2. 守护线程(Daemon Thread)

两者的关系: 只要用户线程还存在,所有的守护线程必须工作,只有当所有的用户线程都结束了,守护线程随着JVM一同结束工作

守护线程

Daemon守护线程的优先级别低,工作就是为其他线程运行提供便利服务,最典型的守护线程就是GC(垃圾回收器),

用户如何定义守护线程?

  1. 使用isDaemon方法来判断线程是否为守护线程
  2. 使用setDaemon方法来设置线程为守护线程

注意点:

  1. 用户设置的守护线程必须要在线程开始执行之前设置,不能把正在运行的常规线程设置成守护线程
  2. 在守护线程中新产生的线程也是守护线程
  3. 不能把所有工作都交给守护线程,如读写操作和计算逻辑,因为一旦用户线程结束,所有的守护线程也会跟着结束,可能会导致计算结果每次都不同

代码

public class ThreadDaemonDemo {
    static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.println("我是守护线程");
            }
        }
    }
    static class MyThreadB extends Thread {
        @Override
        public void run() {
            for (int i = 0; i <= 100; i++) {
                if(i == 50) {
                    try {
                        Thread.currentThread().sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(i);
            }
        }
    }
    public static void msikaoain(String[] args) {
        MyThread t = new MyThread();
        MyThreadB b = new MyThreadB();
        t.setDaemon(true);
        t.start();
        b.start();
    }
}

从结果可以看出,当用户线程执行完毕后,守护线程停止

FAQ

sleep、yield、join方法有什么区别?

yield

  1. 当前线程变为就绪状态
  2. 只有与当前线程相同或者更高优先级的线程才会得到执行机会

sleep

  1. 当前线程状态变为等待睡眠状态
  2. 等待时间过后恢复为就绪状态
  3. 调用方法后,所有优先级的线程都有机会进入运行状态

join

  1. 线程进入阻塞状态
  2. 必须等待调用的线程执行完毕才能继续执行

为什么sleep和yield方法是静态的?

它们针对的是正在运行的线程,对于其他线程是没有意义的,设为静态方法可以避免程序员在其他状态调用它们

Java线程是否按照线程优先级严格执行?

即使设置了优先级也无法保证线程按照优先级严格执行。因为线程优先级依赖与操作系统的支持。

线程间通信

当多个线程需要合作完成一个需求时,并且有先后顺序,那么就需要对线程之间进行协调。

主要用到的方法wait/notify/notifyAll

wait方法

让当前正在运行的线程释放它所有的锁,并进入到阻塞状态,需要等待notify/notifyAll来唤醒。如果没有释放锁,那么其他线程就无法进入到对象的同步方法或者同步控制块中,就无法执行notify或者notifyAll来唤醒阻塞状态的线程,造成死锁

notity方法

唤醒一个阻塞状态的线程,让它拿到对象锁,具体唤醒哪一个线程由JVM控制

notifyAll方法

唤醒所有阻塞状态的线程

注意事项

  1. 上面三个方法都是在Object类中的方法
  2. 上面三个方法都只能在synchronized方法或者synchronized代码块中使用

为何不将它们定义在Thread中?

同步线程中,只有同一个锁上被等待线程,可以被同一个锁上的notify唤醒,无法对不同锁中的线程进行唤醒

代码

public class ThreadWaitNotifyDemo {
    private static final int QUEUE_SIZE = 10;
    private static final PriorityQueue<Integer> queue = new PriorityQueue<>(QUEUE_SIZE);

    public static void main(String[] args) {
        new Producer("生产者A").start();
        new Producer("生产者B").start();
        new Consumer("消费者").start();
    }

    static class Consumer extends Thread {

        Consumer(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                //对象锁,同处理同一个对象的对象才能进入
                synchronized (queue) {
                    //如果列表空了,消费者则不能消费,进入阻塞状态,同时唤醒消费者
                    while (queue.size() == 0) {
                        try {
                            System.out.println("队列空,等待数据");
                            queue.wait();
                            queue.notify();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notifyAll();
                        }
                    }
                    //取走一件商品后,便进入阻塞,让下一位消费者或者生产者工作
                    queue.poll(); // 每次移走队首元素
                    queue.notify();
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 从队列取走一个元素,队列当前有:" + queue.size() + "个元素");
                }
            }
        }
    }

    static class Producer extends Thread {

        Producer(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == QUEUE_SIZE) {
                        try {
                            System.out.println("队列满,等待有空余空间");
                            queue.wait();
                            queue.notify();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notifyAll();
                        }
                    }
                    queue.offer(1); // 每次插入一个元素
                    queue.notify();
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 向队列取中插入一个元素,队列当前有:" + queue.size() + "个元素");
                }
            }
        }
    }
}

管道

用于线程之间的数据交流

四个具体实现

  1. PipedOutputStream
  2. PipedInputStream
  3. PipedReader
  4. PipedWriter

实现步骤

  1. 连接输入输出管道
  2. 输入端输入
  3. 输出端输出

代码

public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        Thread writeThread = new Thread(new Write(out), "WriteThread");
        writeThread.start();
        printThread.start();
    }

    static class Print implements Runnable {

        private PipedReader in;

        Print(PipedReader in) {
            this.in = in;
        }

        public void run() {
            int receive = 0;
            try {
                StringBuilder sb = new StringBuilder("");
                while (true) {
                    receive = in.read();
                    if ((char) receive == '-') {
                        break;
                    }
                    sb.append((char) receive);

                }
                System.out.println(sb.toString());
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class Write implements Runnable {
        private PipedWriter out;

        public Write(PipedWriter out) {
            this.out = out;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                StringBuilder sb = new StringBuilder("");
                while (true) {
                    receive = System.in.read();
                    sb.append((char) receive);
                    if ((char) receive == '-') {
                        break;
                    }
                }
                out.write(sb.toString());
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }