JavaSE学习总结(十九)多线程/线程和进程的区别/创建线程/线程池/停止线程/sleep和wait的区别/礼让/Join()/线程状态/守护线程/线程同步和锁/死锁/Lock/管程法/信号灯法

一、程序、进程和线程

(一)概念

程序是一组指令的集合,它是静态的实体,没有执行的含义。
进程是一个动态的实体,有自己的生命周期。一般说来,一个进程肯定与一个程序相对应,并且只有一个,但是一个程序可以有多个进程,或者一个进程都没有(因为它没有执行)。除此之外,进程还有并发性和交往性。
简单地说,进程是程序的一部分,程序运行的时候会产生进程。
线程是进程中的一个实体,作为系统调度和分派的基本单位。
一个程序至少有一个进程,一个进程至少有一个线程
多进程: 在操作系统中能同时运行多个任务(程序)。
多线程: 在同一应用程序中有多个顺序流同时执行。

(二)线程和进程的区别

  1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。

  2. 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。同一进程中线程的切换不会引起进程切换,从而避免了昂贵的系统调用,但是在由一个进程中的线程切换到另一进程中的线程,依然会引起进程切换。

  3. 线程和进程最根本的区别在于:进程是资源分配的单位,线程是CPU调度和分派的基本单位。

  4. 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源,另外还包括一点必不可少的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。

二、多线程

举个例子,以前的公路是一条路,慢慢因为车太多了,道路堵塞,效率极低。 为了提高使用的效率,能够充分利用道路,于是加了多个车道。 从此,妈妈再也不用担心道路堵塞了。这就类似多线程。
在这里插入图片描述
注意:

  1. 很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
  2. main() 称之为主线程,是系统的入口,用于执行整个程序。
  3. 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为地干预的。
  4. 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
  5. 线程会带来额外的开销,如cpu调度时间、并发控制开销。
  6. 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

(一)Thread类

即线程类,它继承自java.lang.object,实现了Runnable接口

1.构造方法摘要

Thread()分配新的 Thread 对象。
Thread(Runnable target)根据Runnable的子实现类分配新的 Thread 对象。
Thread(Runnable target, String name)根据Runnable的子实现类及给定的名称分配新的 Thread 对象。
Thread(String name)根据给定的名称分配新的 Thread 对象。

2.常用方法

void run()Thread 的子类应该重写该方法。 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
static Thread currentThread()返回对当前正在执行的线程对象的引用。
String getName()返回该线程的名称。
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
void setPriority(int newPriority) 更改线程的优先级
int getPriority()返回线程的优先级。
Thread.State getState()返回该线程的状态。
void join() 等待该线程终止(插队
static void yield() 暂停当前正在执行的线程对象,并执行其他线程(礼让
void interrupt() 中断线程
boolean isAlive() 测试线程是否处于活动状态

(二)线程五大状态

在这里插入图片描述

(三)线程创建

在这里插入图片描述

  1. 继承Thread类
    将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

  2. 实现Runnale接口
    声明实现 Runnable 接口的类。该类应实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。

  3. 实现Callable接口
    (1)实现Callable接口,需要返回值类型
    (2)重写call方法,需要抛出异常
    (3)创建目标对象
    (4)创建执行服务
    (5)提交执行
    (6)获取结果
    (7)关闭服务

1.第一种创建方式(Thread)

第一步:自定义继承Thread类的线程类
第二步:重写run()方法,编写线程执行体
第三步:创建线程对象 ,调用start()方法启动线程。

案例演示1

public class TestThread extends Thread{
    //run()方法线程体
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我一边听歌"+i);
        }
    }

    //主线程
    public static void main(String[] args) {
        //创建线程对象
        TestThread testThread = new TestThread();
        //调用start()方法,用来启动线程
        testThread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("我一边写作业"+i);
        }
    }
}

在这里插入图片描述
通过运行结果可以看到,两个线程的输出内容是交替输出的,而不是先输出完一个线程的再输出另外一个线程,因为两个线程是同时进行的。

案例演示2
实现多线程同时下载网图
首先,我们要下载一个commons-io-2.6的jar包,里面包含根据地址下载文件的工具类。下载好后复制粘贴到我们新建的项目的文件夹lib里:
在这里插入图片描述
再右键lib,点击Add as library
在这里插入图片描述

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;

//定义一个下载器类
class Downloader{
    //静态方法
    public static void downloader(String url,String name){
        try {
            //别人写好的工具类里的下载方法:将给定地址的图片保存到指定文件中
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class DownloadPicture extends Thread{
    private String url;
    private String name;
    public DownloadPicture(String url,String name){
        this.url=url;
        this.name=name;
    }
    @Override
    public void run() {
        Downloader.downloader(url,name);
        System.out.println(name+"下载完成");
    }

    public static void main(String[] args) {
        DownloadPicture d1 = new DownloadPicture("https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", "1.jpg");
        DownloadPicture d2 = new DownloadPicture("https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF", "2.jpg");
        DownloadPicture d3 = new DownloadPicture("https://t7.baidu.com/it/u=1956604245,3662848045&fm=193&f=GIF", "3.jpg");
        //启动线程
        d1.start();
        d2.start();
        d3.start();
    }
}

会发现结果的顺序和我们代码里start()方法的执行顺序不太相同,因为三个线程是同时进行的。
在这里插入图片描述

2.第二种创建方式(Runnable)

1.自定义类实现Runnable接口
2.重写run()方法,编写线程执行体
3.创建线程对象。启动线程:需要创建一个线程Thread对象(类似代理),然后把Runnable接口实现类的对象丢到Thread的构造参数里,调用start()启动

案例演示1

public class TestThread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我一边听歌"+i);
        }
    }

    public static void main(String[] args) {
        TestThread1 t1 = new TestThread1();
//        Thread thread = new Thread(t1);
//        thread.start();
        new Thread(t1).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("我一边写作业"+i);
        }
    }
}

在这里插入图片描述
其实这里的新建Thread对象就类似一个静态代理,下面举一个静态代理模式的例子:

//静态代理
public class StaticProxy {
    public static void main(String[] args) {
        Person person = new Person();
        new WeddingCompany(person).happyMarry();
        //new Thread(t).start();一个意思
    }
}

//共同的接口:结婚
interface Marry{
    void happyMarry();
}

//真实对象:要结婚的人
class Person implements Marry{
    @Override
    public void happyMarry() {
        System.out.println("结婚");
    }
}

//代理对象:婚庆公司
class WeddingCompany implements Marry{
    //婚庆需要有这个人,代理对象需要代理一个真实对象
    private Person person;

    public WeddingCompany(Person person) {
        this.person = person;
    }

    @Override
    public void happyMarry() {
        before();
        person.happyMarry();
        after();
    }
    private void before() {
        System.out.println("布置结婚现场,举办婚礼");
    }
    private void after() {
        System.out.println("送客,送还婚纱");
    }
}

在这里插入图片描述

案例演示2
模拟网上抢票

public class UnsafeBuyTicket implements Runnable {
    private int ticket=10;
    @Override
    public void run() {
        while(ticket>0){
            //Thread.currentThread().getName()获取当前线程名字
            System.out.println(Thread.currentThread().getName()+"抢到了第"+(11-ticket)+"张票");
            ticket--;
        }
    }

    public static void main(String[] args) {
        UnsafeBuyTicket b = new UnsafeBuyTicket();
        //Thread第二个参数:创建线程名字
        new Thread(b,"小明").start();
        new Thread(b,"老师").start();
        new Thread(b,"黄牛党").start();
    }
}

在这里插入图片描述
通过结果我们可以看到,抢到的票有重复的,这个就是并发问题,线程不安全,学到后面的线程同步再解决。

案例演示3
模拟龟兔赛跑

public class Race implements Runnable {
    //winner:只有一个胜利者,因此用static修饰
    private static String winner;

    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            //模拟兔子半路偷懒睡觉
            if(Thread.currentThread().getName().equals("兔子")&&i%30==0){//兔子每跑30米睡一觉
                try {
                    Thread.sleep(10);//一次睡10ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //判断是否结束
            boolean flag = isGameOver(i);
            if(flag){//结束的话另外一名选手就不用再跑了
                break;
            }
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"米");
        }
    }
    private boolean isGameOver(int length){
        if (winner!=null){//如果赢家已经诞生,另外一名选手不用再跑了
            return true;
        }
        if (length>=1000){//如果跑到了1000米
            winner=Thread.currentThread().getName();
            System.out.println("比赛结束,赢家是"+Thread.currentThread().getName());
            return true;
        }else {
            return false;
        }
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }
}

在这里插入图片描述
小结

  • 继承Thread类
    子类继承Thread类具备多线程能力
    启动线程:子类对象. start()
    不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    实现接口Runnable具有多线程能力
    启动线程:传入目标对象+Thread对象.start()
    推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
线程池
  • 背景:经常创建和销毁线程、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。因此可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。 可以避免频繁创建销毁、实现重复利用。
  • 好处
  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理(…)
    corePoolSize:核心池的大小
    maximumPoolSize:最大线程数
    keepAliveTime:线程没有任务时最多保持多长时间后会终止
使用方法

JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
  • Future submit(Callable task):执行任务,有返回值,一般用来执行 Callable(下面讲)
  • void shutdown() :关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

因此便有了另外一种使用Runnable创建线程的方法:

案例演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
public class TestPool {
    public static void main(String[] args) {
        //1.创建服务,创建线程池
        //newFixedThreadPool()参数为池子大小
        ExecutorService service= Executors.newFixedThreadPool(4);

        //2.执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //3.关闭连接
        service.shutdown();
    }
}

在这里插入图片描述

3.第三种创建方式(Callable)(了解)

(1)方式1
  1. 实现Callable接口,这里需要一个泛型返回值
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService service = Executors.newFixedThreadPool(1);
  5. 提交执行:Future<Boolean> result1 = service.submit(t1);
  6. 获取结果:boolean r1 = result1.get();
  7. 关闭服务:service.shutdownNow();

案例演示
下载网图

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

//定义一个下载器类
class Downloader{
    //静态方法
    public static void downloader(String url,String name){
        try {
            //别人写好的工具类里的下载方法:将给定地址的图片保存到指定文件中
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

//第三种方法的好处:可以定义返回值 可以抛出异常
public class DownloadPicture implements Callable<Boolean> {
    private String url;
    private String name;

    public DownloadPicture(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public Boolean call() throws Exception {
        Downloader.downloader(url,name);
        System.out.println(name+"下载完成");
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        DownloadPicture d1 = new DownloadPicture("https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", "1.jpg");
        DownloadPicture d2 = new DownloadPicture("https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF", "2.jpg");
        DownloadPicture d3 = new DownloadPicture("https://t7.baidu.com/it/u=1956604245,3662848045&fm=193&f=GIF", "3.jpg");
        //创建执行服务
        ExecutorService service = Executors.newFixedThreadPool(3);//后面讲这几个类
        //提交执行
        Future<Boolean> result1 = service.submit(d1);
        Future<Boolean> result2 = service.submit(d2);
        Future<Boolean> result3 = service.submit(d3);
        //获取结果
        boolean r1 = result1.get();
        boolean r2 = result2.get();
        boolean r3 = result3.get();
        //关闭服务
        service.shutdownNow();
    }
}
(2)方式2

通过实现Callable接口的方式,可以创建一个线程,需要重写其中的call方法。启动线程时,需要新建一个Callable的实例,再用FutureTask实例包装它,最终,再包装成Thread实例,调用start方法启动,并且,可以通过FutureTask的get方法来获取返回值。
在形式上,相比于Runnable接口,Callable方法多了一个泛型的返回值和抛出了一个异常。
案例演示

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//创建线程的方式:Callable
class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return 1024;
    }
}

public class CallableDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //用Callable的方式创建线程,相比较于Runnable,可以获取返回的值
        MyThread thread = new MyThread();
        //需要一个FutureTask,而且在两个线程里传入同一个futureTask,只会执行一次
        FutureTask<Integer> futureTask = new FutureTask<>(thread);

        new Thread(futureTask,"A").start();
        new Thread(futureTask,"AA").start();

        //想要执行两次,就需要再来一个不一样的FutureTask
        FutureTask futureTask1 = new FutureTask(thread);
        new Thread(futureTask1,"B").start();

        System.out.println(futureTask.get());
    }
}

在这里插入图片描述

(四)线程停止

不推荐使用JDK提供的 stop()、 destroy()方法,已废弃

推荐使用一个标志位 flag 控制运行或停止,当flag=false,则终止线程运行

案例演示

public class Test implements Runnable{
    //设置一个标志位
    private boolean flag=true;

    @Override
    public void run() {
        int i=0;
        while(flag){
            System.out.println("run...thread"+i++);
        }
    }
    //设置一个公开的方法停止线程,更改标志位的值
    public void stop(){
        flag=false;
        System.out.println("线程停止");
    }

    public static void main(String[] args) {
        Test test = new Test();
        new Thread(test).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("run...main"+i);
            if (i==900){//当主线程运行到900时,让线程停止
                test.stop();
            }
        }
    }
}

在这里插入图片描述

(五)线程休眠

  • sleep (时间) 指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时
  • 每一个对象都有一个锁,sleep不会释放锁

案例演示
模拟倒计时

import java.text.SimpleDateFormat;
import java.util.Date;

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        Date date = new Date(System.currentTimeMillis());
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);//1000ms=1s
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
            date=new Date(System.currentTimeMillis());
        }
    }
}

在这里插入图片描述

(六)线程礼让

通过Thread中的yield()方法实现礼让:
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。
通俗的讲,礼让就是让cpu重新调度,礼让不一定成功!看CPU心情

案例演示

class TestThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

public class MyTest {
    public static void main(String[] args){
        TestThread t = new TestThread();
        new Thread(t,"a").start();
        new Thread(t,"b").start();
    }
}

第一次运行,礼让不成功:
在这里插入图片描述
第二次运行,礼让成功(b线程未结束,a线程就执行了):
在这里插入图片描述

(七)Join()

Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞(可以理解为插队

public class TestJoin implements Runnable{
    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 500; i++) {
            if (i==1){
                //强制执行,让thread插队进来,让主线程阻塞
                thread.join();
            }
            System.out.println("我是主线程:"+i);
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("我是要插队的线程:"+i);
        }
    }
}

在这里插入图片描述
插队的线程执行完毕主线程才能开始执行

(八)线程状态观测

通过JDK帮助文档,输入Thread.State可看到线程的几种状态(静态字段):

线程状态。线程可以处于下列状态之一:

NEW
至今尚未启动的线程处于这种状态。
RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。
BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。
WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
TERMINATED
已退出的线程处于这种状态。

在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。

案例演示
线程状态观测

public class TestState{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(1000);//阻塞5秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程执行完毕");
            }
        });
        //创建线程后,观测状态
        Thread.State state = thread.getState();
        System.out.println(state);
        //线程启动后,观测状态
        thread.start();
        state = thread.getState();
        System.out.println(state);
        //观测线程终止前的状态
        while(state!=Thread.State.TERMINATED){
            Thread.sleep(1000);
            state = thread.getState();
            System.out.println(state);
        }
        //thread.start();报错,因为线程一旦进入死亡状态,不能再次启动
    }
}

在这里插入图片描述

(九)线程优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行(但并不是优先级高的线程就一定会比优先级低的线程先执行,只是优先执行的权重/概率高一点,主要还是看CPU的调度

优先级为1-10,如果超过这个范围则报错

几个优先级静态字段:
static int MAX_PRIORITY线程可以具有的最高优先级10。
static int MIN_PRIORITY线程可以具有的最低优先级1。
static int NORM_PRIORITY分配给线程的默认优先级5。

设置优先级和获取优先级的方法:
void setPriority(int newPriority)更改线程的优先级。
int getPriority()返回线程的优先级。

案例演示

class TestThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行了,优先级:"+Thread.currentThread().getPriority());
    }
}
public class Test {
    public static void main(String[] args) {
  		//main线程优先级是默认优先级5
        System.out.println(Thread.currentThread().getName()+"执行了,优先级:"+Thread.currentThread().getPriority());
        TestThread t = new TestThread();
        Thread t1 = new Thread(t, "1");
        Thread t2 = new Thread(t, "2");
        Thread t3 = new Thread(t, "3");
        Thread t4 = new Thread(t, "4");
        Thread t5 = new Thread(t, "5");
        Thread t6 = new Thread(t, "6");

        t1.start();
        //先设置优先级,再启动
        t2.setPriority(4);
        t2.start();

        t3.setPriority(2);
        t3.start();
        
        t4.setPriority(Thread.MAX_PRIORITY);
        t4.start();

        t5.setPriority(8);
        t5.start();

        t6.setPriority(7);
        t6.start();
    }
}

在这里插入图片描述
可以看到,并不是优先级高的线程就一定会比优先级低的线程先执行

(十)守护线程

  • 在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

  • 通俗地讲,任何一个守护线程都是整个JVM中所有非守护线程的保姆:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

  • Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

  • User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。

  • 值得一提的是,守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程:void setDaemon(boolean on)将该线程标记为守护线程(true)或用户线程(false)

  • 当线程只剩下守护线程的时候,JVM就会退出,并不会等待守护线程执行完毕;如果还有其他的任意一个用户线程还在,JVM就不会退出。

  • thread.setDaemon(true)必须在thread.start()之前设置

  • 在Daemon线程中产生的新线程也是Daemon的。

  • 不要认为所有的应用都可以设置成Daemon来进行服务,比如读写操作或者计算逻辑。

  • 守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务(它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。)

案例演示
上帝守护你

class Person implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 36500; i++) {
            System.out.println("开心地活着第"+i+"天");
        }
    }
}
class God implements Runnable{
    @Override
    public void run() {
        for (long i = 0; i < 9999999L; i++) {
            System.out.println("上帝守护你"+i);
        }
    }
}
public class MyTest {
    public static void main(String[] args) {
        God god = new God();
        Person person = new Person();
        Thread t1 = new Thread(god);
        t1.setDaemon(true);//设置上帝为守护线程
        t1.start();
        new Thread(person).start();
    }
}

在这里插入图片描述

在这里插入图片描述
通过结果我们可以看出,用户线程已经执行完毕,而守护线程没执行完毕,JVM就退出了。

(十一)线程同步

现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭 , 最天然的解决办法就是排队,一个个来。

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,但通常会遇到线程不安全的问题,这时候我们就需要线程同步线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 。当一个线程获得对象的排它锁,那么它独占资源,其他线程必须等待,使用后释放锁即可。(就比如有一个公共卫生间,我进去了,把门锁上,后面的人只能等我出来,把锁开了,才能使用卫生间)

锁机制虽然安全,但也存在以下问题 :

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

保证线程安全可以通过“队列”+“锁”来完成。

1.三大线程不安全案例

案例演示1
抢票

public class UnsafeBuyTicket implements Runnable {
    //票数
    private int ticketNums = 10;
    //标志位
    private boolean flag = true;

    @Override
    public void run() {
        //买票
        while (flag) {
            buyTicket();
        }
    }

    public void buyTicket() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        //模拟网络延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票");
    }
    
    public static void main(String[] args) {
        UnsafeBuyTicket b = new UnsafeBuyTicket();
        new Thread(b,"小明").start();
        new Thread(b,"老师").start();
        new Thread(b,"黄牛党").start();
    }
}

在这里插入图片描述

结果出现了同一张票,被几个人拿到的情况,线程不安全。

案例演示2
银行取款

public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"结婚基金");

        Bank you = new Bank("你",account,50);
        Bank wife = new Bank("老婆",account,100);

        you.start();
        wife.start();
    }
}

//账户
class Account{
    int money;//余额
    String name; //卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//银行
class Bank extends Thread{
    Account account;  //账户
    int drawingMoney; //要取的钱
    int nowMoney; //手里有多少钱

    public Bank(String name,Account account,int drawingMoney){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        //判断能否取钱
        if (account.money<drawingMoney){
            System.out.println(this.getName()+"想取"+this.drawingMoney+",但余额不足");
            return;
        }

        //为了放大问题发生性,我们加个延时.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //余额 = 余额 - 你取走的钱
        account.money = account.money - drawingMoney;
        //手里的钱 = 手里的钱 + 你取的钱
        nowMoney = drawingMoney + nowMoney;

        System.out.println(this.account.name+"账户余额:"+account.money);
        System.out.println(this.getName()+"手里的钱:"+nowMoney);
    }
}

在这里插入图片描述
结果发现,余额本身只有100元,但两人取出来的钱共为150,线程不安全

案例演示3
线程不安全的集合

import java.util.ArrayList;

public class UnsafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {//一万条线程同时启动
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

在这里插入图片描述
结果并不是10000,这是因为这么多线程同时启动,可能某一瞬间两个线程的 list.add()同时运行, 将两个线程的名字存到了同一个位置,形成了覆盖,因此list的长度就会少。

2.同步方法及同步块

那么我们该如何让线程变得安全呢?我们可以运用同步方法或同步块解决问题。

(1)同步方法

由于我们已经可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需要再针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和synchronized 块

同步方法 : public synchronized void method(int args) {}
synchronized方法控制对 “对象” 的访问,每个对象对应一把锁,每个 synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,被 synchronized修饰的方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

缺陷 : 若将一个大的方法用synchronized修饰将会影响效率(方法里面需要修改的内容才需要锁,锁的太多,浪费资源)

案例演示1
抢票

public class SafeBuyTicket implements Runnable {
    private int ticket=10;

    @Override
    public synchronized void run() {//synchronized同步方法,锁的是this,即这个对象本身,或者是class
        while(ticket>0){
            System.out.println(Thread.currentThread().getName()+"抢到了第"+(11-ticket)+"张票");
            ticket--;
        }
    }

    public static void main(String[] args) {
        SafeBuyTicket b = new SafeBuyTicket();

        new Thread(b,"小明").start();
        new Thread(b,"老师").start();
        new Thread(b,"黄牛党").start();
    }
}

在这里插入图片描述
每张票都只被抢到一次,线程安全

(2)同步块

同步块 : synchronized (Obj ) { }

  • Obj 称之为同步监视器
  • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器

同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class (后篇反射中讲解)

同步块同步监视器的执行过程

  1. 第一个线程访问,锁定同步监视器,执行其中代码
  2. 第二个线程访问,发现同步监视器被锁定,无法访问
  3. 第一个线程访问完毕,解锁同步监视器
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

案例演示2
银行取钱

public class SafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"结婚基金");

        Bank you = new Bank("你",account,50);
        Bank wife = new Bank("老婆",account,100);

        you.start();
        wife.start();
    }
}

//账户
class Account{
    int money;//余额
    String name; //卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//银行
class Bank extends Thread{
    Account account;  //账户
    int drawingMoney; //要取的钱
    int nowMoney; //手里有多少钱

    public Bank(String name,Account account,int drawingMoney){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        synchronized(account){//synchronized同步块
            //判断能否取钱
            if (account.money<drawingMoney){
                System.out.println(this.getName()+"想取"+this.drawingMoney+",但余额不足");
                return;
            }

            //为了放大问题发生性,我们加个延时.
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //余额 = 余额 - 你取走的钱
            account.money = account.money - drawingMoney;
            //手里的钱 = 手里的钱 + 你取的钱
            nowMoney = drawingMoney + nowMoney;

            System.out.println(this.account.name+"账户余额:"+account.money);
            System.out.println(this.getName()+"手里的钱:"+nowMoney);
        }
    }
}

在这里插入图片描述

这个案例没有用同步方法解决,是因为如果将synchronized关键字加到run方法前面,而run方法的this是Bank类的对象,那么锁的是Bank类的对象,而实际上共享资源(或是说想要增删改的量)是Account类的对象中的余额,因此我们可以用同步块实现同步,将Account的对象account作为同步监视器放到同步块中,监视account。

注意:锁的是要增删改的量

案例演示3
集合

import java.util.ArrayList;

public class SafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {//一万条线程同时启动
            new Thread(()->{
                synchronized (list){//synchronized同步块
                    list.add(Thread.currentThread().getName());
                }

            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

在这里插入图片描述
扩充
线程安全的集合CopyOnWriteArrayList
本身就线程安全,不需要运用同步方法和同步块

import java.util.concurrent.CopyOnWriteArrayList;

public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

在这里插入图片描述

(十二)死锁

多个线程各自占有一些共享资源,并且互相等待其他线程使用完占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。(比如,有两个小孩A和B,小孩A手里拿着玩具汽车,小孩B手里拿着玩具飞机,A拿着玩具汽车不放,并且想要玩具飞机,B同样拿着玩具飞机不放,想要玩具汽车,于是他们谁都没办法同时玩两种玩具)某一个同步块同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题。

产生死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。

上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。

案例演示

class Lipstick{//口红

}
class Mirror{//镜子

}
class Girl extends Thread{//化妆的女孩
	//需要的资源只有一份,用static来修饰
    static Lipstick lipstick=new Lipstick();
    static Mirror mirror=new Mirror();
    private String name;//女孩名字
    private int choice;//选择

    public Girl(String name, int choice) {
        this.name = name;
        this.choice = choice;
    }

    @Override
    public void run() {
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void makeUp() throws InterruptedException {
        if(choice==0){//第一个姑娘先拿起了唯一的口红,过了一秒她抱着口红不放且想用唯一的镜子
            synchronized (lipstick){
                System.out.println(this.name+"拿到了口红");
                Thread.sleep(1000);
                synchronized (mirror){
                    System.out.println(this.name+"拿到了镜子");
                }
            }
        }else{//第二个姑娘先拿起了唯一的镜子,过了两秒她抱着镜子不放且想用唯一的口红
            synchronized (mirror){
                System.out.println(this.name+"拿到了镜子");
                Thread.sleep(2000);
                synchronized (lipstick){
                    System.out.println(this.name+"拿到了口红");
                }
            }
        }
    }
}
public class MakeUp {
    public static void main(String[] args) {
        Girl g1 = new Girl("小红", 0);
        Girl g2 = new Girl("小芳", 1);
        g1.start();
        g2.start();
    }
}

在这里插入图片描述
程序卡住,并没停止,因为两个线程都还在等待对方各自的资源,这就是死锁。

解决办法:将嵌套着的synchronized同步块取出来,也即意味着女孩一次只占用一个资源,用完再用下一个资源。

class Lipstick{//口红

}
class Mirror{//镜子

}
class Girl extends Thread{//化妆的女孩
	//需要的资源只有一份,用static来修饰
    static Lipstick lipstick=new Lipstick();
    static Mirror mirror=new Mirror();
    private String name;
    private int choice;

    public Girl(String name, int choice) {
        this.name = name;
        this.choice = choice;
    }

    @Override
    public void run() {
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void makeUp() throws InterruptedException {
        if(choice==0){//第一个姑娘先拿起了唯一的口红,过了一秒她想用唯一的镜子
            synchronized (lipstick){
                System.out.println(this.name+"拿到了口红");
                Thread.sleep(1000);
            }
            synchronized (mirror){
                System.out.println(this.name+"拿到了镜子");
            }
        }else{//第二个姑娘先拿起了唯一的镜子,过了两秒她想用唯一的口红
            synchronized (mirror){
                System.out.println(this.name+"拿到了镜子");
                Thread.sleep(2000);
            }
            synchronized (lipstick){
                System.out.println(this.name+"拿到了口红");
            }
        }
    }
}
public class MakeUp {
    public static void main(String[] args) {
        Girl g1 = new Girl("小红", 0);
        Girl g2 = new Girl("小芳", 1);
        g1.start();
        g2.start();
    }
}

在这里插入图片描述

(十三)Lock锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock的对象充当

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

  • ReentrantLock(可重入锁) 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

格式

class A{
	private final ReentrantLock lock = new ReenTrantLock(); 
	public void m(){ 
		lock.lock(); 
		try{ 
			//保证线程安全的代码; 
		} finally{
			 lock.unlock(); 
		 } 
	 } 
 }

synchronized 与 Lock 的区别

  • Lock是显式锁(需要手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
  • Synchronized 如果线程 1获得锁,那么线程2会阻塞并等待,一直傻傻的等;Lock锁就不一定会等待下去
  • Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,是否公平可以
    自己设置
  • Synchronized 适合锁少量的同步代码;Lock 适合锁大量的同步代码!

优先使用顺序
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

案例演示
抢票

import java.util.concurrent.locks.ReentrantLock;

public class UnsafeBuyTicket implements Runnable {
    private int ticket=10;
    private final ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        lock.lock();
        try {
            while(ticket>0){
                System.out.println(Thread.currentThread().getName()+"抢到了第"+(11-ticket)+"张票");
                ticket--;
            }
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        UnsafeBuyTicket b = new UnsafeBuyTicket();

        new Thread(b,"小明").start();
        new Thread(b,"老师").start();
        new Thread(b,"黄牛党").start();
    }
}

在这里插入图片描述

(十四)线程通信

1.生产者/消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费

  • 如果仓库中没有产品,则生产者将产品放入仓库;如果有产品则停止生产并等待,直到仓库中的产品被消费者取走为止

  • 如果仓库中放有产品 ,则消费者可以将产品取走消费 ;如果没有产品则停止消费并等待,直到仓库中再次放入产品为止

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件

  • 对于生产者,没有生产产品之前,消费者要等待;而生产了产品之后,又需要马上通知消费者消费

  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费

  • 在生产者消费者问题中,仅有synchronized是不够的

    • synchronized 可阻止并发更新同一个共享资源,实现了同步
    • synchronized 不能用来实现不同线程之间的消息传递 (通信)

Java提供了几个方法解决线程之间的通信问题:

方法名作用
wait()表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

注意 : 均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

wait和sleep的区别

wait()sleep()
同步只能在同步方法或者同步代码块中调用wait()方法,否则抛出IllegalMonitorStateException异常不需要在同步方法或同步块中调用
作用对象wait()方法定义在Object类中,作用于对象本身sleep()方法定义在java.lang.Thread中,作用于当前线程
是否释放锁资源
唤醒条件超时或其他线程调用对象的notify()或者notifyAll()方法超时或者调用interrupt()方法
方法属性wait()是实例方法sleep()是静态方法
  • sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。

  • wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait()方法结束,执行后面的代码;如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait()方法一旦过了超时时间,并不需要其他线程执行notify()也能自动解除阻塞,但是如果没设置超时时间的wait()方法必须等待其他线程执行notify()

  • 在调用sleep()方法的过程中,线程不会释放对象锁;而当调用wait()方法的时候,该线程会放弃对象锁,只有针对此对象调用notify()方法后本线程才进入此对象的等待锁定池准备,其他线程释放该锁后该线程获得该锁并运行后面的代码。

  • 执行wait()方法时,后续的代码不再执行,会保存当前上下文,也就是当前时间点 CPU 寄存器和程序计数器的内容,下次获取到锁后(超时或被唤醒),会从保存的上下文加载当时的状态,继续执行wait()方法后续的代码,这其实就是上下文切换

解决生产者/消费者问题有两种方式:管程法信号灯法

2.管程法

利用缓冲区
在这里插入图片描述

  • 生产者 : 负责生产数据的模块 (可能是方法,对象,线程,进程) ;
  • 消费者 : 负责处理数据的模块 (可能是方法,对象,线程,进程) ;
  • 缓冲区 : 消费者不能直接使用生产者的数据,他们之间有个 “缓冲区”,生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

案例演示

class Producer extends Thread{//生产者
    public Container container;
    public Producer(Container container){
        this.container=container;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.push(new Product(i));
            System.out.println("生产者生产了第"+i+"个产品");
        }
    }
}
class Consumer extends Thread{//消费者
    public Container container;
    public Consumer(Container container){
        this.container=container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Product p = container.pop();
            System.out.println("消费者消费了第"+p.id+"个产品");
        }
    }
}
class Product{//产品
    public int id;

    public Product(int id) {
        this.id = id;
    }
}
class Container{//缓冲区
    public int count=0;//缓冲区的计数器
    Product[] products=new Product[10];//假设缓冲区大小为10
    //定义一个生产者的生产方法
    public synchronized void push(Product p){//注意:wait、notify、notifyAll只能在同步方法或者同步代码块中使用
        if(count==products.length){//如果缓冲区满了,生产者停止生产,等待消费者消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果缓冲区没满,那么将产品放进缓冲区中,并通知消费者消费
        products[count]=p;
        count++;
        this.notifyAll();
    }
    public synchronized Product pop()  {//注意:wait、notify、notifyAll只能在同步方法或者同步代码块中使用
        if(count==0){//如果缓冲区空了,消费者停止消费,等待生产者生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果缓冲区不为空,消费者从缓冲区取走产品,消费后通知生产者生产
        count--;
        Product p=products[count];
        this.notifyAll();
        return p;
    }
}
//测试生产者和消费者问题
public class TestPC {
    public static void main(String[] args) {
        Container container = new Container();
        new Producer(container).start();
        new Consumer(container).start();
    }
}

在这里插入图片描述

3.信号灯法

通过切换boolean值(信号灯)来决定生产者和消费者线程的运行,决定是等待还是唤醒

class Player extends Thread{
    public Movie movie;
    public Player(Movie movie){
        this.movie=movie;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%2==0){
                movie.play("王牌特工");
            }else{
                movie.play("王牌特工2");
            }
        }
    }
}
class Audience extends Thread{
    public Movie movie;
    public Audience(Movie movie){
        this.movie=movie;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            movie.watch();
        }
    }
}
class Movie{
    public String name;//电影名称

    //设true 表示 -->生产者生产,消费者等待(演员拍戏,观众等待上映)
    //设false 表示 -->消费者消费,生产者等待(观众观看,演员等待片酬)
    boolean flag=true;//设置一个标志位(信号灯)

    public synchronized void play(String name){//注意:wait、notify、notifyAll只能在同步方法或者同步代码块中使用
        if(!flag){//flag==false 演员等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员拍完了"+name);
        this.name=name;
        //演员拍完戏,宣传电影,通知观众观看
        this.notifyAll();
        this.flag=!this.flag;//切换信号为false,让观众观看
    }
    public synchronized void watch(){//注意:wait、notify、notifyAll只能在同步方法或者同步代码块中使用
        if(flag){//flag==true 观众等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众观看了"+name);
        //观众观看完,通知演员多拍一部
        this.notifyAll();
        this.flag=!this.flag;//切换信号为true,让演员拍戏
    }
}
//测试信号灯法
public class TestPC {
    public static void main(String[] args) {
        Movie movie = new Movie();
        new Player(movie).start();
        new Audience(movie).start();
    }
}

在这里插入图片描述

(十五)interrupt()

interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。 更确切的说,如果线程被wait()、join()和sleep()三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将提早地终结被阻塞状态,并抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常)。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait()、sleep()或join()时,才会马上抛出 InterruptedException。

注意:

  • 当线程A执行到wait()、sleep()、join()时,抛出InterruptedException后,中断状态会被系统复位,因此这个时候线程A调用Thread.interrupted()返回的是false。

  • 如果线程被调用了interrupt(),此时该线程并不在wait()、sleep()、join()时,下次执行wait()、sleep()、join()时,一样会抛出InterruptedException,当然抛出后该线程的中断状态也会被系统复位。

总结:调用interrupt()方法,立刻改变的是中断状态,但如果不是在阻塞态,就不会抛出异常;如果在进入阻塞态后,中断状态为已中断,就会立刻抛出异常

  1. sleep()和interrupt()
    假设线程a正在使用sleep()暂停着: Thread.sleep(100000),如果要取消它的等待状态,可以在正在执行的线程里(比如这里是b)调用a.interrupt(),令线程a放弃睡眠操作。即,在线程b中执行a.interrupt(),处于阻塞中的线程a将放弃睡眠操作。
    当在sleep中时线程被调用interrupt()时,就会放弃暂停的状态并马上抛出InterruptedException。抛出异常的是a线程。

  2. wait()和interrupt()
    线程a调用了wait()进入了等待状态,也可以用interrupt()取消。不过这时候要注意的问题。线程调用了wait(),会释放锁并进入该对象的锁的等待区,当对等待中的线程调用interrupt()时,会先重新获取锁,再抛出异常。在获取锁之前,是无法抛出异常的。(不是马上抛出异常)

  3. join()和interrupt()
    当线程以join()等待其他线程结束时,当它被调用interrupt(),它与sleep()一样,会马上抛出异常
    注意,调用的interrupt()方法,一定是调用被阻塞线程的interrupt()方法。如在线程b中调用线程a.interrupt()。

写在后面

1. Java真的可以开启线程吗?

开不了
start()方法其实是调用了本地方法private native void start0();(底层的C++代码),Java 无法直接操作硬件

2.公司中的多线程开发,需要尽量降低耦合性

线程就是一个单独的资源类,没有任何附属的操作(不继承Thread,也不实现Runnable)

案例演示

//Test类既不继承Thread,也不实现Runnable接口
//这样做的好处是降低耦合性
class Test{
    public int ticket=10;
    public synchronized void buyTicket(){
        while(ticket>0){
            try {
                Thread.sleep(1000);//模拟延时,放大问题发生性
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"买了第"+ticket--+"张票");
        }
    }
}
public class MyTest {
    public static void main(String[] args) {
        Test test = new Test();
        //不继承Thread,也不实现Runnable接口一样可以创建线程
        //把test的方法丢到Runnable接口的匿名内部类的run方法里即可
        new Thread(()->test.buyTicket(),"小明").start();
        new Thread(()->test.buyTicket(),"老师").start();
        new Thread(()->test.buyTicket(),"黄牛党").start();
    }
}

在这里插入图片描述

3.为什么生产者/消费者问题中没有else?

案例演示

class Shop{
    public int num=0;
    public synchronized void add(){//店员进货
        if(num!=0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num++;
            System.out.println(Thread.currentThread().getName()+"进货,货物余量:"+num);
            this.notifyAll();
        }
    }
    public synchronized void buy(){//客人买货
        if(num==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num--;
            System.out.println(Thread.currentThread().getName()+"买货,货物余量:"+num);
            this.notifyAll();
        }
    }
}
public class MyTest{
    public static void main(String[] args) {
        Shop shop = new Shop();
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep(200);//放大问题发生性
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                shop.add();
            }

        },"店员").start();

        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                shop.buy();
            }
        },"客人").start();
    }
}

然而结果是控制台并没有停止,而是一直在等待。由于生产者睡眠了200毫秒,因而可能产生的情况是消费者线程先走完,而生产者线程由于wait()没有线程来唤醒,所以最终导致一直等待,因而程序不会结束,控制台就不终止。所以我们需要去掉else,那么无论最终哪个线程先走完,都会执行wait后面的方法,即它在结束前会唤醒等待的线程,因而这个线程最终也会完整的执行完,最后程序终止。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值