Java多线程之基础篇


本文整理自RedSpider社区开源文章:深入浅出Java多线程

一、进程与线程

1.1 基本概念

进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态

此时,CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完
当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的状态进行恢复,接着继续执行

一个进程就包含了多个线程,每个线程负责一个单独的子任务

进程让操作系统的并发性成为了可能,而线程让进程内部的并发成为了可能

进程与线程的区别:

  • 进程单独占有各自的内存空间,有内存隔离,数据共享过程复杂但同步简单;线程在其对应的进程中共享内存空间,数据共享简单但同步复杂
  • 因为进程之间互不干扰,故不容易影响主程序的稳定性;而某个线程崩溃可能影响到整个进程崩溃,可靠性低
  • 因进程独占内存空间,故创建和销毁开销大(重量级);线程开销小(轻量级)

1.2 上下文切换

CPU从一个进程/线程切换到另一个进程/线程

上下文:某一时间点CPU寄存器和程序计数器中的内容

CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。

但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。所以减少系统中上下文切换次数,可以提升多线程性能

二、多线程实现

2.1 继承Thread类

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("This is myThread");
    }

    public static void main(String[] args) {
				// 调用start()方法后,等待获取到时间片后就会执行run方法
        new Thread(new MyThread()).start();
    }
}

2.2 实现Runnable接口

public class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("This is myThread");
    }

    public static void main(String[] args) {
        new Thread(new MyThread()).start();
    }
}

2.3 实现Callable接口

Callable一般是配合线程池工具ExecutorService来使用的

与前两种多线程实现方式不同的是,实现Callable接口的方式具有返回值

public class MyOtherThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return 2;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<Integer> result = executor.submit(new MyOtherThread());
        // 注意get()方法会阻塞当前线程
        System.out.println(result.get());
    }
}

2.4 Future接口

public abstract interface Future<V> {
		// 尝试取消,但不一定成功,参数表示是否采用中断的方式取消线程执行
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

2.5 FutureTask类

FutureTask可以在高并发环境下确保任务只执行一次

FutureTask实现了RunnableFuture接口

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

示例:

与上面1.3不同的是,此处调用的是submit(Runnable task)方法,同时使用的是futureTask调用get();而1.3处调用的是submit(Callable<T> task)方法,同时使用的是其返回值去调用get()

public class MyOtherThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return 2;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new MyOtherThread());
        executor.submit(futureTask);
//        Future<Integer> result = executor.submit(new MyOtherThread());
        System.out.println(futureTask.get());
    }
}

FutureTask的几个状态:

/**
  *
  * state可能的状态转变路径如下:
  * NEW -> COMPLETING -> NORMAL
  * NEW -> COMPLETING -> EXCEPTIONAL
  * NEW -> CANCELLED
  * NEW -> INTERRUPTING -> INTERRUPTED
  */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

2.6 Thread类和Runnable接口的区别

  • Runnable更符合面向对象,使用更灵活(Java多实现,单继承特性)
  • Runnable降低了线程对象和线程任务的耦合性
  • 建议优先使用Runnable接口方式自定义线程类

三、线程组和线程优先级

3.1 线程组(ThreadGroup)

ThreadGroup包含并管理着Thread,执行main()方法线程的名字是main,如果在new Thread时没有显式指定线程组,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组

线程组的作用就是统一控制线程的优先级和检查线程的权限

public class Demo {
    public static void main(String[] args) {
        Thread testThread = new Thread(() -> {
            System.out.println("testThread当前线程组名字:" +
                    Thread.currentThread().getThreadGroup().getName());
            System.out.println("testThread线程名字:" +
                    Thread.currentThread().getName());
        });

        testThread.start();
    System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());
        System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
    }
}

// 执行main所在线程的线程组名字:main
// 执行main方法线程名字:main
// testThread当前线程组名字:main
// testThread线程名字:Thread-0

3.2 线程优先级

Java中默认线程优先级为5

线程优先级范围是1~10(由低到高),但该值只是提供给操作系统做参考(有更高的概率获取到时间片去执行),不过最终决定权还是在操作系统的手上(线程调度算法)

public class PriorityThread implements Runnable {

    @Override
    public void run() {
        System.out.println("当前线程名:" + Thread.currentThread().getName() + ", 优先级为:" + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        PriorityThread priorityThread = new PriorityThread();
        IntStream.range(1, 10).forEach(
                i -> {
                    Thread t = new Thread(priorityThread);
                    t.setPriority(i);
                    t.start();
                }
        );
    }
}

运行结果:

当前线程名:Thread-0, 优先级为:1
当前线程名:Thread-5, 优先级为:6
当前线程名:Thread-7, 优先级为:8
当前线程名:Thread-6, 优先级为:7
当前线程名:Thread-4, 优先级为:5
当前线程名:Thread-1, 优先级为:2
当前线程名:Thread-3, 优先级为:4
当前线程名:Thread-2, 优先级为:3
当前线程名:Thread-8, 优先级为:9

若某个线程优先级大于线程所在线程组的最大优先级,以线程组的最大优先级为准

public static void main(String[] args) {
    ThreadGroup threadGroup = new ThreadGroup("t1");
    threadGroup.setMaxPriority(6);
    Thread thread = new Thread(threadGroup,"thread");
    thread.setPriority(9);
    System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority()); // 6
    System.out.println("我是线程的优先级"+thread.getPriority()); // 6
}

四、Java线程状态

4.1 操作系统中的线程状态转换

在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的

  • 就绪状态(Ready)
  • 执行状态(Running)
  • 等待状态(Waiting)

4.2 Java线程的6个状态

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  • NEW :新建一个线程,处于等待状态

    Thread thread = new Thread(() -> {
    });
    System.out.println(thread.getState());
  • Runnable :包含ReadyRunning两个状态。当线程调用start()方法后就进入就绪Ready态,等待操作系统分配CPU时间片,分配后进入Running运行态。当调用 yield() 方法后,允许当前线程让出CPU,但具体让不让由操作系统决定。如果让了,那么当前线程则会处于Ready态继续竞争CPU,直至执行

    thread.start();
  • Timed_waiting:指定时间让出CPU,此时线程不会执行,也不会被系统调度,直到等待时间到期后才会执行

    Object obj = new Object();
    Thread thread = new Thread(() -> {
        synchronized (obj) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    thread.start();
    
    while (true) {
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
  • Waiting :可以被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过 synchronized获得锁,调用 wait方法进入等待状态。最后通过 notify、notifyall 唤醒

    Object obj = new Object();
    Thread thread = new Thread(() -> {
        synchronized (obj) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    
    thread.start();
    
    while (true) {
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
  • Blocked:当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如 synchronized锁,先获得的先执行,没有获得的进入阻塞状态

    // 两个线程,产生锁竞争
    Object obj = new Object();
    // 该线程获取锁后休眠一段时间,不释放锁
    new Thread(() -> {
        synchronized (obj) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    
    // 该线程无法获取锁,被挂起 Blocked
    Thread thread = new Thread(() -> {
        synchronized (obj) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    
    thread.start();
    while (true) {
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
  • Terminated:这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常

    // 正常运行结束即可
    Thread thread = new Thread(() -> {
    });
    thread.start();
    
    System.out.println(thread.getState());
    System.out.println(thread.getState());
    System.out.println(thread.getState());

4.3 线程状态转换

线程状态切换

4.3.1 BLOCKED与RUNNABLE状态切换

@Test
public void blockedTest() {

    Thread a = new Thread(new Runnable() {
        @Override
        public void run() {
            testMethod();
        }
    }, "a");
    Thread b = new Thread(new Runnable() {
        @Override
        public void run() {
            testMethod();
        }
    }, "b");

    a.start();
		// 目的是为了让main线程休息下,从而让两个线程充分争夺锁
		Thread.sleep(1000L);
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出?
    System.out.println(b.getName() + ":" + b.getState()); // 输出?
}

// 同步方法争夺锁
private synchronized void testMethod() {
    try {
        Thread.sleep(2000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

4.3.2 WAITING状态与RUNNABLE状态切换

public void blockedTest() {
    ······
    a.start();
		// join()会让main线程等待a线程执行完,才继续
    a.join();
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
    System.out.println(b.getName() + ":" + b.getState()); // RUNNABLE/TERMINATED
}

4.3.3 TIMED_WAITING与RUNNABLE状态切换

public void blockedTest() {
    ······
    a.start();
		// join(long)使当前线程执行指定时间,并且使线程进入TIMED_WAITING状态
    a.join(1000L);
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_WAITING
    System.out.println(b.getName() + ":" + b.getState());
}

4.3.4 线程中断

线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理
在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己而定,可以在合适的时间处理中断请求,也可以完全不处理继续执行下去

  • Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase)
  • Thread.interrupted(): 测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新转为false
  • Thread.isInterrupted(): 测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态

五、Java线程间的通信

5.1 锁与同步

在Java中,锁的概念是基于对象的,所以又叫对象锁
那么线程与锁的关系就像是婚姻
线程持有锁(结婚)
线程释放锁(离婚),其它线程才能获取锁

线程同步就是线程间按照一定的顺序执行

无锁示例:

public class NoneLock {

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread A " + i);
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread B " + i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
}

上述代码执行的结果是无序的,每一次的结果都不一致

使用对象锁示例:

public class ObjectLock {
		// 对象锁
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread A " + i);
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread B " + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
				// 同一时间只能有一个线程持有锁,睡眠是让A先执行
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

5.2 等待/通知机制

  • Object.wait() : 进入等待,并释放锁
  • notify() : 随机叫醒一个正在等待的线程
  • notifyAll() : 叫醒所有正在等待的线程
public class WaitAndNotify {
		// 使用同一个对象锁
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadA: " + i);
													// 随机叫醒等待中的线程
                        lock.notify();
													// 进入等待,等待唤醒,释放锁
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadB: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

// 输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4

5.3 使用volatile实现信号量

volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的

示例(非线程安全的):

public class Signal {
		// 保证可见性
    private static volatile int signal = 0;

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 0) {
                    System.out.println("threadA: " + signal);
                    signal++;
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 1) {
                    System.out.println("threadB: " + signal);
                    signal = signal + 1;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

// 输出:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4

5.4 管道

基于字符的:

  • PipedWriter
  • PipedReader

基于字节流的:

  • PipedOutputStream
  • PipedInputStream
public class Pipe {
    static class ReaderThread implements Runnable {
        private PipedReader reader;

        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            System.out.println("this is reader");
            int receive = 0;
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class WriterThread implements Runnable {

        private PipedWriter writer;

        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }

        @Override
        public void run() {
            System.out.println("this is writer");
            int receive = 0;
            try {
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        writer.connect(reader); // 这里注意一定要连接,才能通信

        new Thread(new ReaderThread(reader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(writer)).start();
    }
}

// 输出:
this is reader
this is writer
test

5.5 一些方法

5.5.1 join方法

让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程

public class Join {
    static class ThreadA implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println("我是子线程,我先睡一秒");
                Thread.sleep(1000);
                System.out.println("我是子线程,我睡完了一秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA());
        thread.start();
        thread.join();
        System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
    }
}

5.5.2 sleep方法

sleep方法并不会释放锁,而wait方法会释放锁

区别:

  • wait可以指定时间,也可以不指定;而sleep必须指定时间
  • wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁
  • wait必须放在同步块或同步方法中,而sleep可以在任意位置

评论
  目录