Java—多线程

文章目录

    • 一、并发和并行
      • 1.1、并发
      • 1.2、并行
    • 二、进程和线程
      • 2.1、线程
      • 2.2、进程
    • 三、多线程
      • 3.1、线程的创建和启动
      • 3.2、Thread类和Runnable接口的区别
      • 3.3、Thread类的常用方法
      • 3.4、线程安全问题
      • 3.5、线程安全的解决方案—同步技术
        • 方案1:使用同步代码块
        • 方案2:使用同步方法
        • 方案3:使用Lock锁
    • 四、线程状态
      • 4.1、线程状态定义
      • 4.2、线程状态转换图
      • 4.3、线程间通信
        • 4.3.1、等待唤醒机制
    • 五、线程池
      • 5.1、线程池的介绍
      • 5.2、线程池的使用

一、并发和并行

说到多线程,我们就不得不涉及到并发并行的概念。
并发: 两个或多个事件在同一时间段发生。
并行: 两个或多个事件的同一时刻发生(同时进行)。

1.1、并发

Java—多线程_第1张图片
通过上面的图面可以看出,在单核CPU的情况下,并发是多个任务轮流互相争抢CPU资源来执行任务的,任务是交替执行的。

1.2、并行

Java—多线程_第2张图片
通过上面的图面可以看出,在多个CPU的情况下,并行是每个任务都会分配CPU资源去执行任务,它们不需要争抢CPU资源,多个任务同时执行,所以效率比并发要强。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单个CPU系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。 对这部分内容感兴趣的同学可以去看《操作系统》。

二、进程和线程

进程: 进程是指程序执行时的一个实例,分配系统资源(CPU时间、内存等)的基本单位。
线程: 线程是程序执行时的最小单位,它是进程的一个执行流,是系统调度和分派的基本单位。
进程和线程的关系: 一个进程至少包含有一个线程,甚至多个线程。所以也称线程是轻量级进程。
注意: 本文主要讲的是Java的多线程,进程了解下概念就行,能区分进程和线程的区别。

2.1、线程

线程调度方式: 分时调度抢占式调度

  1. 分时调度:所有线程轮流使用CPU的使用权,并且平均分配每个线程占用的CPU时间片。

  2. 抢占式调度:优先让优先级高的线程使用CPU,如果线程优先级相同,就会随机选择一个线程,Java使用的正式抢占式调度。

2.2、进程

进程就了解下即可,这里不多做介绍,感兴趣的同学可以去阅读看下《操作系统》进程的部分。

三、多线程

多线程: 程序在运行时产生了至少2个线程的程序。

3.1、线程的创建和启动

线成的创建方式有2种
(1)继承Thread类
继承Thread类的实现步骤和具体代码如下:

  • 1.创建一个Thread的子类
  • 2.在子类中重写Thread父类的run方法,即设置线程的任务(干什么)
  • 3.创建Thread的子类对象
  • 4.Thread子类对象调用父类的start()方法,开启一个新的线程,执行run()方法的任务
// 1.创建一个Thread的子类
public class MyThread extends Thread {
    // 2.在子类中重写Thread父类的run方法,即设置线程的任务(干什么)
    @Override
    public void run(){
        for (int i = 0; i <10 ; i++) {
            System.out.println("run->"+i);
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        // 3.创建Thread的子类对象
        MyThread mt = new MyThread();

        //4.Thread子类对象调用父类的start()方法,开启一个新的线程,执行run()方法的任务
        mt.start();

        for (int i = 0; i <10 ; i++) {
            System.out.println("main->"+i);
        }
        // 每次运行结果都可能不一样
    }
}

(2)实现Runnable接口
实现Runnable接口的实现步骤和具体代码如下:

  • 1.创建一个Runnable接口的实现类
  • 2.重写run方法,设置线程的任务
  • 3.创建一个Runnable的实现类对象
  • 4.创建Thread对象,构造方法中传入Runnable的实现类对象
  • 5.Thread对象调用start()方法,开启线程执行run()方法的任务
// 1.创建一个Runnable接口的实现类
public class RunnableImpl implements Runnable{
   // 2.重写run方法,设置线程的任务
   @Override
   public void run() {
       for (int i = 0; i <10 ; i++) {
           System.out.println("run->"+i);
       }
   }
}

public class RunnableDemo {
   public static void main(String[] args) {
       // 3.创建一个Runnable的实现类对象
       Runnable r = new RunnableImpl();
       // 4.创建Thread对象,构造方法中传入Runnable的实现类对象
       Thread t = new Thread(r);
       // 5.Thread对象调用start()方法,开启线程执行run()方法的任务
       t.start();
       for (int i = 0; i <10 ; i++) {
            System.out.println("main->"+i);
       }
   }
}

3.2、Thread类和Runnable接口的区别

实现Runnable接口比继承Thread类所具有的优势:

  • 1.适合多个相同的程序代码的线程去共享同一个资源。
  • 2.可以避免java中的单继承的局限性。
  • 3.增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  • 4.线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

3.3、Thread类的常用方法

方法名称 作用
public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法
public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。Thread 的子类应该重写该方法。
public final String getName() 返回该线程的名称
public final void setName(String name) 改变线程名称,使之与参数 name 相同
public final void setPriority(int priority) 更改线程的优先级
public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程
public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒
public void interrupt() 中断线程
public final boolean isAlive() 测试线程是否处于活动状态
public static Thread currentThread() 返回对当前正在执行的线程对象的引用
public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程

3.4、线程安全问题

线程安全问题: 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。例如,银行取钱,卖票问题。
线程安全产生的原因:

  • 1.多个线程在操作共享的数据。
  • 2.操作共享数据的线程代码有多条。

代码示例

/**
 * 模仿卖票操作(总共100张票)
 * 从第100张票开始卖
 */
public class RunnableImpl implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                // 为了提高线程安全问题出现的概率 在这里休眠一下
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "->正在卖第" + ticket + "张票");
                ticket--;
            }else{
                return;
            }
        }
    }
}


public class TicketTest {
    public static void main(String[] args) {
        Runnable r = new RunnableImpl();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);

        t1.start();
        t2.start();
        t3.start();
    }
}

输出结果

卖票窗口3->正在卖第100张票
卖票窗口1->正在卖第100张票
卖票窗口2->正在卖第100张票
卖票窗口3->正在卖第97张票
卖票窗口1->正在卖第96张票
卖票窗口2->正在卖第96张票
卖票窗口2->正在卖第94张票
卖票窗口1->正在卖第93张票
卖票窗口3->正在卖第93张票
卖票窗口2->正在卖第91张票
卖票窗口1->正在卖第90张票
卖票窗口3->正在卖第89张票
卖票窗口2->正在卖第88张票
卖票窗口3->正在卖第87张票
卖票窗口1->正在卖第87张票
卖票窗口2->正在卖第85张票
卖票窗口1->正在卖第84张票
卖票窗口3->正在卖第84张票
卖票窗口2->正在卖第82张票
卖票窗口3->正在卖第81张票
卖票窗口1->正在卖第80张票
卖票窗口2->正在卖第79张票
卖票窗口3->正在卖第78张票
卖票窗口1->正在卖第78张票
卖票窗口2->正在卖第76张票
卖票窗口3->正在卖第75张票
卖票窗口1->正在卖第74张票
卖票窗口2->正在卖第73张票
卖票窗口1->正在卖第72张票
卖票窗口3->正在卖第72张票
卖票窗口2->正在卖第70张票
卖票窗口1->正在卖第69张票
卖票窗口3->正在卖第69张票
卖票窗口2->正在卖第67张票
卖票窗口1->正在卖第66张票
卖票窗口3->正在卖第66张票
卖票窗口2->正在卖第64张票
卖票窗口3->正在卖第63张票
卖票窗口1->正在卖第63张票
卖票窗口2->正在卖第61张票
卖票窗口1->正在卖第60张票
卖票窗口3->正在卖第60张票
卖票窗口2->正在卖第58张票
卖票窗口3->正在卖第57张票
卖票窗口1->正在卖第57张票
卖票窗口2->正在卖第55张票
卖票窗口1->正在卖第54张票
卖票窗口3->正在卖第54张票
卖票窗口2->正在卖第52张票
卖票窗口3->正在卖第51张票
卖票窗口1->正在卖第51张票
卖票窗口2->正在卖第49张票
卖票窗口1->正在卖第48张票
卖票窗口3->正在卖第48张票
卖票窗口2->正在卖第46张票
卖票窗口3->正在卖第45张票
卖票窗口1->正在卖第45张票
卖票窗口2->正在卖第43张票
卖票窗口1->正在卖第42张票
卖票窗口3->正在卖第42张票
卖票窗口2->正在卖第40张票
卖票窗口1->正在卖第39张票
卖票窗口3->正在卖第38张票
卖票窗口2->正在卖第37张票
卖票窗口3->正在卖第36张票
卖票窗口1->正在卖第35张票
卖票窗口2->正在卖第34张票
卖票窗口3->正在卖第33张票
卖票窗口1->正在卖第33张票
卖票窗口2->正在卖第31张票
卖票窗口1->正在卖第30张票
卖票窗口3->正在卖第30张票
卖票窗口2->正在卖第28张票
卖票窗口3->正在卖第27张票
卖票窗口1->正在卖第26张票
卖票窗口2->正在卖第25张票
卖票窗口1->正在卖第24张票
卖票窗口3->正在卖第23张票
卖票窗口2->正在卖第22张票
卖票窗口1->正在卖第21张票
卖票窗口3->正在卖第21张票
卖票窗口2->正在卖第19张票
卖票窗口3->正在卖第18张票
卖票窗口1->正在卖第17张票
卖票窗口3->正在卖第16张票
卖票窗口2->正在卖第15张票
卖票窗口1->正在卖第14张票
卖票窗口3->正在卖第13张票
卖票窗口2->正在卖第13张票
卖票窗口1->正在卖第11张票
卖票窗口2->正在卖第10张票
卖票窗口3->正在卖第10张票
卖票窗口1->正在卖第8张票
卖票窗口2->正在卖第7张票
卖票窗口3->正在卖第7张票
卖票窗口1->正在卖第5张票
卖票窗口2->正在卖第4张票
卖票窗口1->正在卖第3张票
卖票窗口3->正在卖第4张票
卖票窗口1->正在卖第1张票
卖票窗口2->正在卖第1张票
卖票窗口3->正在卖第-1张票

总结: 上述的卖票例子就能说明线程安全问题,3个人同时卖票同一张票,甚至卖出了不存在的第-1张票。当我们多个人进行卖票,就是多个线程对共享数据进行访问修改,如果只是查票剩余多少,不会出现线程安全问题,如果对剩余票量进行修改,就会出现线程安全问题,这是不允许的情况,下面有解决方案。

3.5、线程安全的解决方案—同步技术

解决方案思路: 当有一个线程在操作多条代码的共享数据时,只让当前线程可以操作,其他线程不允许操作多条代码的共享数据。

方案1:使用同步代码块

格式:
synchronized(锁对象){
可能出现线程安全问题的代码(访问共享数据的代码)
}

注意:

  • 1.同步代码块中的锁对象,可以是任意的对象。
  • 2.但是必须保证多个线程使用的锁对象是同一个。
  • 3.锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块执行。

代码示例

public class RunnableImpl implements Runnable {
    private int ticket = 100;

    // 创建一个锁对象
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    // 为了提高线程安全问题出现的概率 在这里休眠一下
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "->正在卖第" + ticket + "张票");
                    ticket--;
                } else {
                    return;
                }
            }
        }
    }
}

public class SynchronizedDemo {
    public static void main(String[] args) {
        Runnable r = new RunnableImpl();
        Thread t1 = new Thread(r,"卖票窗口1");
        Thread t2 = new Thread(r,"卖票窗口2");
        Thread t3 = new Thread(r,"卖票窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

输出结果

卖票窗口1->正在卖第100张票
卖票窗口1->正在卖第99张票
卖票窗口1->正在卖第98张票
卖票窗口1->正在卖第97张票
卖票窗口1->正在卖第96张票
卖票窗口1->正在卖第95张票
卖票窗口1->正在卖第94张票
卖票窗口1->正在卖第93张票
卖票窗口1->正在卖第92张票
卖票窗口1->正在卖第91张票
卖票窗口1->正在卖第90张票
卖票窗口1->正在卖第89张票
卖票窗口1->正在卖第88张票
卖票窗口1->正在卖第87张票
卖票窗口1->正在卖第86张票
卖票窗口1->正在卖第85张票
卖票窗口1->正在卖第84张票
卖票窗口1->正在卖第83张票
卖票窗口1->正在卖第82张票
卖票窗口1->正在卖第81张票
卖票窗口1->正在卖第80张票
卖票窗口1->正在卖第79张票
卖票窗口1->正在卖第78张票
卖票窗口1->正在卖第77张票
卖票窗口1->正在卖第76张票
卖票窗口1->正在卖第75张票
卖票窗口1->正在卖第74张票
卖票窗口1->正在卖第73张票
卖票窗口1->正在卖第72张票
卖票窗口1->正在卖第71张票
卖票窗口1->正在卖第70张票
卖票窗口3->正在卖第69张票
卖票窗口3->正在卖第68张票
卖票窗口3->正在卖第67张票
卖票窗口3->正在卖第66张票
卖票窗口3->正在卖第65张票
卖票窗口3->正在卖第64张票
卖票窗口3->正在卖第63张票
卖票窗口3->正在卖第62张票
卖票窗口3->正在卖第61张票
卖票窗口3->正在卖第60张票
卖票窗口3->正在卖第59张票
卖票窗口3->正在卖第58张票
卖票窗口3->正在卖第57张票
卖票窗口3->正在卖第56张票
卖票窗口3->正在卖第55张票
卖票窗口3->正在卖第54张票
卖票窗口3->正在卖第53张票
卖票窗口3->正在卖第52张票
卖票窗口3->正在卖第51张票
卖票窗口3->正在卖第50张票
卖票窗口3->正在卖第49张票
卖票窗口3->正在卖第48张票
卖票窗口3->正在卖第47张票
卖票窗口3->正在卖第46张票
卖票窗口3->正在卖第45张票
卖票窗口3->正在卖第44张票
卖票窗口3->正在卖第43张票
卖票窗口3->正在卖第42张票
卖票窗口3->正在卖第41张票
卖票窗口3->正在卖第40张票
卖票窗口3->正在卖第39张票
卖票窗口3->正在卖第38张票
卖票窗口3->正在卖第37张票
卖票窗口3->正在卖第36张票
卖票窗口3->正在卖第35张票
卖票窗口3->正在卖第34张票
卖票窗口3->正在卖第33张票
卖票窗口3->正在卖第32张票
卖票窗口3->正在卖第31张票
卖票窗口3->正在卖第30张票
卖票窗口3->正在卖第29张票
卖票窗口3->正在卖第28张票
卖票窗口3->正在卖第27张票
卖票窗口3->正在卖第26张票
卖票窗口3->正在卖第25张票
卖票窗口3->正在卖第24张票
卖票窗口3->正在卖第23张票
卖票窗口3->正在卖第22张票
卖票窗口3->正在卖第21张票
卖票窗口3->正在卖第20张票
卖票窗口3->正在卖第19张票
卖票窗口3->正在卖第18张票
卖票窗口3->正在卖第17张票
卖票窗口3->正在卖第16张票
卖票窗口3->正在卖第15张票
卖票窗口3->正在卖第14张票
卖票窗口3->正在卖第13张票
卖票窗口3->正在卖第12张票
卖票窗口3->正在卖第11张票
卖票窗口3->正在卖第10张票
卖票窗口3->正在卖第9张票
卖票窗口3->正在卖第8张票
卖票窗口3->正在卖第7张票
卖票窗口3->正在卖第6张票
卖票窗口3->正在卖第5张票
卖票窗口3->正在卖第4张票
卖票窗口3->正在卖第3张票
卖票窗口3->正在卖第2张票
卖票窗口3->正在卖第1张票

方案2:使用同步方法

格式:
访问修饰符 synchronized 返回值类型 方法名称(){
可能出现线程安全问题的代码(访问共享数据的代码)
}

注意:

  • 同步方法会把内部的代码块锁住,只让一个线程执行
  • 同步方法的锁对象是实现类对象(this)

代码示例

public class RunnableImpl implements Runnable {
    private int ticket = 100;


    @Override
    public  void run() {
        System.out.println("run:"+this);
        while (true) {
            sendTicket();
        }
    }


    public synchronized void  sendTicket(){
        if (ticket > 0) {
            // 为了提高线程安全问题出现的概率 在这里休眠一下
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "->正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}


public class SynchronizedDemo {
    public static void main(String[] args) {
        Runnable r = new RunnableImpl();
        System.out.println("RunnableImpl:"+r);

        Thread t1 = new Thread(r,"卖票窗口1");
        Thread t2 = new Thread(r,"卖票窗口2");
        Thread t3 = new Thread(r,"卖票窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

输出结果

  • RunnableImpl:com.zsd.stage1_7_2.demo7.RunnableImpl@65ab7765
    和run:com.zsd.stage1_7_2.demo7.RunnableImpl@65ab7765是同一个对象。 可以知道锁对象(this)就是实现类对象
RunnableImpl:com.zsd.stage1_7_2.demo7.RunnableImpl@65ab7765
run:com.zsd.stage1_7_2.demo7.RunnableImpl@65ab7765
run:com.zsd.stage1_7_2.demo7.RunnableImpl@65ab7765
run:com.zsd.stage1_7_2.demo7.RunnableImpl@65ab7765
卖票窗口1->正在卖第100张票
卖票窗口1->正在卖第99张票
卖票窗口1->正在卖第98张票
卖票窗口1->正在卖第97张票
卖票窗口1->正在卖第96张票
卖票窗口1->正在卖第95张票
卖票窗口1->正在卖第94张票
卖票窗口1->正在卖第93张票
卖票窗口1->正在卖第92张票
卖票窗口1->正在卖第91张票
卖票窗口1->正在卖第90张票
卖票窗口1->正在卖第89张票
卖票窗口1->正在卖第88张票
卖票窗口1->正在卖第87张票
卖票窗口1->正在卖第86张票
卖票窗口1->正在卖第85张票
卖票窗口1->正在卖第84张票
卖票窗口1->正在卖第83张票
卖票窗口1->正在卖第82张票
卖票窗口1->正在卖第81张票
卖票窗口1->正在卖第80张票
卖票窗口1->正在卖第79张票
卖票窗口1->正在卖第78张票
卖票窗口1->正在卖第77张票
卖票窗口1->正在卖第76张票
卖票窗口1->正在卖第75张票
卖票窗口1->正在卖第74张票
卖票窗口1->正在卖第73张票
卖票窗口1->正在卖第72张票
卖票窗口1->正在卖第71张票
卖票窗口1->正在卖第70张票
卖票窗口1->正在卖第69张票
卖票窗口1->正在卖第68张票
卖票窗口1->正在卖第67张票
卖票窗口1->正在卖第66张票
卖票窗口1->正在卖第65张票
卖票窗口1->正在卖第64张票
卖票窗口1->正在卖第63张票
卖票窗口1->正在卖第62张票
卖票窗口1->正在卖第61张票
卖票窗口1->正在卖第60张票
卖票窗口1->正在卖第59张票
卖票窗口1->正在卖第58张票
卖票窗口1->正在卖第57张票
卖票窗口1->正在卖第56张票
卖票窗口1->正在卖第55张票
卖票窗口1->正在卖第54张票
卖票窗口1->正在卖第53张票
卖票窗口1->正在卖第52张票
卖票窗口1->正在卖第51张票
卖票窗口1->正在卖第50张票
卖票窗口1->正在卖第49张票
卖票窗口1->正在卖第48张票
卖票窗口1->正在卖第47张票
卖票窗口1->正在卖第46张票
卖票窗口1->正在卖第45张票
卖票窗口1->正在卖第44张票
卖票窗口1->正在卖第43张票
卖票窗口1->正在卖第42张票
卖票窗口3->正在卖第41张票
卖票窗口3->正在卖第40张票
卖票窗口3->正在卖第39张票
卖票窗口3->正在卖第38张票
卖票窗口3->正在卖第37张票
卖票窗口3->正在卖第36张票
卖票窗口3->正在卖第35张票
卖票窗口3->正在卖第34张票
卖票窗口3->正在卖第33张票
卖票窗口3->正在卖第32张票
卖票窗口3->正在卖第31张票
卖票窗口3->正在卖第30张票
卖票窗口3->正在卖第29张票
卖票窗口3->正在卖第28张票
卖票窗口3->正在卖第27张票
卖票窗口3->正在卖第26张票
卖票窗口3->正在卖第25张票
卖票窗口3->正在卖第24张票
卖票窗口3->正在卖第23张票
卖票窗口3->正在卖第22张票
卖票窗口3->正在卖第21张票
卖票窗口3->正在卖第20张票
卖票窗口3->正在卖第19张票
卖票窗口3->正在卖第18张票
卖票窗口3->正在卖第17张票
卖票窗口3->正在卖第16张票
卖票窗口3->正在卖第15张票
卖票窗口3->正在卖第14张票
卖票窗口3->正在卖第13张票
卖票窗口3->正在卖第12张票
卖票窗口3->正在卖第11张票
卖票窗口3->正在卖第10张票
卖票窗口3->正在卖第9张票
卖票窗口3->正在卖第8张票
卖票窗口3->正在卖第7张票
卖票窗口3->正在卖第6张票
卖票窗口3->正在卖第5张票
卖票窗口3->正在卖第4张票
卖票窗口3->正在卖第3张票
卖票窗口3->正在卖第2张票
卖票窗口3->正在卖第1张票

方案3:使用Lock锁

Lock实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
使用Lock接口的实现类ReentrantLock。

使用步骤:

  • 1.创建一个成员ReentrantLock类对象
  • 2.在可能发生线程安全问题的代码前调用lock()方法加锁
  • 3.在可能发生线程安全问题的代码后调用unlock()方法释放锁
public class RunnableImpl implements Runnable {
    // 多线程共享的数据-票数
    private int ticket = 100;

    // 1.创建一个成员ReentrantLock类对象
    Lock l = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 2.在可能发生线程安全问题的代码前调用lock()方法加锁
            l.lock();
            if (ticket > 0) {
                // 为了提高线程安全问题出现的概率 在这里休眠一下
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "->正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 3.在可能发生线程安全问题的代码后调用unlock()方法释放锁
                    // jdk文档中推荐在finally中释放锁,无论是否发生异常,锁都要释放
                    l.unlock();
                }

            }
        }
    }
}

public class LockDemo {
    public static void main(String[] args) {
        Runnable r = new RunnableImpl();

        Thread t1 = new Thread(r,"卖票窗口1");
        Thread t2 = new Thread(r,"卖票窗口2");
        Thread t3 = new Thread(r,"卖票窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

输出结果

卖票窗口1->正在卖第100张票
卖票窗口1->正在卖第99张票
卖票窗口1->正在卖第98张票
卖票窗口1->正在卖第97张票
卖票窗口1->正在卖第96张票
卖票窗口1->正在卖第95张票
卖票窗口1->正在卖第94张票
卖票窗口1->正在卖第93张票
卖票窗口1->正在卖第92张票
卖票窗口1->正在卖第91张票
卖票窗口1->正在卖第90张票
卖票窗口1->正在卖第89张票
卖票窗口1->正在卖第88张票
卖票窗口1->正在卖第87张票
卖票窗口1->正在卖第86张票
卖票窗口1->正在卖第85张票
卖票窗口1->正在卖第84张票
卖票窗口1->正在卖第83张票
卖票窗口1->正在卖第82张票
卖票窗口1->正在卖第81张票
卖票窗口1->正在卖第80张票
卖票窗口1->正在卖第79张票
卖票窗口1->正在卖第78张票
卖票窗口1->正在卖第77张票
卖票窗口1->正在卖第76张票
卖票窗口1->正在卖第75张票
卖票窗口1->正在卖第74张票
卖票窗口1->正在卖第73张票
卖票窗口1->正在卖第72张票
卖票窗口1->正在卖第71张票
卖票窗口1->正在卖第70张票
卖票窗口1->正在卖第69张票
卖票窗口1->正在卖第68张票
卖票窗口1->正在卖第67张票
卖票窗口1->正在卖第66张票
卖票窗口1->正在卖第65张票
卖票窗口1->正在卖第64张票
卖票窗口2->正在卖第63张票
卖票窗口2->正在卖第62张票
卖票窗口2->正在卖第61张票
卖票窗口2->正在卖第60张票
卖票窗口2->正在卖第59张票
卖票窗口2->正在卖第58张票
卖票窗口2->正在卖第57张票
卖票窗口2->正在卖第56张票
卖票窗口2->正在卖第55张票
卖票窗口2->正在卖第54张票
卖票窗口2->正在卖第53张票
卖票窗口2->正在卖第52张票
卖票窗口2->正在卖第51张票
卖票窗口2->正在卖第50张票
卖票窗口2->正在卖第49张票
卖票窗口2->正在卖第48张票
卖票窗口2->正在卖第47张票
卖票窗口2->正在卖第46张票
卖票窗口2->正在卖第45张票
卖票窗口2->正在卖第44张票
卖票窗口2->正在卖第43张票
卖票窗口2->正在卖第42张票
卖票窗口2->正在卖第41张票
卖票窗口2->正在卖第40张票
卖票窗口2->正在卖第39张票
卖票窗口2->正在卖第38张票
卖票窗口2->正在卖第37张票
卖票窗口2->正在卖第36张票
卖票窗口2->正在卖第35张票
卖票窗口2->正在卖第34张票
卖票窗口2->正在卖第33张票
卖票窗口2->正在卖第32张票
卖票窗口2->正在卖第31张票
卖票窗口2->正在卖第30张票
卖票窗口2->正在卖第29张票
卖票窗口2->正在卖第28张票
卖票窗口2->正在卖第27张票
卖票窗口2->正在卖第26张票
卖票窗口2->正在卖第25张票
卖票窗口2->正在卖第24张票
卖票窗口2->正在卖第23张票
卖票窗口2->正在卖第22张票
卖票窗口2->正在卖第21张票
卖票窗口2->正在卖第20张票
卖票窗口2->正在卖第19张票
卖票窗口2->正在卖第18张票
卖票窗口2->正在卖第17张票
卖票窗口2->正在卖第16张票
卖票窗口2->正在卖第15张票
卖票窗口2->正在卖第14张票
卖票窗口2->正在卖第13张票
卖票窗口2->正在卖第12张票
卖票窗口2->正在卖第11张票
卖票窗口2->正在卖第10张票
卖票窗口2->正在卖第9张票
卖票窗口2->正在卖第8张票
卖票窗口2->正在卖第7张票
卖票窗口2->正在卖第6张票
卖票窗口2->正在卖第5张票
卖票窗口2->正在卖第4张票
卖票窗口2->正在卖第3张票
卖票窗口2->正在卖第2张票
卖票窗口2->正在卖第1张票

四、线程状态

4.1、线程状态定义

Java线程状态定义在Thread.State这个枚举类型中,源码如下:

public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
 }
线程状态 意义
NEW(新建状态) 线程刚被创建,但是并未启动。还没调用start方法。
RUNNABLE(可运行状态) 可运行线程的线程状态。处于可运行状态的某一线程正在 Java 虚拟机中运行,但它可能正在等待操作系统中的其他资源,比如处理器。
BLOCKED(阻塞状态) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING((无限等待状态) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify()或者notifyAll()才能够唤醒。
TIMED_WAITING(定时等待状态) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep()、Object.wait()。
TERMINATED(终止状态) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

4.2、线程状态转换图

4.3、线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

为什么要处理线程间通信?
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源?
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

4.3.1、等待唤醒机制

什么是等待唤醒机制?
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

代码示例

  1. 一个线程唤醒另一个处于等待状态的线程,主要用到wait()和notify()方法。
/**
 * 等待和唤醒案例:线程之间的通信
 *
 * 模仿买包子操作:
 * 创建一个顾客线程:告知老板包子的口味和数量,调用wait(),放弃cpu执行,进入WAITING((无限等待状态)。
 * 创建一个老板线程:花费5秒做包子,调用notify()唤醒顾客线程,包子已经做好。
 *
 * 注意:
 *      顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
 *      保证锁对象唯一
 *      只有锁对象才能调用wait()和notify()
 *
 *  Object中的类:
 *      public final void wait() throws InterruptedException 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
 *
 *      public final void notify() 唤醒在此对象监视器上等待的单个线程。
 */
public class WaitAndNotifyDemo {
    public static void main(String[] args) {
        // 创建锁对象,保证唯一
        Object obj = new Object();

        // 创建一个客户线程
        new Thread() {
            @Override
            public void run() {
                // 为了保证等待和唤醒只有一个线程在执行,这里需要使用同步
                synchronized (obj) {
                    System.out.println("顾客:告知老板包子的口味和数量,等待老板做包子...");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("顾客:开始吃新鲜出炉的包子");
                }
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                try {
                    // 老板线程休眠5秒,模仿做包子操作
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 为了保证等待和唤醒只有一个线程在执行,这里需要使用同步
                synchronized (obj){
                    System.out.println("老板:做好包子,通知顾客");
                    obj.notify();
                }
            }
        }.start();
    }
}

输出结果:

顾客:告知老板包子的口味和数量,等待老板做包子...
老板:做好包子,通知顾客
顾客:开始吃新鲜出炉的包子
  1. 等待一段时间后自然唤醒,只要使用带参的wait(long timeout)
public class WaitParmDemo {
    public static void main(String[] args) {
        // 创建锁对象,保证唯一
        Object obj = new Object();

        // 创建一个客户线程
        new Thread() {
            @Override
            public void run() {
                // 为了保证等待和唤醒只有一个线程在执行,这里需要使用同步
                synchronized (obj) {
                    System.out.println("顾客:告知老板包子的口味和数量,等待老板做包子...");
                    try {
                        // 进入等待状态,如果没有其他线程唤醒他,等5秒结束,自动唤醒自己
                        obj.wait(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("顾客:等了5秒还没好,我走了!!!");
                }
            }
        }.start();

    }
}

输出结果:

	顾客:告知老板包子的口味和数量,等待老板做包子...
	顾客:等了5秒还没好,我走了!!!
  1. 一个线程通知唤醒所有正在等待的线程,只要使用wait()和notifyAll()方法。
public class WaitAndNotifyAllDemo {
    public static void main(String[] args) {
        // 创建锁对象,保证唯一
        Object obj = new Object();

        // 创建一个客户线程
        new Thread() {
            @Override
            public void run() {
                // 为了保证等待和唤醒只有一个线程在执行,这里需要使用同步
                synchronized (obj) {
                    System.out.println("顾客1:告知老板包子的口味和数量,等待老板做包子...");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("顾客1:开始吃新鲜出炉的包子");
                }
            }
        }.start();

        // 创建一个客户线程
        new Thread() {
            @Override
            public void run() {
                // 为了保证等待和唤醒只有一个线程在执行,这里需要使用同步
                synchronized (obj) {
                    System.out.println("顾客2:告知老板包子的口味和数量,等待老板做包子...");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("顾客2:开始吃新鲜出炉的包子");
                }
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                try {
                    // 老板线程休眠5秒,模仿做包子操作
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 为了保证等待和唤醒只有一个线程在执行,这里需要使用同步
                synchronized (obj){
                    System.out.println("老板:做好包子,通知所有顾客");
                    obj.notifyAll();
                }
            }
        }.start();
    }
}

输出结果:

顾客1:告知老板包子的口味和数量,等待老板做包子...
顾客2:告知老板包子的口味和数量,等待老板做包子...
老板:做好包子,通知所有顾客
顾客1:开始吃新鲜出炉的包子
顾客2:开始吃新鲜出炉的包子
  1. 等待唤醒机制其实就是经典的“生产者与消费者”的问题。
// 资源类
public class BaoZi {
    // 皮
    String pi;
    // 馅
    String xian;
    // 包子状态 有包子:true 没包子:false 初始值是false
    Boolean flag  = false;
}

// 生产者包子铺类
public class BaoZiPu extends Thread {
    // 创建包子成员
    private BaoZi bz;


    // 使用带参构造方法 对包子成员变量赋值
    public BaoZiPu(BaoZi bz) {
        this.bz = bz;
    }

    // 设置线程任务:生成包子
    @Override
    public void run() {
        // 定义一个变量
        int count = 0;

        // 让包子铺一直生产包子
        while (true) {
            // 必须使用同步技术保证两个线程只有一个在执行
            synchronized (bz) {
                // 对包子的状态进行判断
                if (bz.flag == true) {
                    try {
                        // 包子铺调用wait方法进入等待状态
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 被唤醒之后执行,包子铺生成包子
                // 增加一些趣味性:交替生产两种包子
                if (count % 2 == 0) {
                    // 生产 薄皮三鲜馅包子
                    bz.pi = "薄皮";
                    bz.xian = "三鲜馅";
                } else {
                    // 生产 冰皮牛肉大葱馅包子
                    bz.pi = "冰皮";
                    bz.xian = "牛肉大葱馅";
                }
                count++;
                System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "包子");
                // 生产包子需要3秒钟
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 包子铺生产好包子
                // 修改包子的状态为有:true
                bz.flag = true;
                // 唤醒顾客线程,让顾客线程可以吃包子了
                bz.notify();
                System.out.println("包子铺生产好了:" + bz.pi + bz.xian + "包子,顾客可以准备吃了");
            }
        }
    }
}

// 消费者食客类
public class Eater extends Thread {
    // 创建包子成员
    private BaoZi bz;

    // 使用带参构造方法 对包子成员变量赋值
    public Eater(BaoZi bz){
        this.bz = bz;
    }
    @Override
    public void run() {
        // 定义一个变量
        int count = 0;

        // 死循环,让食客一直吃包子
        while (true) {
            // 必须使用同步技术保证两个线程只有一个在执行
            synchronized (bz) {
                // 对包子的状态进行判断
                if (bz.flag == false) {
                    try {
                        // 食客调用wait方法进入等待状态
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 被唤醒之后执行的代码,吃包子
                System.out.println("食客正在吃" + bz.pi + bz.xian + "包子");
                // 食客吃完包子
                // 修改包子的状态为没有:false
                bz.flag = false;
                // 食客唤醒包子铺生产包子
                bz.notify();

                System.out.println("食客已经把" + bz.pi + bz.xian + "包子吃完了,唤醒包子铺生产包子");
                System.out.println("=======================================================================");
            }
        }
    }
}

// 测试类
public class MainTest {
    public static void main(String[] args) {
        // 实例化包子对象
        BaoZi bz = new BaoZi();
        // 创建包子铺线程并开启,生产包子
        new BaoZiPu(bz).start();
        // 创建食客类线程并开启,吃包子
        new Eater(bz).start();
    }
}

输出结果:

包子铺正在生产:薄皮三鲜馅包子
包子铺生产好了:薄皮三鲜馅包子,顾客可以准备吃了
食客正在吃薄皮三鲜馅包子
食客已经把薄皮三鲜馅包子吃完了,唤醒包子铺生产包子
=======================================================================
包子铺正在生产:冰皮牛肉大葱馅包子
包子铺生产好了:冰皮牛肉大葱馅包子,顾客可以准备吃了
食客正在吃冰皮牛肉大葱馅包子
食客已经把冰皮牛肉大葱馅包子吃完了,唤醒包子铺生产包子
=======================================================================

五、线程池

5.1、线程池的介绍

线程池: 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建销毁线程对象的操作,节省了时间,也无需反复创建销毁线程而消耗过多资源。
合理利用线程池能够带来三个好处:

  • 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内
    存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

5.2、线程池的使用

线程池:由JDK1.5之后提供的
java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executors的静态方法:

static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。

java.util.concurrent.ExecutorService:线程池接口
ExecutorService的常用方法:

submit Future submit(Runnable task) 提交一个 Runnable 任务用于执行。

void shutdown() 关闭/销毁线程池。

使用线程池中线程对象的步骤:

  • 1.使用线程池的工厂类Executors里提供的newFixedThreadPool()方法创建指定线程数量的线程池
  • 2.创建Runnable接口的实现类,重写run()方法,设置线程任务。
  • 3.调用ExecutorService中的submit()方法,提交任务(Runnable接口实现类对象),开启线程,执行run()方法。
  • 4.调用ExecutorService中的shutdown()方法,关闭线程池(一般不推荐,违背了我们使用线程池复用的初衷)。

代码示例:

public class RunnableImpl implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行任务");
    }
}

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(2);
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());
        // pool-1-thread-2执行任务
        // pool-1-thread-1执行任务
        // pool-1-thread-1执行任务

        // 关闭/销毁线程池(不推荐)
        es.shutdown();

        // 关闭线程池后,使用线程开启任务报错
        // java.util.concurrent.RejectedExecutionException
        //es.submit(new RunnableImpl());
    }
}

你可能感兴趣的:(Java)