java笔记_并发入门


以下内容为从网上摘抄整理而来,仅用于本人知识积累


多线程概述

线程是独立的执行路径;

在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程main()、gc线程(守护线程);

main()称之为主线程,为程序的总入口,用于执行整个程序;

在一个进程中,如果开辟了多个线程,线程的运行是由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不能人为干预

对同一份资源操作时,可能会存在资源抢占的问题,需要加入并发控制;

线程会带来额外开销,比如cpu调度时间,并发控制开销等;

每个线程在自己的工作内存内交互,如果内存控制不当可能导致数据不一致。


线程的基本属性

1.优先级

Thread 有个优先级字段:private int priority

操作系统采用时间片(CPU 单次执行某线程的时间)的形式来调度线程的运行,线程被 CPU 调用的时间超过它的时间片后,就会发生线程调度。

线程的优先级可以在一定程度上影响它得到时间片的多少,也就是被处理的机会。

Java 中 Thread 的优先级为从 1 到 10 逐渐提高,默认为 5。

有长耗时操作的线程,一般建议设置低优先级,确保处理器不会被独占太久;
频繁阻塞(休眠或者 I/O)的线程建议设置高优先级。

 public final static int MIN_PRIORITY = 1;

 //线程的默认优先级
 public final static int NORM_PRIORITY = 5;

 public final static int MAX_PRIORITY = 10;

线程优先级只是对操作系统分配时间片的建议。
虽然 Java 提供了 10 个优先级别,但不同的操作系统的优先级并不相同,不能很好的和 Java 的 10 个优先级别对应。
所以我们应该使用 MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

理论上来说系统会根据优先级来决定首先使哪个线程进入运行状态。当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级

另外,线程优先级具有继承特性比如 A 线程启动 B 线程,则 B 线程的优先级和 A 是一样的。线程优先级还具有随机性 也就是说线程优先级高的不一定每一次都先执行完。

Thread 类中包含的成员变量代表了线程的某些优先级。如Thread.MIN_PRIORITY(常数 1),Thread.NORM_PRIORITY(常数 5),Thread.MAX_PRIORITY(常数 10)。其中每个线程的优先级都在1到10 之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)。

一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了。

相关方法:

public final void setPriority(int newPriority) //为线程设定优先级
public final int getPriority() //获取线程的优先级

设置线程优先级方法源码:

public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    //线程游戏优先级不能小于 1 也不能大于 10,否则会抛出异常
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    //如果指定的线程优先级大于该线程所在线程组的最大优先级,那么该线程的优先级将设为线程组的最大优先级
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}

2.用户线程和守护线程

用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。

守护线程:运行在后台,为其他前台线程服务。守护线程相当于小弟,做一些后台调度、支持性工作,比如 JVM 的垃圾回收、内存管理等线程都是守护线程。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作

main 函数所在的线程就是一个用户线程,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

那么守护线程和用户线程有什么区别呢?

比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。

注意事项:

  • 1.setDaemon(true) 必须在 start() 方法前执行,否则会抛出 IllegalThreadStateException 异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。
    2.在守护线程中产生的新线程也是守护线程
    3.不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
    4.守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。

3.守护线程详解

Thread 中有个布尔值标识当前线程是否为守护线程:

private boolean daemon = false;

同时也提供了设置和查看当前线程是否为守护线程的方法:

// 设置守护线程
public final void setDaemon(boolean on) {
    checkAccess();
    if (isAlive()) {
        throw new IllegalThreadStateException();
    }
    daemon = on;
}

// 判断是否为守护线程
public final boolean isDaemon() {
    return daemon;
}

Daemon 属性需要在调用线程的 start() 方法之前调用

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。下面以一个简单的例子来表述Daemon线程的使用。

public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        //确保main线程结束前能给daemonThread能够分到时间片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

i am alive
finally block
i am alive

上面的例子中daemodThread run()方法中是一个while死循环,会一直打印,但是当main线程结束后daemonThread就会退出所以不会出现死循环的情况。main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中"finally block"操作。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并没有打印finnal块中的。

这里需要注意的是,由于上述特性,Java 虚拟机退出后,在守护线程中的 finally 块中的代码不一定执行。

举个例子:

public class DaemonTreadTest0 {
    static class DaemonThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println(Thread.currentThread().getName() + " finally is called!");
            }
        }
    }

    public static void main(String[] args) {
        DaemonThread thread = new DaemonThread();
        thread.setDaemon(true);
        thread.start();
    }
}

上述代码中将线程设置为守护线程,由于 main 线程启动 DaemonThread 后就结束,此时虚拟机中没有非守护线程,虚拟机也会退出,守护进程被终止,但是它的 finally 块中的内容却没有被调用。

如果将 setDaemon 方法注释掉,就会发现有运行结果:

Thread-0 finally is called!

因此,守护线程中 不能依靠 finally 进行资源关闭和清理。


创建线程的四种方式

1. 实现 Runnable 接口

  1. 定义Runnable接口实现类MyRunnable,并重写run()方法
  2. 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }
}
public class RunnableTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }
}

结果:

main main()方法执行完成
Thread-0 run()方法执行中...

例子:

public class ThreadTest0 {

    /**
     * 1.实现 Runnable 接口,在 run() 方法中写要执行的任务
     */
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
            	// Random.nextInt() 产生300以内的随机数
                Thread.sleep(new Random().nextInt(300));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": 您的外卖已送达");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            //2.创建一个送餐员线程,然后将任务传递给他,同时起个名
            Thread th = new Thread(new Task(), "外卖任务 " + i);
            //3.命令送餐员出发!
            th.start();
        }
    }
}

注意,上述代码中调用的是送餐员线程的 th.start() 方法,然后线程会调用 Task 对象的 run() 方法执行任务。运行结果如下:

外卖任务 3: 您的外卖已送达
外卖任务 1: 您的外卖已送达
外卖任务 0: 您的外卖已送达
外卖任务 2: 您的外卖已送达

可以看到执行任务的是各个线程。如果在 main() 方法中直接调用 run 方法,就相当于主线程直接执行任务,没有在子线程中进行。

直接在 main 中调用 run()

public static void main(String[] args) {
    for (int i = 0; i < 4; i++) {
        //2.创建一个送餐员线程,然后将任务传递给他,同时起个名
        Task task = new Task();
        Thread th = new Thread(task, "外卖任务 " + i);
        //3.直接执行任务
        task.run();
    }
}

运行结果:

main: 您的外卖已送达
main: 您的外卖已送达
main: 您的外卖已送达
main: 您的外卖已送达

2. 继承 Thread 类,重写 Run 方法

  1. 定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法就是线程要执行的业务逻辑方法
  2. 创建自定义的线程子类对象
  3. 调用子类实例的star()方法来启动线程
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
    }
}
public class TheadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread(); 	
        myThread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }
}

结果:

main main()方法执行结束
Thread-0 run()方法正在执行...

为什么直接继承 Thread 也可以在子线程中执行任务呢?
从 Thread 源码中我们可以看到, Thread 其实也实现了 Runnable :

public class Thread implements Runnable

3. 使用 CallableFuture 创建线程

实现 Callable 接口,重写 call() 方法,用 FutureTask 获得结果

  1. 创建实现Callable接口的类myCallable
  2. 以myCallable为参数创建FutureTask对象
  3. 将FutureTask作为参数创建Thread对象
  4. 调用线程对象的start()方法
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
        return 1;
    }
}
public class CallableTest {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("返回结果 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }
}

返回结果:

Thread-0 call()方法执行中...
返回结果 1
main main()方法执行完成

例子:

public class CallableTest {
    /**
     * 实现 Callable 接口
     */
    static class DeliverCallable implements Callable<String> {
        /**
         * 执行方法,相当于 Runnable 的 run, 不过可以有返回值和抛出异常
         * @return
         * @throws Exception
         */
        @Override
        public String call() throws Exception {
            Thread.sleep(new Random().nextInt(10000));
            System.out.println(Thread.currentThread().getName() + ":您的外卖已送达");
            return Thread.currentThread().getName() + " 送达时间:" + System.currentTimeMillis() + "\n";
        }
    }

    /**
     * Callable 作为参数传递给 FutureTask,FutureTask 再作为参数传递给 Thread(类似 Runnable),然后就可以在子线程执行
     * @param args
     */
    public static void main(String[] args) {
        List<FutureTask<String>> futureTasks = new ArrayList<>(4);
        for (int i = 0; i < 4; i++) {
            DeliverCallable callable = new DeliverCallable();
            FutureTask<String> futureTask = new FutureTask<>(callable);
            futureTasks.add(futureTask);

            Thread thread = new Thread(futureTask, "送餐员 " + i);
            thread.start();
        }

        StringBuilder results = new StringBuilder();
        futureTasks.forEach(futureTask -> {
            try {
                //获取线程返回结果,没返回就会阻塞
                results.append(futureTask.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        System.out.println(System.currentTimeMillis() + " 得到结果:\n" + results);
    }
}

第三种创建线程的方式与前两种的不同之处在于,以 Callable 作为任务,而不是 Runnable,这种方式的好处是可以获得结果响应中断

运行结果:

送餐员 3:您的外卖已送达
送餐员 1:您的外卖已送达
送餐员 0:您的外卖已送达
送餐员 2:您的外卖已送达
1487998155430 得到结果:
送餐员 0 送达时间:1487998155076
送餐员 1 送达时间:1487998150453
送餐员 2 送达时间:1487998155430
送餐员 3 送达时间:1487998149779

4. 使用线程池创建线程

Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

主要有 newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,四种线程池

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }
}
public class SingleThreadExecutorTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnableTest);
        }

        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }
}

结果:

线程任务开始执行
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...

线程的生命周期

在这里插入图片描述

  • 新建状态(New)
    当线程对象对创建后,即进入了新建状态,如:Thread thread= new MyThread();

  • 就绪状态(Runnable)
    当调用线程对象的start()方法(thread.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了thread.start()此线程立即就会执行;

  • 运行状态(Running)
    当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。

  • 阻塞状态(Blocked)
    处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
    ①等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
    ②同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
    ③其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  • 死亡状态(Dead)
    线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
    线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

注意:
就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

线程状态介绍备注
NEW新创建还未调用 start() 方法;还不是活着的 (alive)
READY就绪的调用了 start() ,此时线程已经准备好被执行,处于就绪队列;是活着的(alive)
RUNNING运行中线程获得 CPU 资源,正在执行任务;活着的
BLOCKED阻塞的线程阻塞于锁或者调用了 sleep;活着的
WAITING等待中线程由于某种原因等待其他线程;或者的
TIMED_WAITING超时等待与 WAITING 的区别是可以在特定时间后自动返回;活着的
TERMINATED终止执行完毕或者被其他线程杀死;不是活着的

线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

当线程执行 wait() 方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis) 方法或 wait(long millis) 方法可以将 Java 线程置于 TIMED_ WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnablerun() 方法之后将会进入到 TERMINATED(终止) 状态。

有几点注意:

  • 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
  • 四个让线程进入 WAITING 状态的方法
    • Object.wait()
    • Thread.join()
    • LockSupport.park()
    • Lock.lock()

Java 中关于“线程是否活着”的定义

Thread 中有个判断是否为活着的方法:

public final native boolean isAlive()

Java 中线程除了 NEW 和 TERMINITED 状态,其他状态下调用 isAlive() 方法均返回 true,也就是活着的。

线程的关键方法

1. Thread.sleep()

Thread.sleep() 是一个静态方法:

public static native void sleep(long millis) throws InterruptedException;

sleep() 方法:

  • 使当前所在线程进入阻塞
  • 只是让出 CPU ,并没有释放对象锁
  • 由于休眠时间结束后不一定会立即被 CPU 调度,因此线程休眠的时间可能大于传入参数
  • 如果被中断会抛出 InterruptedException

注意:
①. sleep是静态方法,它的作用是使当前所在线程阻塞。因此最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。最好在线程内部直接调用 Thread.sleep()。如果你在主线程调用某个线程的 sleep() 方法,其实阻塞的是主线程!

看下面的例子:

public class Test1 {
	public static void main(String[] args) throws InterruptedException {
		System.out.println(Thread.currentThread().getName());
		MyThread myThread=new MyThread();
		myThread.start();
		myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程
		Thread.sleep(10);
		for(int i=0;i<100;i++){
			System.out.println("main"+i);
		}
	}
}

② Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

public class Test1 {
	public static void main(String[] args) throws InterruptedException {
		new MyThread().start();
		new MyThread().start();
	}
}

class MyThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 3; i++) {
			System.out.println(this.getName()+"线程" + i + "次执行!");
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

看某一次的运行结果:

Thread-0线程0次执行!
Thread-1线程0次执行!
Thread-1线程1次执行!
Thread-0线程1次执行!
Thread-0线程2次执行!
Thread-1线程2次执行!

可以看到,线程0首先执行,然后线程1执行一次,又了执行一次。可以看到它并不是按照sleep的顺序执行的。

2. Object.wait()

Thread.sleep() 容易混淆的是 Object.wait() 方法。

Object.wait() 方法:

  • 让出 CPU,释放对象锁
  • 在调用前需要先拥有某对象的锁,所以一般需要在synchronized关键字修饰的代码块或方法中执行。
  • 使该线程进入该对象监视器的等待队列
  • 需要在synchronized关键字修饰的代码块或方法中执行。
  • 执行wait、notify、notifyAll都必须先拥有该monitor。
  • notify只能唤醒其中一个线程。
  • 多个锁的情况下,只会释放当前的锁。
  • 都属于Object类

场景:线程1持有两把锁并释放其中一把,线程2能拿到几把锁。

public class ReleaseOwnMonitor {
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resourceA) {
                System.out.println(Thread.currentThread().getName() + " 得到了resourceA锁");
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + " 得到了resourceB锁");
                    try {
                        System.out.println(Thread.currentThread().getName() + " 释放了resourceA锁");
                        resourceA.wait();
                        System.out.println(Thread.currentThread().getName() + " 后续happy");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (resourceA) {
                System.out.println(Thread.currentThread().getName() + " 表示收到了老铁释放的resourceA了");
                System.out.println(Thread.currentThread().getName() + " 心想:那么resourceB还在它手上吗,拿到了我再吱声");
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + " 我拿到resourceB啦");
                }
            }
        }).start();
    }
}

根据控制台输出,我们可以看到线程2只拿到了线程1释放的当前的锁A,锁B没有被释放导致线程2一直在等待锁的过程中。
在这里插入图片描述

四种被唤醒的情况:

  • 另一个线程调用这个对象的notify()方法,且刚好被唤醒的是本线程。
  • 另一个线程调用这个对象的notifyAll()方法。
  • 过了wait(long timeout)规定的超时时间,如果传入的是0 ,则表示永久等待。
  • 线程自身调用了interrupt()(遇到中断时,会抛出InterruptedException并释放monitor锁)。

1. notify

public class WaitNotify {
    public static Object object = new Object();

    static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + " 开始执行");
                try {
                    // 在同步代码块中执行了 wait() 方法后,释放了锁
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 获取到了monitor锁");
            }
        }
    }

    // 唤醒 Thread1
    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                object.notify();
                System.out.println(Thread.currentThread().getName() + " 调用了notify");
            }
        }
    }

    // 让Thread1先进入wait状态,再被Thread2唤醒
    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
        thread1.start();
        Thread.sleep(200);
        thread2.start();
    }
}

在这里插入图片描述

2. notify与notifyAll的区别

  • 场景:线程1和线程2被阻塞,线程3唤醒它们。
  • notifyAll :调用 notifyAll 唤醒全部线程(start的调用顺序不一定能保证线程的执行顺序,因此线程3需要等待一会儿再start)。
public class NotifyAndNotifyAll implements Runnable{
    private static final Object resource = new Object();

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new NotifyAndNotifyAll();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(() -> {
            synchronized (resource) {
                resource.notifyAll();
                System.out.println(Thread.currentThread().getName() + " 看我叫醒这两个猪");
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(200);
        thread3.start();
    }

    @Override
    public void run() {
        synchronized (resource) {
            System.out.println(Thread.currentThread().getName() + " 得到了锁");
            try {
                System.out.println(Thread.currentThread().getName() + " 困了,休息会儿");
                resource.wait();
                System.out.println(Thread.currentThread().getName() + " 醒啦,一起来happy");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

在这里插入图片描述

notify:notify只能唤醒其中一个线程。

3. sleep与wait的区别

  1. wait()方法来自于Object类,而sleep()方法来自于Thread类
//wait
public final void wait()  throws InterruptedException
//sleep
public static native void sleep(long millis) throws InterruptedException;
  1. wait() 方法必须在同步代码块中调用,否则会抛出异常IllegalMonitorStateException; 而sleep()则不会

  2. sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)

  3. wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。

  4. wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

4. Thread.join

Thread.join() 表示线程合并,调用线程会进入阻塞状态,需要等待被调用线程结束后才可以执行。

线程的合并的含义就是将几个并发执行线程的线程合并为一个单线程执行。

join方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这就像两个好基友,一个基友先走在前面突然看见另一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友赶上来后,就两人携手并进。其实线程间的这种协作方式也符合现实生活。在软件开发的过程中,从客户那里获取需求后,需要经过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进。如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供如下这些方法:

方法名详细注释备注
public final void join() throws InterruptedException等待这个线程死亡。如果任何线程中断当前线程,如果抛出InterruptedException异常时,当前线程的中断状态将被清除
public final void join(long millis) throws InterruptedException等待这个线程死亡的时间最多为millis毫秒。0的超时意味着永远等待。 如果millis为负数,抛出IllegalArgumentException异常
public final void join(long millis, int nanos) throws InterruptedException等待最多millis毫秒加上这个线程死亡的nanos纳秒。如果millis为负数或者nanos不在0-999999范围抛出IllegalArgumentException异常

Thread类除了提供join()方法外,另外还提供了超时等待的方法,如果线程threadB在等待的时间内还没有结束的话,threadA会在超时之后继续执行。join方法源码关键是:

 while (isAlive()) {
    wait(0);
 }

可以看出来当前等待对象threadA会一直阻塞,直到被等待对象threadB结束后即isAlive()返回false的时候才会结束while循环,当threadB退出时会调用notifyAll()方法通知所有的等待线程。下面用一个具体的例子来说说join方法的使用:

public class JoinDemo {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 10; i++) {
            Thread curThread = new JoinThread(previousThread);
            curThread.start();
            previousThread = curThread;
        }
    }

    static class JoinThread extends Thread {
        private Thread thread;

        public JoinThread(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
                System.out.println(thread.getName() + " terminated.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果为:

main terminated.
Thread-0 terminated.
Thread-1 terminated.
Thread-2 terminated.
Thread-3 terminated.
Thread-4 terminated.
Thread-5 terminated.
Thread-6 terminated.
Thread-7 terminated.
Thread-8 terminated.

在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程…

Thread.join 源码:

//无参方法
public final void join() throws InterruptedException {
    join(0);
}
//有参方法,表示等待 millis 毫秒后自动返回
public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
//有参方法,表示等待 millis + (nanos - 50000) 毫秒后结束
public final synchronized void join(long millis, int nanos)
throws InterruptedException {

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }

    join(millis);
}

通过源码可以发现,Thread.join 是通过 synchronized + Object.wait() 实现的。

例子:

  • join 方法的用法,如果没有调用 join 方法,两条输出语句 如厕上完出来 是紧接着输出的,而调用了 join 方法,main 线程就会等待它们执行完毕再执行。
public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 我花了 2 秒上完了厕所");
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 我花了 3 秒上完了厕所");
        });
        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName() + " 小伙伴要如厕,我在门口开始等候它们");
        thread1.join();
        thread2.join();
        System.out.println(Thread.currentThread().getName() + " 它们终于上完出来啦");
    }
}

main 小伙伴要如厕,我在门口开始等候它们
Thread-0 我花了 2 秒上完了厕所
Thread-1 我花了 3 秒上完了厕所
main 它们终于上完出来啦
  • 当主线程中断时,子线程也需要中断。
public class JoinInterrupted {
    public static void main(String[] args) {
        Thread mianThread = Thread.currentThread();
        Thread thread1 = new Thread(() -> {
            try {
                mianThread.interrupt();
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + " 我执行完毕了");
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 我也中断了");

            }
        });
        thread1.start();
        System.out.println(Thread.currentThread().getName() + " 等待子线程搞完");
        try {
            // 主线程加入 Thread1
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 被中断了");
            // 将主线程的中断传递给子线程,不然会不一致
            thread1.interrupt();
        }
        System.out.println(Thread.currentThread().getName() + " 子线程执行完毕了");
    }
}

main 等待子线程搞完
main 被中断了
main 子线程执行完毕了
Thread-0 我也中断了
  • join期间,线程是 Waiting状态。
public class JoinState {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + " 我想知道此时主线程的状态 : " + mainThread.getState());
                System.out.println(Thread.currentThread().getName() + " 运行结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        System.out.println(Thread.currentThread().getName() + " 等待子线程运行完毕");
        thread.join();
        System.out.println(Thread.currentThread().getName() + " 子线程运行完毕");
    }
}
main 等待子线程运行完毕
Thread-0 我想知道此时主线程的状态 : WAITING
Thread-0 运行结束
main 子线程运行完毕

5.Thread.yield()

Thread. yield() 也是一个静态方法:

public static native void yield();

Thread.yield() 表示暂停当前线程,让出 CPU 给优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程。

  • 和 sleep() 方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。
  • yield() 方法只是让当前线程暂停一下,重新进入就绪的线程池中。

yield() 一般使用较少。

关于sleep()方法和yield()方的区别如下:

①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

线程中断

有时候我们需要中断一个正在运行的线程,一种很容易想到的方法是在线程的 run() 方法中加一个循环条件:

public class ThreadInterruptTest1 {
    static class InterruptThread extends Thread{
        private boolean running;

        public InterruptThread(boolean running) {
            this.running = running;
        }

        public boolean isRunning() {
            return running;
        }

        public void setRunning(boolean running) {
            this.running = running;
        }

        @Override
        public void run() {
            while (running){
                System.out.println(Thread.currentThread().getName() + " is running");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptThread thread = new InterruptThread(true);
        thread.start();

        Thread.sleep(5000);
        thread.setRunning(false);
    }
}

上面的代码中线程 InterruptThread 有一个标志位 running,当这个标志位为 true 时才可以运行。
因此我们可以通过修改这个标志位为 false 来中断该线程。

其实 Thread 内部也为我们提供了同样的机制 :

方法名方法介绍
public void interrupt()试图中断调用线程,设置中断标志位为 true
public boolean isInterrupted()返回调用线程是否被中断
public static boolean interrupted()返回当前线程是否被中断的状态值,同时将中断标志位复位(设为 false)

1.public void interrupt()

它的作用是设置标志位为 true,能否达到中断调用线程的效果,还取决于该线程是否可以响应中断。

简单例子:

public class Test1 {
	public static void main(String[] args) throws InterruptedException {
		MyThread thread=new MyThread();
		thread.start();
	}
}
 
class MyThread extends Thread {
	int i=1;
	@Override
	public void run() {
		while (true) {
			System.out.println(i);
			System.out.println(this.isInterrupted());
			try {
				System.out.println("我马上去sleep了");
				Thread.sleep(2000);
				this.interrupt();
			} catch (InterruptedException e) {
				System.out.println("异常捕获了"+this.isInterrupted());
				return;
			}
			i++;
		}
	}
}

结果:

1
false
我马上去sleep了
2
true
我马上去sleep了
异常捕获了false

可以看到,首先执行第一次while循环,在第一次循环中,睡眠2秒,然后将中断状态设置为true。当进入到第二次循环的时候,中断状态就是第一次设置的true,当它再次进入sleep的时候,马上就抛出了InterruptedException异常,然后被我们捕获了。然后中断状态又被重新自动设置为false了(从最后一条输出可以看出来)。

所以,我们可以使用interrupt方法结束一个线程。具体使用如下:

public class Test1 {
	public static void main(String[] args) throws InterruptedException {
		MyThread thread=new MyThread();
		thread.start();
		Thread.sleep(3000);
		thread.interrupt();
	}
}
 
class MyThread extends Thread {
	int i=0;
	@Override
	public void run() {
		while (true) {
			System.out.println(i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				System.out.println("中断异常被捕获了");
				return;
			}
			i++;
		}
	}
}

多测试几次,会发现一般有两种执行结果:

0
1
2
中断异常被捕获了

或者

0
1
2
3
中断异常被捕获了

这两种结果恰恰说明了 只要一个线程的中断状态一旦为true,只要它进入sleep等状态,或者处于sleep状态,立马抛出InterruptedException异常。

第一种情况,是当主线程从3秒睡眠状态醒来之后,调用了子线程的interrupt方法,此时子线程正处于sleep状态,立马抛出InterruptedException异常。

第二种情况,是当主线程从3秒睡眠状态醒来之后,调用了子线程的interrupt方法,此时子线程还没有处于sleep状态。然后再第3次while循环的时候,在此进入sleep状态,立马抛出InterruptedException异常。

若线程无法响应中断,该线程并不会中断,举个例子:

public class ThreadInterruptTest2 {

    static class UnInterruptThread extends Thread{
        public UnInterruptThread(String s) {
            setName(s);
        }

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " is running!");
            }
        }
    }

    static class  UnInterruptRunnable implements Runnable{

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " is running!");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UnInterruptThread thread = new UnInterruptThread("无法中断的线程");
//        Thread thread = new Thread(new UnInterruptRunnable(), "无法中断");
        thread.start();

        //先让它执行一秒
        Thread.sleep(1000);

        thread.interrupt();

        //不立即退出
        Thread.sleep(3000);
    }
}

上面的例子在调用 thread.interrupt() 方法后仍然会继续执行。

2.public boolean isInterrupted()

我们可以通过 isInterrupted() 知道调用线程是否被中断,以此来作为线程是否运行的判断标志。

isInterrupted() 在刚创建时默认为 false 不用多说;
线程有许多方法可以响应中断(比如 Thread.sleep()Thread.wait()),这些方法在收到中断请求、抛出 InterruptedException 之前,JVM 会先把该线程的中断标志位复位,这时调用 isInterrupted 将会返回 false;
线程结束后,线程的中断标志位也会复位为 false。

举个例子:

public class ThreadInterruptTest {
    /**
     * 调用 Thread.sleep() 方法的线程,线程如果在 sleep 时被中断,会抛出 InterruptedException
     * 我们在代码中进行捕获,并且查看 JVM 是否将中断标志位重置
     */
    static class SleepThread extends Thread{
        public SleepThread(String s) {
            setName(s);
        }
        @Override
        public void run() {
            while (!isInterrupted()){
                try {
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("SleepRunner 在 sleep 时被中断了,此时中断标志位为:" + isInterrupted());
                }
            }
        }
    }

    /**
     * 希望通过这个线程了解:线程运行结束后,中断标志位会重置
     */
    static class BusyThread extends Thread{

        public BusyThread(String s) {
            setName(s);
        }

        @Override
        public void run() {
            while (!isInterrupted()){
                System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SleepThread sleepThread = new SleepThread("SleepRunner:");
        BusyThread busyThread = new BusyThread("BusyRunner:");
        //新创建的线程 中断标志为 false
        System.out.println("SleepThread 新创建时的中断标志位:" + sleepThread.isInterrupted());

        Thread.sleep(2000);
        //启动两个线程
        sleepThread.start();
        busyThread.start();
        //让它们运行一秒
        Thread.sleep(1000);
        //分别中断两个线程
        sleepThread.interrupt();
        busyThread.interrupt();
        //查看线程的中断标志位
        Thread.sleep(2000);
        System.out.println("由于中断标志位变为 true 导致运行结束的线程,中断标志位为: " + busyThread.isInterrupted());

        Thread.sleep(1000);
    }
}

上述代码中 两个线程都使用 isInterrupted 作为循环执行任务的条件,其中 SleepThread 方法调用了 Thread.sleep,这个方法的会响应中断,抛出异常。

运行结果如下:

在这里插入图片描述
可以看到:

  • 线程中,在抛出 InterruptedException 前 JVM 的确会重置中断标志位为 false
  • 这将导致以 isInterrupted 方法作为循环执行任务的线程无法正确中断

3.public static boolean interrupted()

Thread.interrupted() 方法是一个静态方法,它会返回调用线程(而不是被调用线程)的中断标志位,返回后重置中断标志位。

因此 Thread.interrupted() 第二次调用永远返回 false

源码:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

总结

每个Thread都有一个中断状状态,默认为 false。可以通过Thread对象的 isInterrupted() 方法来判断该线程的中断状态。可以通过Thread对象的 interrupt() 方法将中断状态设置为 true

当一个线程处于sleep、wait、join这三种状态之一的时候,如果此时他的中断状态为 true,那么它就会抛出一个 InterruptedException 的异常,并将中断状态重新设置为 false

问题扩展

1.为什么线程通信的方法 wait(), notify() 和 notifyAll()被定义在Object类中?而sleep()定义在Thread类?

因为 wait(), notify() 和 notifyAll()是锁级别操作,而锁是属于对象的,每一个对象的对象头中都包含几位用于保存当前锁的状态的预留,因此锁是绑定于某一个对象,而不是线程。

2.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

3.实现Runnable接口和Callable接口的区别

Runnable 自Java 1.0 以来一直存在,但 Callable 仅在Java 1.5中引入,目的就是为了来处理 Runnable 不支持的用例。Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)Executors.callable(Runnable task,Object resule))。

Runnable.java

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}

Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

4.一个Java应用程序至少有几个线程?

两个:

  • 主线程:负责main方法代码的执行,
  • 垃圾回收器线程:负责了回收垃圾。

5.请说出同步线程及线程调度相关的方法?

wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

注意: java 5 通过 Lock 接口提供了显示的锁机制,Lock 接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调

// 6. 继承thread后oop是什么意思

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值