跳至主要內容

多线程

wangdx大约 24 分钟

多线程

进程与线程

  • 进程(process)是计算机程序执行的基础单位,指的是一个程序一次动态的执行过程,它经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生发展到最终消亡的过程。多进程操作系统能同时运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的 CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好象是在“同时”运行一样

  • 虽然多进程可以提高硬件资源的利用率,但是对于进程的启动与销毁依然需要消耗大量的系统性能,导致程序的执行性能下降.所以为了进一步提升并发操作的处理能力,在进程的基础上又划分出了多线程的概念,这些线程依附于指定的进程,并且可以快速启动以及并发执行

多线程

多线程实现分析

  • Java 是一门支持多线程的编程语言,所以通过 Java 开发的项目拥有较高的处理性能,如果要想进行多线程的开发,则必须提供有一个多线程的处理类,在该处理类定义了线程要执行的功能,这样该类中所产生的若干个实例化对象就都可以并行执行

  • 在 Java 开发中,多线程的主类是实现多线程的关键,而要想定义此类则有三种方式:

    • 继承 Thread 类
    • 实现 Callable 接口
    • 实现 Runnable 接口

Thread 实现多线程

案例
/**
 * Thread实现线程
 *
 * @author wangdx
 */
public class MyThread extends Thread {
    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("【" + this.name + "】线程执行,当前x=" + i);
        }
    }
}

class Test {
    public static void main(String[] args) {
        MyThread threadA = new MyThread("yix-A");
        MyThread threadB = new MyThread("yix-B");
        MyThread threadC = new MyThread("yix-C");
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

【yix-B】线程执行,当前x=0
【yix-C】线程执行,当前x=0
【yix-A】线程执行,当前x=0
【yix-C】线程执行,当前x=1
【yix-B】线程执行,当前x=1
【yix-C】线程执行,当前x=2
【yix-A】线程执行,当前x=1
【yix-C】线程执行,当前x=3
....

Runnable 实现多线程

在 Java 语言中一个子类只允许继承一个父类,所以在多线程的实现中,如果使用了 extends 关键字去继承 Thread 父类,那么就会出现有单继承的使用局限,所以在实际的项目开发中,为了解决这种局限,也可以通过 java.lang.Runnable 接口来实现多继承

案例
/**
 * Runnable实现线程
 * @author wangdx
 */
public class MyThread implements Runnable {
    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("【" + this.name + "】线程执行,当前x=" + i);
        }
    }
}

案例
class Test {
    public static void main(String[] args) {
        MyThread threadA = new MyThread("yix-A");
        MyThread threadB = new MyThread("yix-B");
        MyThread threadC = new MyThread("yix-C");

        new Thread(threadA).start();
        new Thread(threadB).start();
        new Thread(threadC).start();
    }
}
/*
 * lambda实现多线程
 * */
class Test1 {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            String name = "yix-" + i + "线程对象";
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    System.out.println("【" + name + "】线程执行,当前index=" + j);
                }
            }).start();
        }
    }
}

Thread 与 Runnable 联系

  • 通过之前的分析已经清楚了如何通过 Thread 类以及 Runnable 接口实现多线程结构,从实现方案上来讲,使用 Runnable 接口实现的多线程可以有效的避免单继承局限。但是不管采用何种方案线程的启动只能够通过 Thread 类的 start()方法来实现,而对于 Thread 类来讲,除了可以实现线程启动之外,从定义上也与 Runnable 接口有着继承的关联。
public class Thread implements Runnable{}
  • 在实际项目中多线程编程的核心意义在于提高单位时间的业务处理性能,即:多个线程可以实现同个资源的操作,而在这样的结构中 Thread 类描述的就是每个线程对象,而并发资源就可以通过 Runnable 进行定义

案例
/**
 * 模拟网络售票
 *
 * @author wangdx
 */
public class MyThread implements Runnable {
    private int ticket = 5;

    @Override
    public void run() {
        while (this.ticket > 0) {
            if (this.ticket > 0) {
                System.out.println("【卖票】ticket=" + this.ticket--);
            } else {
                break;
            }
        }
    }
}

class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread threadA = new Thread(myThread);
        Thread threadB = new Thread(myThread);
        Thread threadC = new Thread(myThread);
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

【卖票】ticket=3
【卖票】ticket=5
【卖票】ticket=4
【卖票】ticket=1
【卖票】ticket=2

Callable 实现多线程

  • java.lang.Runnable 接口是在 JDK13.0 时所提供的线程实现接口,但是在进行 Runnable 接口设计时其线程执行方法 run() 并没有任何的返回值,这样对于线程执行后的状态获取就非常的繁琐,为了解决此类问题,从 JDK13.5 版本后提供了一个新的多线程实现接口 iava.uti.concurrent.Calable,此接定义如下:
package java.util.concurrent
@Functionallnterface
public interface Callable<V>{
    public V call() throws Exception;
}
  • 需要注意的是所有的多线程启动都需要通过 Thread 类中的 start()方法来完成,但是在 Thread 类中只允许接收 Runnable 接口实例,而如果要调用 Callable 实现的多线程,那么就需要将 Callable 封装在 FutureTask 对象实例之中,同时 FutureTask 实现了 Runnable 接口,这样就可以将此对象传递到 Thread 类的实例之中进行处理

案例
/**
 * 使用Callable实现多线程
 *
 * @author wangdx
 */
public class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        String result = "";
        for (int i = 0; i < 10; i++) {
            result += "【" + i + "】一祥集团:www.yix.com\n";
        }
        return result;
    }
}

class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        FutureTask<String> futureTask = new FutureTask<>(myThread);
        Thread threadA = new Thread(futureTask);
        Thread threadB = new Thread(futureTask);
        threadA.start();
        threadB.start();
        System.out.println(futureTask.get());
    }
}0】一祥集团:www.yix.com
【1】一祥集团:www.yix.com
【2】一祥集团:www.yix.com
 ......

多线程运行状态

  • 【创建状态】在 Java 程序里面如果要进行多线程对象创建则一定要有线程的主题类,同时要通过关键字 new 进行 Thread 类的实例化对象的创建;

  • 【就绪状态】当线程对象创建完成之后肯定需要进行线程的启动,而线程的启动肯定要通过 Thread 类的内部提供的 start()方法来完成,但是调用 start()方法的时候并不是立刻就进行多线程的执行的,而是会直接进入到就绪状态,这样就可以等待 CPU 调度执行;

  • 【运行状态】当系统为线程分配了相关的硬件资源之后,所有的线程将按照其定义的核心业务的功能进于执行,但是这个执行并不是一直执行的,而是需要进行资源的抢占,运行一段时间之后都会由系统产个阳寒时间,使当前线程暂停执行,这样该线程就会让出当前 CPU 的资源,并重新进入到调度队列等待被重新调度;

  • 【阻塞状态】当某一个线程对象让出了当前的资源之后,那么该线程对象将进入到一种阻塞状态(此时线程之中未完成的代码暂时不执行了),其他的线程继续完成自己先前未完成的任务,除了这种自动的系统级控制之外,也可以利用 Thread 类之中的一些线程的控制方法来进行线程的阻塞操作;

  • 【终止状态】当线程的方法全部执行完毕之后,该线程就将释放掉所占用的全部资源,并且结束全部的执行

线程命名和取得

在 Java 中每一个线程的启动以及运行都是由 CPU 调度完成的,所以对于一个线程的主体操作方法“run()”也会有不同的线程对象分别调用,而对于 run()方法来讲可以获得的仅仅是当前的线程对象,所以每一个线程只能够通过名称实现唯一的标记

案例
public class Test {

    /*
     * 获取当前线程名称
     * */
    public static void main(String[] args) {
        Runnable threadBody = () -> {
            System.out.println("【线程名称】" + Thread.currentThread().getName());
        };
        new Thread(threadBody, "yix-A").start();
        new Thread(threadBody).start();
        new Thread(threadBody, "yix-B").start();
        new Thread(threadBody).start();
    }
}

【线程名称】yix-B
【线程名称】yix-A
【线程名称】Thread-1
【线程名称】Thread-0
class Test2 {
    /*
     * 获取主线程
     * */
    public static void main(String[] args) {
        Runnable threadBody = () -> {
            System.out.println("【线程名称】" + Thread.currentThread().getName());
        };
        new Thread(threadBody, "yix-A").start();
        threadBody.run();
    }
}

【线程名称】yix-A
【线程名称】main
  • 在一个操作系统中有可能会根据不同的业务需要产生若干个 JVM 进程,每一个 JVM 进程之间都是完全独立的,所以里面的线程都无法直接访问。

线程休眠

案例
public class MyThread {
    /*
    * 观察线程休眠
    * */
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("【" + Thread.currentThread().getName() + "】num = " + j);
                }
            }, "yix-" + i).start();
        }
    }
}

【yix-2】num = 0
【yix-7】num = 0
【yix-1】num = 0
【yix-9】num = 0
....

线程中断

案例
class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("【" + Thread.currentThread().getName() + "】准备休眠5秒");
                Thread.sleep(5000);
                System.out.println("【" + Thread.currentThread().getName() + "】休眠好了");
            } catch (InterruptedException e) {
                System.out.println("【" + Thread.currentThread().getName() + "】休眠异常,无法休眠");
            }
        }, "休眠线程");
        thread.start();
        System.out.println("【中断状态】"+thread.isInterrupted());
        Thread.sleep(2000);
        thread.interrupt();
        System.out.println("【中断状态】"+thread.isInterrupted());
    }
}

【中断状态】false
【休眠线程】准备休眠5秒
【中断状态】true
【休眠线程】休眠异常,无法休眠

线程强制执行

案例
class Test3 {
    public static void main(String[] args) throws InterruptedException {
        /*
         * 线程强制执行
         * */
        Thread mainThread = Thread.currentThread();
        Thread joinThread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                try {
                    Thread.sleep(100);
                    if (i >= 10) {
                        mainThread.join();
                    }
                    System.out.println("【" + Thread.currentThread().getName() + "】子线程执行i=" + i);
                } catch (InterruptedException e) {
                }
            }
        }, "工作线程");
        joinThread.start();
        for (int i = 0; i < 30; i++) {
            Thread.sleep(100);
            System.out.println("【" + Thread.currentThread().getName() + "】主线程执行i=" + i);
        }
    }
}

线程礼让

  • 在多线程的执行过程之中,所有的线程肯定要轮流进行 CPU 资源的抢占,那么既然有这样的抢占处理,每当有一个线程抢占到了资源之后,可以通过一种礼让的形式让出当前抢占的资源,如果要想实现这种礼让操作可以使用 yield()方法完成,此方法定义如下:
案例
class Test4 {
    public static void main(String[] args) throws InterruptedException {
        /*
         * 线程礼让
         * */
        Thread joinThread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                try {
                    if (i % 2 == 0) {
                        Thread.yield();
                        System.out.println("线程礼让执行");
                    }
                    Thread.sleep(100);
                    System.out.println("【" + Thread.currentThread().getName() + "】子线程执行i=" + i);
                } catch (InterruptedException e) {
                }
            }
        }, "工作线程");
        joinThread.start();
        for (int i = 0; i < 30; i++) {
            Thread.sleep(100);
            System.out.println("【" + Thread.currentThread().getName() + "】主线程执行i=" + i);
        }
    }
}

线程优先级

每一个程序中的线程如果要执行,则一定要进行 CPU 资源竞争,而如果现在希望某一个线程在每次资源竞争时都可以优先获取到资源时,就可以设置不同的线程优先级

案例
class Test5 {
    /*
     * 设置线程优先级
     * */
    public static void main(String[] args) {
        Thread thread[] = new Thread[3];
        for (int i = 0; i < thread.length; i++) {
            thread[i] = new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(200);
                        System.out.println("【" + Thread.currentThread().getName() + "】线程执行,线程优先级:" + Thread.currentThread().getPriority());
                    } catch (InterruptedException e) {
                    }
                }
            });
        }
        thread[0].setPriority(Thread.MIN_PRIORITY);
        thread[2].setPriority(Thread.MIN_PRIORITY);
        thread[1].setPriority(Thread.MAX_PRIORITY);
        for (int i = 0; i < thread.length; i++) {
            thread[i].start();
        }
    }
}

多线程同步

多线程同步问题引出

Java 通过线程并行处理机制,可以有效的提高程序的处理性能,在传统的主线程应用中一个程序在处理某些资源时会由主方法完成,但是这样的处理速度一定会比较慢。但是如果采用了多线程的处理机制,利用主线程创建出许多的子线程,可以通过多个资源起完成同一资源的操作

案例
class Test6 {
    public static int ticket = 5;
    /*
    * 多线程售票
    * */
    public static void main(String[] args) {
        Runnable body = () -> {
            while (true) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("【" + Thread.currentThread().getName() + "】售票,当前剩余票数:" + (--ticket));
                } else {
                    break;
                }
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(body).start();
        }
    }
}Thread-0】售票,当前剩余票数:3Thread-1】售票,当前剩余票数:4Thread-3】售票,当前剩余票数:4Thread-2】售票,当前剩余票数:1Thread-4】售票,当前剩余票数:2Thread-4】售票,当前剩余票数:-1Thread-0】售票,当前剩余票数:0Thread-3】售票,当前剩余票数:-2Thread-2】售票,当前剩余票数:0Thread-1】售票,当前剩余票数:-3

线程同步处理

  • 在多线程并发资源处理时,如果要想保证数据计算的准确性,则应该加入同步的处理机制,所谓的同步指的就是将若干个操作程序逻辑单元设置为一个整体,并且保证每一次只允许有一个线程在执行这些单元,,不管整个整体单元执行多久只要当前存在有执行线程,则其他线程都不允许执行此部分代码,这就相当于为整个单元设置了一个执行锁如果锁处于关闭状态,则其他线程将全部处于阻塞状态,一直到线程锁重新处于开启状态,则可以有新的执行线程进入

同步代码块

  • 同步代码块是使用 synchronized 定义的一个代码块,在使用同步代码块的时候需要提供有一个同步对象实现的锁定处理,实现语法如下:
synchronized(同步对象){}
案例
class Test7 {
    public static int ticket = 5;

    /*
     * 同步代码块多线程售票
     * */
    public static void main(String[] args) {
        Test7 test7 = new Test7();
        Runnable body = () -> {
            while (true) {
                synchronized (test7) {
                    if (ticket > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                        }
                        System.out.println("【" + Thread.currentThread().getName() + "】售票,当前剩余票数:" + (--ticket));
                    } else {
                        break;
                    }
                }
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(body, "售票员-" + i).start();
        }
    }
}
【售票员-0】售票,当前剩余票数:4
【售票员-0】售票,当前剩余票数:3
【售票员-0】售票,当前剩余票数:2
【售票员-0】售票,当前剩余票数:1
【售票员-0】售票,当前剩余票数:0

同步方法

将若干个程序处理单元封装在一个方法中,随后在方法定义时使用 synchronized 关键字,这样在该方法调用时每次只允许一个线程进入执行,同步方法定义如下:

[public|protected|private] [static][final][synchronized] 返回值类型 方法名称(参数列表)[throws 异常]{
    [return 返回值]
}
案例
class Test8 {
    public static int ticket = 5;

    /*
     * 同步方法
     * */
    public static void main(String[] args) {
        Test7 test7 = new Test7();
        Runnable body = () -> {
            while (sale()) {
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(body, "售票员-" + i).start();
        }
    }

    public static synchronized boolean sale() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            System.out.println("【" + Thread.currentThread().getName() + "】售票,当前剩余票数:" + (--ticket));
            return true;
        } else {
            return false;
        }
    }
}

线程死锁

  • 同步的核心机制在于,一个线程要等待另外一个线程执行完成后才可以执行所需要的操作,虽然在整个同步处理过程中,可以通过同步实现共享资源的正确处理,但是过多的同步也有可能会产生问题。
  • 例如:张三想借王八的图书,那么张三对王八说:先借我你的书看,我可以借你我的画看,而王八也对张三说:先借我你的画看,我就借你我的书看,此时张三和王八两人都在等着各自的回复,最终执行的结果也就可想而知,张三借不到王八的书,王八也借不到张三的画,这实际上就属于死锁的状态
案例
public class Book {
}

class Paint {
}

class Test {
    public static void main(String[] args) {
        Book book = new Book();
        Paint paint = new Paint();
        Thread threadBook = new Thread(() -> {
            synchronized (paint) {
                System.out.println("张三对王八说:先借我书,我再借你图画,否则不借");
                try {
                    Thread.sleep(1000);
                    synchronized (book) {
                        Thread.sleep(1000);
                        System.out.println("张三借到书,过上幸福生活");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread threadPaint = new Thread(() -> {
            synchronized (book) {
                System.out.println("王八对张三说:先借我图画,我再借你书,否则不借");
                try {
                    Thread.sleep(1000);
                    synchronized (paint) {
                        Thread.sleep(1000);
                        System.out.println("王八借到图画,过上幸福生活");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        threadPaint.start();
        threadBook.start();
    }
}

王八对张三说:先借我图画,我再借你书,否则不借
张三对王八说:先借我书,我再借你图画,否则不借
程序进入死锁状态

生产者与消费者

生产者与消费者模型简介

  • 在多线程设计开发中,除了可以使用多线程技术提高程序的处理性能之外,而后最重要的就是进行两个线程对象之间的状态通讯操作,例如:现在假设有两个程序线程,一个完线程负责数据的生产,另外一个线程负责数据的消费,每当生产者线程生产完成一个完整的数据内容后,则消费者线程取走该数据

  • 生产者与消费者线程彼此之间依靠一块公共区域进行通讯连接,生产者所生产完成的数据内容要保存在该区域内,而后消费者要通过该区域取走数据,这样两个线程之间就存在有如下的协同处理关系:

    • 生产者需要生成一组完整的数据,如果数据没有全部生产完成,则消费线程不能取走该数据信息;
    • 如果现在生产者线程的生产性能很强,而消费者线程的消费性能较弱,那么当完整的数据生产完成后,生产者线程应该等待消费者线程将数据取走后,再生产下一组完整的数据项;- 如果现在消费者线程性能很强,并且发现没有新的数据被生产出来时,则消费者线程需要等待生产者线程将数据生产完整后再取走。

生产者与消费者基础模型

  • 为了更好的理解生产者与消费者模型的实现机制,下面将通过一个基本的开发模型为读者演示这一操作,由于生产者和消费者属于两个不同的线程类型,所以这两个类之间可以依靠同一个 Message 类的对象实例实现操作协同,如图所示,而为了简化信息生产的复杂度,本次的生产者将循环生产两组信息,而消费者也将循环取走这两组信息信息的具体内容定义如下

    • 第一组:title="沐言科技"、content="www.yootk.com"

    • 第二组:title ="李兴华高薪就业编程训练营"、content="edu.yootk.com"

案例
public class Message {
    private String title;
    private String content;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

class ProducerThread implements Runnable {
    private Message message;

    public ProducerThread(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            try {
                if (i % 2 == 0) {
                    this.message.setTitle("李兴华编程训练营");
                    Thread.sleep(100);
                    this.message.setContent("edu.yootk.com");
                } else {
                    this.message.setTitle("沐言科技");
                    Thread.sleep(100);
                    this.message.setContent("www.yootk.com");
                }
            } catch (InterruptedException e) {
            }
        }
    }
}

class ConsumerThread implements Runnable {
    private Message message;

    public ConsumerThread(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("【消费者】title=" + this.message.getTitle() + "、content=" + this.message.getContent());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
        }
    }
}

class Test {
    public static void main(String[] args) {
        Message message = new Message();
        new Thread(new ProducerThread(message)).start();
        new Thread(new ConsumerThread(message)).start();
    }
}

【消费者】title=李兴华编程训练营、content=null
【消费者】title=沐言科技、content=edu.yootk.com
【消费者】title=李兴华编程训练营、content=www.yootk.com
【消费者】title=沐言科技、content=edu.yootk.com
【消费者】title=李兴华编程训练营、content=www.yootk.com

解决数据同步问题

如果要想解决生产者中生产数据的同步问题,就需要将 title 与 content 两个属性的设置操作部分放在一个同步方法或者同步代码块之中,当消费者线程消费数据时,就需要等待生产者线程将数据生产完成后再取出,这样就可以保证最终操作数据的完整性

案例
public class Message {
    private String title;
    private String content;

    public synchronized void set(String title, String content) {
        this.title = title;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        this.content = content;
    }

    public synchronized String get() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
        }
        return "title=" + this.title + "、content=" + this.content;
    }
}

class ProducerThread implements Runnable {
    private Message message;

    public ProducerThread(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if (i % 2 == 0) {
                this.message.set("李兴华编程训练营", "edu.yootk.com");
            } else {
                this.message.set("沐言科技", "www.yootk.com");
            }
        }
    }
}

class ConsumerThread implements Runnable {
    private Message message;

    public ConsumerThread(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("【消费者】" + this.message.get());
        }
    }
}

class Test {
    public static void main(String[] args) {
        Message message = new Message();
        new Thread(new ProducerThread(message)).start();
        new Thread(new ConsumerThread(message)).start();
    }
}

【消费者】title=李兴华编程训练营、content=edu.yootk.com
【消费者】title=沐言科技、content=www.yootk.com
【消费者】title=李兴华编程训练营、content=edu.yootk.com
【消费者】title=沐言科技、content=www.yootk.com
【消费者】title=李兴华编程训练营、content=edu.yootk.com
【消费者】title=沐言科技、content=www.yootk.com
【消费者】title=李兴华编程训练营、content=edu.yootk.com
【消费者】title=沐言科技、content=www.yootk.com
【消费者】title=李兴华编程训练营、content=edu.yootk.com
【消费者】title=沐言科技、content=www.yootk.com
【消费者】title=沐言科技、content=www.yootk.com

解决线程重复操作问题

项目中引入同步处理机制后,虽然可以实现生产数据的完整性操作,但是却出现了线程协调处理的问题,本次程序的核心功能在于:每当生产完成一个数据之后,消费者才可以取走,而在消费者未取走之前,生产者是无法生产的,必须一直等待消费者消费后才可以继续生产。同理,消费者在生产者未生产完新数据后,则需要一直等待生产者生产数据。

案例
public class Message {
    private String title;
    private String content;
    private boolean flag = true;

    public synchronized void set(String title, String content) {
        if (this.flag == false) {
            try {
                super.wait();
            } catch (InterruptedException e) {
            }
        }
        this.title = title;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        this.content = content;
        this.flag = false;
        super.notify();
    }

    public synchronized String get() {
        if (this.flag == true) {
            try {
                super.wait();
            } catch (InterruptedException e) {
            }
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
        }
        this.flag = true;
        super.notify();
        return "title=" + this.title + "、content=" + this.content;
    }
}

优雅的停止线程

  • 在多线程编程开发中,所有的线程控制的处理操作只需要依靠 Thread 类提供的方法即可实现,但是在早期的 Thread 类中提供了线程的暂停与停止处理方法,遗憾的是这些方法已经在 JDK 1.2 版本中被废弃了。

  • 之所以这些方法会被废弃,主要的原因在于早期的多线程技术较为简单,但是随着软件多线程的运行模式也在发生着改变,所以这些线程的处理方法有可和硬件技术的发展,能产生死锁问题

  • 对于现在的多线程开发来讲,如果要想实现一个线程的停止处理操作,那么就可以借助一些逻辑的处理方式来完成,例如:可以在程序中添加一个停止的标志位,而后在每次执行线程操作方法前都进行该标志位的判断,如果满足判断条件则结束线程的执行否则则继续执行该线程

案例
public class Message implements Runnable {
    private boolean stopFlag = false;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            if (this.stopFlag) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            System.out.println("【Message消息数出】i=" + i);
        }
    }

    public void stop() {
        this.stopFlag = true;
    }
}

class Test {
    public static void main(String[] args) throws InterruptedException {
        Message message = new Message();
        new Thread(message).start();
        Thread.sleep(2000);
        message.stop();
    }
}

守护线程

守护线程是一种运行在后台的程序线程,并且其都有“宿主”线程,当“宿主”线程消失后,对应的守护线程也将消失

案例
public class Message implements Runnable {
    public Message() {
        Thread daemonThread = new Thread(() -> {
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                }
                System.out.println("【守护线程】");
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            System.out.println("【消息输出】");
        }
    }
}

class Test {
    public static void main(String[] args) {
        Message message = new Message();
        new Thread(message).start();
    }
}

【守护线程】
【消息输出】
【守护线程】

volatile 关键字

在传统多线程开发中,每当需要通过线程进行公共资源中的数据操作时,都会直接通过主内存读取所需要的数据信息的副本,而后在该线程的工作内存中进行数据的加载、使用、赋值以及存储操作,而后再将存储的信息写会到主内存之中,这样一来就会因为这种副本数据的读取与写入机制而造成数据无法及时同步的问题,为了解决这样的线程副本操作,所以在 Java 中提供了一个 volatile 关键字,该关键字主要用于属性定义中,并且可以直接进行主内存中的数据操作。

案例
public class TicketThread implements Runnable {
    private volatile int ticket = 5;

    @Override
    public void run() {
        while (this.sale()) {
            ;
        }
    }

    public synchronized boolean sale() {
        if (this.ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            System.out.println("【" + Thread.currentThread().getName() + "】售票,剩余票数:" + (--this.ticket));
            return true;
        }
        return false;
    }
}

class Test {
    public static void main(String[] args) {
        TicketThread thread = new TicketThread();
        new Thread(thread, "售票员-A").start();
        new Thread(thread, "售票员-B").start();
        new Thread(thread, "售票员-C").start();
    }
}

面试题

加减

设计 4 个线程对象,两个线程执行减操作,两个线程执行加操作。

案例
package com.yix.thread.w13;

/**
 * @author wangdx
 */
public class Resource {
    private int num = 0; //要操作的数字
    private boolean flag;//设置一个标识

    // flag = true:表示可以进行加法操作,但是不允许进行减法操作
    // flag = false:表示可以进行减法操作,但是不允许进行加法操作
    public synchronized void add() {
        while (flag == false) {
            try {
                super.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        this.num++;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("【" + Thread.currentThread().getName() + "】加法后num=" + this.num);
        this.flag = false; //表示加结束
        this.notifyAll();
    }

    public synchronized void sub() {
        while (flag == true) {
            try {
                super.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        this.num--;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("【" + Thread.currentThread().getName() + "】加法后num=" + this.num);
        this.flag = true; //表示减结束
        this.notifyAll();
    }
}

class Test {
    public static void main(String[] args) {
        Resource resource = new Resource();
        for (int x = 0; x < 2; x++) {
            new Thread(() -> {
                for (int y = 0; y < 50; y++) {
                    synchronized (resource) {
                        resource.add();
                    }
                }

            }, "加法线程-" + x).start();
        }
        for (int x = 0; x < 2; x++) {
            new Thread(() -> {
                for (int y = 0; y < 50; y++) {
                    synchronized (resource) {
                        resource.sub();
                    }
                }
            }, "减法线程-" + x).start();
        }
    }
}
【减法线程-0】加法后num=-1
【加法线程-0】加法后num=0
【减法线程-0】加法后num=-1
【加法线程-0】加法后num=0

电脑生产

设计一个生产电脑和搬运电脑类,要求生产出一台电脑就搬走一台电脑,如果没有新的电脑生产出来,则搬运工要等待新电脑产出;如果生产出的电脑没有搬走,则要等待电脑搬走之后再生产,并统计出生产的电脑数量。

案例
public class Resource {
    private int num = 0; //统计电脑生产数量
    private volatile Computer computer; //描述生产和取走电脑

    public Resource() {
        Thread numDaemonThread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("【后台线程】当前电脑成产数量:" + this.num);
            }
        });
        numDaemonThread.setDaemon(true); //设置为后台线程
        numDaemonThread.start();  //启动后台线程
    }

    //生产电脑
    public synchronized void create(String brand, double price) {
        if (this.computer != null) { //电脑存在,没有拿走
            try {
                super.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.computer = new Computer(brand, price);
        this.num++;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("【" + Thread.currentThread().getName() + "】电脑成产完成:" + this.computer);
        super.notify();
    }

    public synchronized void get() {
        if (this.computer == null) { //等待电脑成产
            try {
                super.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("【" + Thread.currentThread().getName() + "】搬运电脑:" + this.computer);
        this.computer = null;
        super.notify();
    }
}

class Computer {
    private String brand;
    private double price;

    public Computer(String brand, double price) {
        this.brand = brand;
        this.price = price;
    }

    @Override
    public String toString() {
        return "电脑品牌=" + brand + "、电脑价格=" + price;
    }
}

class Test1 {
    public static void main(String[] args) {
        Resource resource = new Resource();
        new Thread(() -> {
            for (int y = 0; y < 50; y++) {
                if (y % 2 == 0) {
                    resource.create("外星人-" + y, 29999.99);
                } else {
                    resource.create("MacBook-" + y, 19999.99);
                }
            }
        }, "成产商").start();
        new Thread(() -> {
            for (int y = 0; y < 50; y++) {
                resource.get();
            }
        }, "搬运工").start();
    }
}

竞拍抢答

实现一个竞拍抢答程序:要求设置三个抢答者(三个线程),而后同时发出抢答指令,抢答成功者给出成功提示,未抢答成功者给出失败提示。

案例
public class AnswerThread implements Callable {
    private boolean flag;

    @Override
    public String call() throws Exception {
        Thread.sleep(1000); //抢答需要时间延迟
        synchronized (this) {  //多个线程同时进入,需要依次锁定
            if (this.flag == false) {
                this.flag = true;
                return "【" + Thread.currentThread().getName() + "】恭喜,抢答成功!!!";
            } else {
                return "【" + Thread.currentThread().getName() + "】遗憾,抢答失败!!!";
            }
        }
    }
}

class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        AnswerThread answerThread = new AnswerThread();

        FutureTask<String> taskA = new FutureTask<>(answerThread);
        FutureTask<String> taskB = new FutureTask<>(answerThread);
        FutureTask<String> taskC = new FutureTask<>(answerThread);

        new Thread(taskA,"抢答者A").start();
        new Thread(taskB,"抢答者B").start();
        new Thread(taskC,"抢答者C").start();
        System.out.println(taskA.get());
        System.out.println(taskB.get());
        System.out.println(taskC.get());
    }
}
上次编辑于: