多线程机制

一.多线程的基本使用

1.1 线程的创建和使用

        进程是资源分配的基本单位,所有与该进程有关的资源,如打印机,输入的缓冲队列等都被记录在进程控制块中,以表示该进程拥有这些资源或正在使用它们。与进程相对应,线程是进程内一个相对独立的、可调度的执行单元。线程属于某一个进程,并与进程内的其他线程一起共享进程的资源。

        一个正在运行的程序对于操作系统而言称为进程。程序和进程的关系可以理解为,程序是一段静态的代码,是应用程序执行的蓝本,而进程是指一个正在内存中运行的程序,并且有独立的地址空间。

        线程和进程一样拥有独立的执行路径,二者的区别在于,线程存在于进程中,拥有独立的执行堆栈和程序计数器,但没有独立的存储空间。一个线程会和所属进程中的其他线程共享存储空间。        

        线程的三大优势:

系统开销小

创建和撤销线程的系统开销,以及多个线程之间的切换,都比使用进程进行相同操作要小的多。

方便通信和资源共享

如果是在进程之间通信,往往要求系统内核的参与,以提供通信机制和保护机制。而线程间通信在同一进程的地址空间内,共享主存和文件,操作简单,无须系统内核参与。

简化程序结构

用户在实现多任务的程序时,采用多线程机制实现,程序结构清晰,独立性强

线程是相对独立的、可调度的执行单元,因此在线程的运行过程中,会分别处于不同的状态。

通常而言,线程主要有下列 5 种状态。

新建状态:创建一个新的子线程。
就绪状态:线程已经具备运行的条件,等待调度程序分配 CPU 资源给这个线程运行。
运行状态:调度程序分配 CPU 资源给该线程,该线程正在执行。
阻塞状态:线程正等待除了 CPU 资源以外的某个条件符合或某个事件发生。
死亡状态:又可称为终止状态,表示线程体操作已经完成结束

定义线程类的方式——继承 Thread 类和实现 Runnable 接口。

Thread 类和 Runnable 接口都是在 java.lang 包中,无须导入,直接可以使用。

public class 类名 extends Thread{
    //属性
    //其他方法
    public void run() { // 重写 Thread 类中的 run() 方法
        //线程需要执行的核心代码
    }
}
public class 类名 implements Runnable{
    //属性
    //其他方法
    public void run() { // 实现 Runnable 接口中的 run() 方法
        //线程需要执行的核心代码
    }
}

和继承 Thread 类非常类似,实现 Runnable 接口的线程类也需要编写 run() 方法,将线程的核心代码置于该方法中。但是 Runnable 接口中仅仅定义了 run() 这么一个方法,因此还必须将 Runnable 对象转换为 Thread 对象,从而使用 Thread 类中的线程 API

主要区别

由于Java是单继承,一个类继承Thread类以后不能继承其他类,扩展性不好
而实现Runnable接口则可以侧面实现了多继承

使用Runnable创建对象,在主函数中需如下操作

Runnable实现类名 对象名 = new  Runnable实现类名();
Thread 线程对象名 = new Thread(对象名);
/**
 * 创建子线程类和启动子线程
 */
public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new MyThread1();
        MyThread2 mt2 = new MyThread2();
        Thread t2 = new Thread(mt2);
        // 启动子线程
        t1.run();
        t2.run();
    }
}

//继承自 Thread 类创建线程类
class MyThread1 extends Thread {

    //无参构造方法,调用父类构造方法设置线程名称
    public MyThread1() {
        super("我的线程1");
    }

    @Override
    public void run() {
        //通过循环判断,输出20次
        for (int i = 0; i < 20; i++) {
            System.out.println(this.getName() + "运行第" + (i + 1) + "次");
        }

    }
}

//实现Runnable接口创建线程类
class MyThread2 implements Runnable {
    String name = "我的线程2";

    @Override
    public void run() {
        //通过循环判断,输出20次
        for (int i = 0; i < 20; i++) {
            System.out.println(this.name + "运行第" + (i + 1) + "次");
        }
    }
}

1.2 查看 JVM 中的线程名

        在 Java 虚拟机( JVM )中,除了用户创建的线程,还有服务于用户线程的其他线程,它们根据用途被分配到不同的组中进行管理。

ThreadGroup 介绍

        ThreadGroup 字面意思是线程组,也就是一组线程。当然也可以理解为是一个线程的集合。此外,线程组也可以包含其他线程组。线程组构成一棵树,在树中,除了初始线程组外,每个线程组都有一个父线程组。允许线程访问有关自己的线程组的信息,但是不允许它访问有关其线程组的父线程组或其他任何线程组的信息。

提供了两个构造方法:

public ThreadGroup(String name)

创建一个名为 name 的新线程组。

public ThreadGroup(ThreadGroup parent, String name)

创建一个名为 name 的新线程组,并同步指定父线程组。

常用方法如下表所示: 

 ThreadGroup 的使用

        要获取 JVM 中已存在的 ThreadGroup 线程组,我们可以通过 Thread 类中的方法来获取当前线程组。

public static Thread currentThread()

静态方法,用来获取当前线程对象。

public final ThreadGroup getThreadGroup()

返回此线程所属的线程组。如果此线程已死亡处于终止状态,则此方法返回 null。

        在 TestJVMThreadGroup 类中包含了 4 个方法:getRootThreadGroups() 方法用于获得根线程组,gerThreads() 方法用于获得指定线程组中所有线程的名称,getThreadGroups() 方法用于获得线程组中所有子线程组,main() 方法用于测试。

import java.util.ArrayList;
import java.util.List;

/**
 * 获取 JVM 中已经存在的线程组
 */
public class TestJVMThreadGroup {
    // 获得根线程组
    private static ThreadGroup getRootThreadGroups(){
        // 获得当前线程组
        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
        while(true){
            // 如果 getParent() 返回值非空,则不是根线程组
            if(rootGroup.getParent() != null){
                // 获得父线程组
                rootGroup = rootGroup.getParent();
            }else{
                // 如果到达根线程组,则退出循环
                break;
            }
        }
        // 返回根线程组
        return rootGroup;
    }
    // 获得给定线程组中所有线程名
    public static List<String> getThreads(ThreadGroup group){
        // 创建保存线程名的列表
        List<String> threadList = new ArrayList<String>();
        // 根据活动线程数创建线程数组
        Thread[] threads = new Thread[group.activeCount()];
        //复制线程到线程数组
        int count = group.enumerate(threads, false);
        // 遍历线程数组将线程名及其所在组保存到列表中
        for (int i = 0; i<count; i++) {
            threadList.add(group.getName() + "线程组:" + threads[i].getName());
        }
        // 返回列表
        return threadList;
    }
    // 获得线程组中子线程组
    public static List<String>getThreadGroups(ThreadGroup group){
        // 获得给定线程组中线程名
        List<String> threadList = getThreads(group);
        // 创建线程组数组
        ThreadGroup[] groups = new ThreadGroup[group.activeGroupCount()];
        // 复制子线程组到线程组数组
        int count= group.enumerate(groups, false);
        // 遍历所有子线程组
        for (int i= 0; i< count; i++) {
            // 利用 getThreads() 方法获得线程名列表
            threadList.addAll(getThreads(groups[i]));
        }
        // 返回所有线程名
        return threadList;
    }

    public static void main(String[] args){
        for (String string : getThreadGroups(getRootThreadGroups())){
            // 遍历输出列表中的字符串
            System.out.println(string);
        }
    }
}

 总结:

本实验主要讲解 ThreadGroup 线程组的获取和使用。

线程组 ThreadGroup 表示一个线程的集合。
Java 虚拟机( JVM )中,除了用户创建的线程,还有许多服务于用户线程的其他线程。

二.线程控制

2.1 控制线程的方法使用

        TestThread.java 类文件中进行编写,多运行几次会发现,程序每次的运行结果可能是不一样的。这是因为多个线程在执行时会抢占 CPU 资源,抢到之后才会执行。而程序员是无法精准控制 CPU 资源的给定包括时间片的长短。尽管如此,Thread 类还提供了一些线程控制方法,虽不能精准控制线程的抢夺情况,但能够帮助我们更好的控制线程。

void start():使该线程开始执行,Java 虚拟机负责调用该线程的 run() 方法。
void sleep(long millis):静态方法,线程进入阻塞状态,在指定时间(单位为毫秒)到达之后进入就绪状态。
void yield():静态方法,当前线程放弃占用 CPU 资源,回到就绪状态,使其他优先级不低于此线程的线程有机会被执行。
void join():只有当前线程等待加入的线程完成,才能继续往下执行。
void interrupt():中断线程的阻塞状态(而非中断线程),例如一个线程 sleep(1000000000) ,为了中断这个过长的阻塞过程,可以调用该线程的 interrupt() 方法,中断阻塞。需要注意的是,此时 sleep() 方法会抛出 InterruptedException 异常。
void isAlive():判定该线程是否处于活动状态,处于就绪、运行和阻塞状态的都属于活动状态。
void setPriority(int newPriority):设置当前线程的优先级。
int getPriority():获得当前线程的优先级。

线程通常在三种情况下会终止:

  • 最普遍的情况是线程中的 run() 方法执行完毕后线程终止;
  • 线程抛出了异常且未进行异常处理;
  • 调用当前线程的 stop() 方法终止线程(该方法已被废弃)。

sleep() 方法的使用

import java.util.Scanner;
/**
 * sleep() 方法的使用
 */
public class TestThread2 {
    public static void main(String[] args) {
        CountThread t = new CountThread();
        t.start();
        Scanner input = new Scanner(System.in);
        System.out.println("如果想终止输出计数线程,请输入 s 。");
        while (true) {
            String s = input.nextLine();
            if ("s".equalsIgnoreCase(s)) {
                t.stopIt();
                break;
            }
        }
        input.close();
    }
}

//计数功能线程
class CountThread extends Thread {
    private int i = 0;

    public CountThread() {
        super("计数线程");
    }

    //通过设置i=100,让线程终止
    public void stopIt() {
        i = 100;
    }

    public void run() {
        try {
            while (i < 100) {
                System.out.println(this.getName() + "计数:" + (i + 1));
                i++;
                  // 每隔 2 秒
                sleep(2000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

yield() 和 sleep() 的区别

yield() 方法和 sleep() 方法都是 Thread 类的静态方法,都会使当前处于运行状态的线程放弃 CPU 资源,把运行机会让给别的线程。

但两者的区别在于:

        1.sleep() 方法会给其他线程运行的机会,不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;而 yield() 方法只会给相同优先级或者更高优先级的线程一个运行的机会。
        2.当线程执行了 sleep(long millis) 方法后,将转到阻塞状态,参数 millis 指定了睡眠时间;而当线程执行了 yield() 方法后,将转到就绪状态。
        3.sleep() 方法声明抛出 InterruptedException 异常,而 yield() 方法没有声明抛出任何异常。yield() 方法只会给相同优先级或者更高优先级的线程一个运行的机会,因此这是一种不可靠的提高程序并发性的方法,只是让系统的调度程序再重新调度一次,在实际编程过程中并不推荐使用。

interrupt() 方法的使用

        Thread 类的静态方法 sleep() ,可以让当前线程进入等待(阻塞)状态,直到指定的时间流逝,或直到别的线程调用当前线程对象的 interrupt() 方法,interrupt() 方法可以中断线程所处的阻塞状态,使线程恢复进入就绪状态。

/**
 * interrupt() 方法的使用
 */
public class TestThread3 {
    public static void main(String[] args) {
        CountThread2 t = new CountThread2();
        t.start();
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断线程的阻塞状态(而非中断线程)
        t.interrupt();
    }
}
// 创建线程类
class CountThread2 extends Thread {
    private int i = 0;

    public CountThread2() {
        super("计数线程");
    }

    public void run() {
        while (i < 10) {
            try {
                System.out.println(this.getName() + "计数:" + (i + 1));
                i++;
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println("程序捕获了 InterruptedException 异常!");
            }
            System.out.println("计数线程运行1次!");
        }
    }
}

join() 方法的使用

        Thread 类的 join() 方法,可以让当前线程等待加入的线程完成,才能继续往下执行。也就是优先执行,t.join() 会让当前执行该语句的线程进入等待状态,即当前线程进入等待状态,而不是 t 线程进入等待状态。

/**
 * join() 方法的使用
 */
public class TestThread4 {
    public static void main(String[] args) throws InterruptedException {
        SThread st = new SThread();
        QThread qt = new QThread(st);
        st.start();
        qt.start();

    }
}

class QThread extends Thread {
    int i = 0;
    Thread t = null;

    //构造方法,传入一个线程对象
    public QThread(Thread t) {
        super("QThread线程");
        this.t = t;
    }

    public void run() {
        try {
            while (i < 100) {
                //当i=5时,调用SThread线程对象的 join() 方法,等线程t执行完毕再执行本线程
                if (i != 5) {
                    Thread.sleep(500);
                    System.out.println("QThread正在每隔0.5秒输出数字:" + i++);
                } else {
                    t.join();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class SThread extends Thread {
    int i = 0;

    //从0输出到99
    public void run() {
        try {
            while (i < 100) {
                Thread.sleep(1000);
                System.out.println("SThread正在每隔1秒输出数字:" + i++);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

总结

本实验我们主要讲解 Thread 类中的 3 个方法的使用:

void sleep(long millis) 静态方法,线程进入阻塞状态,在指定时间(单位为毫秒)到达之后进入就绪状态。
interrupt() 方法,中断线程所处的阻塞状态,使线程恢复进入就绪状态。
void join() 方法,只有当前线程等待加入的线程完成,才能继续往下执行。

秒表倒计时

import java.util.Scanner;

public class CountDown implements Runnable{
    public void go(){
        CountDown down = new CountDown();
        Thread thread = new Thread(down);
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("请输入倒计时秒数:");
        Scanner input = new Scanner(System.in);
        int num=input.nextInt();

        while (num>0) {
            try {
                Thread.sleep(1000);
                System.out.println(num--);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }

    }

    public static void main(String[] args) {
        new CountDown().go();
    }
}

2.2 线程优先级的使用

同一时刻如果有多个线程处于就绪状态,则它们需要排队等待调度程序分配 CPU 资源。此时每个线程自动获得一个线程的优先级,优先级的高低反映线程的重要或紧急程度。就绪状态的线程按优先级排队,线程调度依据的是优先级基础上的 “先到先服务” 原则。

调度程序负责线程排队和 CPU 资源在线程间的分配,并根据线程调度算法进行调度。当线程调度程序选中某个线程时,该线程获得 CPU 资源从而进入运行状态。

线程调度是抢占式调度,即如果在当前线程执行过程中一个更高优先级的线程进入就绪状态,则这个线程立即被调度执行。

线程的优先级用数字 1 ~ 10 表示(默认值为 5),其中 10 表示优先级最高。

尽管 JDK 给线程优先级设置了 10 个级别,但仍然建议只使用 MAX_PRIORITY(级别为 10)、NORM_PRIORITY(级别为 5)和 MIN_PRIORITY(级别为 1)三个常量来设置线程优先级,让程序具有更好的可读性。

/**
 * 线程优先级的使用
 */
public class TestThread5 {
    public static void main(String[] args){
        QThread2 qt = new QThread2();
        SThread2 st = new SThread2();
        // 给qt设置低优先级,给st设置高优先级
        qt.setPriority(Thread.MIN_PRIORITY);
        st.setPriority(Thread.MAX_PRIORITY);
        // 启动线程
        qt.start();
        st.start();
        // 获取线程的优先级别
        System.out.println("QThread2 线程优先级为:" + qt.getPriority());
        System.out.println("SThread2 线程优先级为:" + st.getPriority());
        // 获取qt线程是否在活动状态
        System.out.println("QThread2 线程是否在活动状态:" + qt.isAlive());
        // 获取st线程是否在活动状态
        System.out.println("SThread2 线程是否在活动状态:" + st.isAlive());
    }
}

class QThread2 extends Thread {
    int i = 0;

    public void run() {
        while (i < 30) {
            System.out.println("QThread2正在输出数字:" + i++);
        }
    }
}

class SThread2 extends Thread {
    int i = 0;

    public void run() {
        while (i < 30) {
            System.out.println("SThread2正在输出数字:" + i++);
        }
    }
}

总结

本实验主要讲解线程优先级别的设置使用。

设置和获取优先级的方法使用,同时也明白一个道理,优先级的高低并不是线程执行顺序的唯一因素,只不过是给系统一个建议,具体的执行还是要根据操作系统来决定。
使用 Thread 类中的 isAlive() 方法,来获取当前线程是否在活动状态,没有进入终止状态的都可以称为活动状态。

2.3 守护线程的使用

守护线程是为其他线程的运行提供便利的线程。

Java 的垃圾收集机制中的一些实现就使用了守护线程。

守护线程会在所有非守护线程结束后自动强制终止,而不是等待其它线程执行完毕后才终止

程序可以包含守护线程和用户线程,当程序只有守护线程时,该程序才能真正结束运行

如果要使一个线程成为守护线程,则必须在调用它的 start() 方法之前,调用线程的 setDaemon(true) 方法。并且可以使用 isDaemon() 方法的返回值( true 或 false )判断一个线程是否为守护线程。

Thread 类中提供的方法:

public final void setDaemon(boolean on)

当 on 设置为 true 时,该线程为守护线程;当 on 设置为 false 时,该线程为用户线程;默认情况下是 false。

public final boolean isDaemon()

判断当前线程是否为守护线程。true - 是,false - 否。

这两个方法都是被 final 修饰的,也就是说这两个方法只能被调用,不能被重写。

/**
 * 守护线程的基本使用
 */
public class TestThread6 {
    public static void main(String[] args) {
        DaemonThread t = new DaemonThread();
        t.start();
        System.out.println("让一切都结束吧");
    }
    // 创建私有静态内部类
    private static class DaemonThread extends Thread {
        //在无参构造方法中设置本线程为守护线程
        public DaemonThread() {
            setDaemon(true);
        }

        public void run() {
            while (true) {
                System.out.println("我是后台线程");
            }
        }
    }
}

         一个程序只有处于守护线程时该程序结束运行,所以即便程序中创建并启动了一个线程 t 且 t 的 run() 方法永久循环输出,仍会在主程序执行完毕后退出程序。所以在执行以上程序时,输出的 “我是后台线程” 字符串出现的次数,每次运行结果都不一样。

守护线程的应用

用户线程是为了完成任务,类似 “作战的战士”。

守护线程是为其他线程服务的,类似 “后勤保障”。

接下来我们将创建两个线程,一个是用户线程,用来输出一些语句;另一个是守护线程,用来计算程序的运行时间。

/**
 * 守护线程的应用
 */
public class TestDaemonThread {
    public static void main(String[] args) {
        Thread userThread = new Thread(new Worker());
        Thread daemonThread = new Thread(new Timer());
        // 设置守护线程
        daemonThread.setDaemon(true);
        // 启动用户和守护线程
        userThread.start();
        daemonThread.start();
        System.out.println("Worker 是否为守护线程:" + userThread.isDaemon());
        System.out.println("Timer 是否为守护线程:" + daemonThread.isDaemon());
    }
}
// 普通线程
class Worker implements Runnable {
    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("《Java编程词典》第"+i+"次更新!");
        }
    }
}

// 守护线程
class Timer implements Runnable {
    @Override
    public void run(){
        long currentTime = System.currentTimeMillis();
        long processTime = 0;
        while (true) {
            if ((System.currentTimeMillis() - currentTime) > processTime){
                processTime = System.currentTimeMillis() - currentTime;
                System.out.println("程序运行时间:"+ processTime);
            }
        }
    }
}

总结 

本实验主要讲解的是守护线程的使用。

守护线程需要通过 setDaemon() 方法设置为 true,通过 isDaemon() 获取是否为守护线程。
用户可以自定义一个守护线程用于服务自己的主线程。
守护线程作为后台线程,其内容不一定会执行。

三.线程间的数据共享

3.1 多线程数据共享

        多线程程序中各个线程大多是独立运行的,但在真正的应用中,程序中的多个线程通常以某种方式进行通信或共享数据。在这种情况下,必须使用同步机制来确保数值被正确地传递,并防止数据不一致。

        当一个数据被多个线程存取的时候,通过检查这个数据的值来进行判断并执行操作是极不安全的。因为在判断之后,有可能因为 CPU 时间切换或阻塞而挂起,挂起过程中这个数据的值很可能被其他线程修改了,判断条件也可能已经不成立了,但此时已经经过了判断,之后的操作还需要继续进行。这就会造成逻辑的混乱,导致数据不一致。

我们通过代码来感受一下多线程下数据共享所存在的问题。

公有data引起混乱

public class TestShareData {
    static int data = 0;

    public static void main(String[] args) {
        ShareThread1 st1 = new ShareThread1();
        ShareThread2 st2 = new ShareThread2();
        new Thread(st1).start();
        new Thread(st2).start();
    }

    //内部类,访问类中静态成员变量data
    private static class ShareThread1 implements Runnable {
        public void run() {
            while (data < 10) {
                try {
                    Thread.sleep(1000);
                    System.out.println("这个小于10的数据是:" + data++);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //内部类,访问类中静态成员变量data
    private static class ShareThread2 implements Runnable {
        public void run() {
            while (data < 100) {
                data++;
            }
        }
    }
}

3.2 多线程下的售票系统

        目前一趟车设定为 100 张票,由 4 个售票窗口进行售票处理,本次挑战需要完成数据共享的效果即可。


public class SellTicketsData {

    public static void main(String[] args) {
        SellThread sellThread1 = new SellThread();
        Thread thread1 = new Thread(sellThread1);
        thread1.setName("售票窗口1 ");

        SellThread sellThread2 = new SellThread();
        Thread thread2 = new Thread(sellThread2);
        thread2.setName("售票窗口2 ");

        SellThread sellThread3 = new SellThread();
        Thread thread3 = new Thread(sellThread3);
        thread3.setName("售票窗口3 ");

        SellThread sellThread4 = new SellThread();
        Thread thread4 = new Thread(sellThread4);
        thread4.setName("售票窗口4 ");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }


}
class SellThread implements Runnable{
     int ticket = 100;

    @Override
    public void run() {
        while (ticket>0)
        System.out.println(Thread.currentThread().getName()+"票价:"+(ticket--));
    }
}

 3.3 多线程同步处理方式一

        当遇到多个线程同时执行,而我们不希望每个线程交替执行时,就可以使用 synchronzied 关键字对同步代码块进行局部封锁,从而实现同步。多线程同步依靠的是对象锁机制,synchronized 关键字就是利用锁来实现对共享资源的互斥访问。

        synchronized() 语句块的参数可以是任何类型的对象。如将 synchronized (lock) 中的 lock 对象改为其他任意对象,或者直接改为 synchronized (this) 都是可以的。

/**
 * 通过对象锁解决多线程下的共享数据问题
 */
public class TestShareData2 {
    static int data = 0;
    //定义了一个对象锁lock
    static final Object lock = new Object();

    public static void main(String[] args) {
        ShareThread1 st1 = new ShareThread1();
        ShareThread2 st2 = new ShareThread2();
        new Thread(st1).start();
        new Thread(st2).start();
    }

    private static class ShareThread1 implements Runnable {
        public void run() {
            //获取对象锁lock
            synchronized (lock) {
                while (data < 10) {
                    try {
                        Thread.sleep(1000);
                        System.out.println("这个小于10的数据是:" + data++);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private static class ShareThread2 implements Runnable {
        public void run() {
            //获取对象锁
            synchronized (lock) {
                
                while (data < 100) {
                    //System.out.println("data的值为:" + data);
                    data++;
                }
                System.out.println("ShareThread2执行完后data的值为:" + data);
            }
        }
    }
}

        在同一段时间内,只会有一个线程成功获取到 lock,然后执行 synchronized 代码块,与此同时其余线程对象就会处于阻塞状态,直到之前的线程对象将 synchronized 代码块执行完毕,从而将 lock 释放之后,所有线程对象再重新争夺 lock ,争夺成功的线程对象再去执行 synchronized 代码块。

通过加锁实现多线程同步

如果希望五个线程之间是顺序地输出,就可通过加锁的方式实现。

/**
 * 启动多个线程,不希望每个线程交替执行。
 */
public class TestSyncThread {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new SyncThread(i)).start();
        }
    }
}
// 创建线程类
class SyncThread implements Runnable {
    private int tid;

    public SyncThread(int id) {
        this.tid = id;
    }

    public void run() {
          // 使用 this 作为竞争对象,代码可以变得非常简洁。
        synchronized(this){
            for (int i = 0; i < 10; i++) {
                System.out.println("线程ID名为: " + this.tid + "正在输出:" + i);
            }
        }
    }
}

总结

        线程对象在访问 synchronized 代码块前,会先主动尝试获取锁对象。并且只有成功的获取到了对象锁之后,线程对象才能执行 synchronized 代码块。此外, synchronized 代码块可以让对象锁在同一时间内,只能被一个线程对象获取到。我们可以将对象锁比作一个唯一的令牌,将多个线程对象比作多个竞争的选手, synchronized 代码块则是一个城堡,并且这个城堡规定同一时间只能有一个选手进入。因此,在任何一段时间内,只会有唯一的一个选手争夺到唯一的令牌,然后进入城堡,而等他离开城堡后,这个令牌会被释放,所有的选手又会重新竞争这个令牌。

实现同步块处理的方式:

synchronized(lock){...},lock 需要是准备好的任何对象。

synchronized(this){...},当程序中只有一层同步块处理时,可以使用 this 关键字作为竞争对象,简化代码的书写。


public class SellTicketsMethod {

    public static void main(String[] args) {
 
        SellThread sellThread1 = new SellThread();
        Thread thread1 = new Thread(sellThread1);
        thread1.setName("售票窗口1 ");

        SellThread sellThread2 = new SellThread();
        Thread thread2 = new Thread(sellThread2);
        thread2.setName("售票窗口2 ");

        SellThread sellThread3 = new SellThread();
        Thread thread3 = new Thread(sellThread3);
        thread3.setName("售票窗口3 ");

        SellThread sellThread4 = new SellThread();
        Thread thread4 = new Thread(sellThread4);
        thread4.setName("售票窗口4 ");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }


}
class SellThread implements Runnable{
    int ticket = 100;

    @Override
    public void run() {
           sell();
    }
    private  synchronized void sell(){
        while (ticket>0){
            System.out.println(Thread.currentThread().getName()+"票价:"+(ticket--));
        }
    }
}

3.4 解决线程死锁问题

什么是线程死锁

多线程同步,解决的是多线程的安全性问题,但同步也同时会带来性能损耗和线程死锁的问题。

多线程同步采用了同步代码块和同步方法的方式,依靠的是锁机制实现了互斥访问。因为是互斥的访问,所以是用串行执行的方式替代了原有的多线程并发执行,因此存在性能问题。

多线程同步的性能问题仅仅是快和慢的问题,但如果出现了线程死锁,那将导致线程长期处于阻塞状态,严重影响系统的性能。

如果线程 A 只有等待线程 B 的完成才能继续,而在线程 B 中又要等待线程 A 的资源,那么这两个线程相互等待对方释放锁时就会发生死锁。出现死锁后,不会出现异常,因此不会有任何提示,只是相关线程都处于阻塞状态,无法继续运行。

死锁产生的原因有以下三个方面:

系统资源不足。如果系统的资源充足,所有线程的资源请求都能够得到满足,自然就不会发生死锁。
线程运行推进的顺序不合适。
资源分配不当等。
产生死锁的必要条件有以下四个:

互斥条件:一个资源每次只能被一个线程使用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
只要系统发生死锁,这四个条件就必然成立;反之,只要破坏四个条件中的任意一个,就可以避免死锁的产生。

/**
 * 产生死锁现象实现
 */
public class TestDeadLock implements Runnable{
    // 使用 flag 变量作为进入不同块的标志
    private boolean flag;
    private static final Object o1 = new Object();
    private static final Object o2 = new Object();

    @Override
    public void run() {
        // 获得当前线程的名字
        String threadName = Thread.currentThread().getName();
        // 输出当前线程的 flag 变量值
        System.out.println(threadName + ": flag = " + flag);
            if (flag == true){
            // 为 o1 加锁
            synchronized(o1){
                try {
                    // 线程休眠 1 秒钟
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 显示进入 o1 块
                System.out.println(threadName+"进入同步块o1准备进入 o2");
                // 为o2加锁
                synchronized (o2) {
                    // 显示进入o2块
                    System.out.println(threadName +"已经进入同步块o2");
                }
            }
        }

        if (flag == false){
            synchronized (o2){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                // 显示进入 o2 块
                System.out.println(threadName +"进入同步块o2准备进入o1");
                synchronized(o1){
                    // 显示进入 o1 块
                    System.out.println(threadName +"已经进入同步块o1");
                }
            }
        }
    }

    public static void main(String[]args){
             // 创建 TestDeadLock 对象 d1
        TestDeadLock d1 = new TestDeadLock();
        // 创建 TestDeadLock 对象 d2
        TestDeadLock d2 = new TestDeadLock();
        // 将 d1 的 flag 设置为 true
        d1.flag = true;
        // 将 d2 的 flag 设置为 false
        d2.flag = false;
        // 在新线程中运行 d1 的 run() 方法
        new Thread(d1).start();
        // 在新线程中运行 d2 的 run() 方法
        new Thread(d2).start();
    }
}

通过观察可以发现此时程序并没有结束,也没有进入下一步的操作。

其实此时程序是进入了死锁状态,死锁的原因是:

当 d1 的 run() 方法运行时,首先获得 o1 对象的内置锁。在其休眠的 1 秒钟内,d2 的 run() 方法开始运行,它获得了 o2 对象的内置锁并进入休眠状态。而当 d1 的 run() 方法需要获得 o2 的内置锁时,该锁已经被占用,因此进入了死锁状态。

 解决死锁问题

看flag == false,去掉了synchronized (o1)

 解决这类死锁问题最有效的方法是破坏循环等待。

四.线程协作

4.1 线程协作处理

        线程协作的一个典型案例就是生产者和消费者问题,生产者和消费者的这种协作是通过线程之间的握手来实现的,而这种握手又是通过 Object 类的 wait() 和 notify() / notifyAll() 方法来实现的。

下面具体来了解生产者和消费者问题:

有一家餐厅举办吃热狗活动,活动时有 5 个顾客来吃, 3 个厨师来做。为了避免浪费,制作好的热狗被放进一个能装 10 个热狗的长条状容器中,并且按照先进先出的原则取热狗。如果长条容器被装满,则停止做热狗;如果顾客发现长条容器内的热狗吃完了,则提醒厨师再做热狗。这里的厨师就是生产者,顾客就是消费者。

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

对于生产者,当生产的产品装满了仓库,则需要停止生产,等待消费者消费后提醒生产者继续生产;

对于消费者,当发现仓库中已没有产品时,则不能消费,等待生产者生产出产品以后通知消费者可以消费。

之前学习的 synchronized 关键字可实现对共享资源的互斥操作,但无法实现不同线程之间消息的传递。

JDK 的 Object 类提供了 void wait()、void notify()、void notifyAll() 三个方法,解决线程之间协作的问题。

语法上,这三个方法都只能在 synchronized 修饰的同步方法或者同步代码块中使用,否则会抛出异常。

下面是这三个方法的简单介绍:

void wait():让当前线程等待,等待其他线程调用此对象的 notify() 方法或 notifyAll() 方法将其唤醒。
void notify():唤醒在此对象锁上等待的单个线程;如果有多个等待的线程,则随机唤醒一个。
void notifyAll():唤醒在此对象锁上等待的所有线程。
import java.util.ArrayList;
import java.util.List;
/**
 * 实现生产者和消费者线程协作
 */
public class TestProdCons {
    //定义一个存放热狗的集合,里面存放的是整数,代表热狗编号
    private static final List<Integer> hotDogs = new ArrayList<Integer>();

    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            new Producer(i).start();
        }
        for (int i = 1; i <= 5; i++) {
            new Consumer(i).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }

    //生产者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
    private static class Producer extends Thread {
        int i = 1;
        int pid = -1;

        public Producer(int id) {
            this.pid = id;
        }

        public void run() {
            while (true) {
                try {
                    //模拟消耗的时间
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (hotDogs) {
                    if (hotDogs.size() < 10) {
                        //热狗编号,如300002代表编号为3的生产者生产的第2个热狗
                        hotDogs.add(pid * 10000 + i);
                        System.out.println("生产者" + pid + "生产热狗,编号为:" + pid * 10000 + i);
                        i++;
                        //唤醒hotDogs对象锁上所有调用wait()方法的线程
                        hotDogs.notifyAll();
                    } else {
                        try {
                            System.out.println("热狗数已到10个,等待消费!");
                            hotDogs.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    //消费者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
    private static class Consumer extends Thread {
        int cid = -1;

        public Consumer(int id) {
            this.cid = id;
        }

        public void run() {
            while (true) {
                synchronized (hotDogs) {
                    try {
                        //模拟消耗的时间
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (hotDogs.size() > 0) {
                        System.out.println("消费者" + this.cid + "正在消费一个热狗,其编号为: " + hotDogs.remove(0));
                        hotDogs.notifyAll();
                    } else {
                        try {
                            System.out.println("已没有热狗,等待生产!");
                            hotDogs.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猪八戒1.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值