一、并发、并行、进程、线程概念

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

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

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

线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

我们可以再电脑底部任务栏,右键——->打开任务管理器,可以查看当前任务的进程:
进程
image.png
线程调度:

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

大部分操作系统都支持多进程并行运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

二、创建线程

继承Thread类

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

    示例1:

    ```java package com.alpaak.part10.demo1;

public class MyThread extends Thread { //定义指定线程名称的构造方法 public MyThread(String name) { //调用父类的String参数的构造方法,指定线程的名称 super(name); } /**

  1. * 重写run方法,完成该线程执行的逻辑
  2. */
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 200; i++) {
  6. System.out.println(getName()+"任务:"+i);
  7. }
  8. }

}

```java
package com.alpaak.part10.demo1;

public class Demo1 {
    public static void main(String[] args) {
        //创建自定义线程对象
        MyThread mt = new MyThread("新建的线程");
        //开启新线程
        mt.start();
        //在主方法中执行for循环
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程任务:"+i);
        }
    }
}

实现Runnable接口

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

    示例2:

    ```java package com.alpaak.part10.demo2;

/**

  • 实现Runnable */ public class MyRunnable implements Runnable {

    @Override public void run() {

     for (int i = 0; i < 200; i++) {
         System.out.println(Thread.currentThread().getName() + " " + i);
     }
    

    } }

```java
package com.alpaak.part10.demo2;

/**
 * 创建自定义类对象  线程任务对象
 */
public class Demo2 {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        Thread t = new Thread(mr, "新建的线程");
        t.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("主线程 : " + i);
        }
    }
}

继承Thread 和实现Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。总结:实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

三、线程常用方法

方法名 说明
public static void sleep(long millis) 当前线程主动休眠 millis 毫秒。
public static void yield() 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
public final void join() 允许其他线程加入到当前线程中。
public void setPriority(int) 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多。
public void setDaemon(boolean) 设置为守护线程线程有两类:用户线程(前台线程)、守护线程(后台线程)

线程的优先级

  • 我们可以通过传递参数给线程的 setPriority() 来设置线程的优先级别
  • 调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。优先级 : 只能反映 线程 的 中或者是 紧急程度 , 不能决定 是否一定先执行
    Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
          线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
          线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
          分配给线程的默认优先级,取值为5。

示例3

package com.alpaak.part10.demo3;

/**
 * 优先级
 *
 */
public class PriorityThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<50;i++) {
            System.out.println(Thread.currentThread().getName()+"============"+i);
        }
    }
}
package com.alpaak.part10.demo3;

public class TestPriority {
    public static void main(String[] args) {
        PriorityThread p1=new PriorityThread();
        p1.setName("p1");
        PriorityThread p2=new PriorityThread();
        p2.setName("p2");
        PriorityThread p3=new PriorityThread();
        p3.setName("p3");


        p1.setPriority(1);
        p3.setPriority(10);

        //启动
        p1.start();
        p2.start();
        p3.start();
    }
}

线程的休眠

使用线程的 sleep() 可以使线程休眠指定的毫秒数,在休眠结束的时候继续执行线程

示例4:

package com.alpaak.part10.demo4;

public class TestSleep {
    public static void main(String[] args) {
        new SleepThread().start();
    }
}

class SleepThread extends Thread {
    @Override
    public void run() {
        String[] names = new String[]{"zs", "ls", "ww", "z6"};
        int index = (int) (Math.random() * 4);
        for (int i = 3; i > 0; i--) {
            System.out.println(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("倒计时:" + i);
        }

        System.out.println("抽中学员为:" + names[index]);
    }
}

线程的让步

  • Thread.yield() 方法作用是:暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程。
  • yield() 做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用 yield() 的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证 **yield()** 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
  • 案例:创建两个线程A,B,分别各打印1000次,从1开始每次增加1,其中B一个线程,每打印一次,就yield一次,观察实验结果.

    示例5:

    ```java package com.alpaak.part10.demo5;

/**

  • 线程的礼让 */ class Task1 implements Runnable { @Override public void run() {
     for (int i = 0; i < 200; i++) {
         System.out.println("A:" + i);
     }
    
    } }

class Task2 implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(“B:” + i); Thread.yield(); } } }

public class Demo { public static void main(String[] args) { new Thread(new Task2()).start(); new Thread(new Task1()).start(); } }

**sleep()和yield()的区别**  <br />sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;<br />yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程**有可能在进入到可执行状态后马上又被执行**。        <br />sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程       <br />另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。 
<a name="VtWpE"></a>
## 线程的合并

- `Thread` 中,`join()`方法的作用是调用线程等待该线程完成后,才能继续往下运行。
- join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

![](https://cdn.nlark.com/yuque/0/2021/png/12417724/1625061414924-333a718f-fb21-4b39-9007-ef4151808be1.png#crop=0&crop=0&crop=1&crop=1&from=url&id=UraPe&margin=%5Bobject%20Object%5D&originHeight=1028&originWidth=799&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)

- **为什么要用join()方法**<br />在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
<a name="Sbwn6"></a>
### 示例6:
```java
package com.alpaak.part10.demo6;

/**
 * 线程的合并 - 抢占
 */
class JoinThread extends Thread{

    public JoinThread(String name){
        super(name);
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始运行");
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"-->子线程:"+i);
        }
        System.out.println(Thread.currentThread().getName()+"线程结束运行");
    }
}

public class JoinDemo {
    public static void main(String[] args) {
        System.out.println("主线程开始运行。。。");
        JoinThread t1 = new JoinThread("新加入的线程");

        t1.start();

//        try {
//            t1.join();
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }

        System.out.println("主线程开始结束。。。");
    }
}

四、守护线程

守护线程.setDaemon(true):设置守护线程
线程有两类:用户线程(前台线程)、守护线程(后台线程)
如果程序中所有前台线程都执行完毕了,后台线程会自动结束
垃圾回收器线程属于守护线程

示例7:

package com.alpaak.part10.demo7;

public class TestDeamon {
    public static void main(String[] args) {
        //创建线程(默认前台线程)
        DeamonThread d1 = new DeamonThread();
        //设置线程为守护线程
        d1.setDaemon(true);//主线程结束便结束了
        d1.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程:----------" + i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
class DeamonThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + "----------" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

五、线程生命周期

第10章 多线程 - 图2

五种基本状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
新建状态(New)
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable)
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running)
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

多线程状态之间的转换

就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

六、线程安全

为什么会出现线程安全问题?

  • 线程不安全:
    • 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
    • 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
    • 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

示例8: 不安全

package com.alpaak.part10.demo8;

class TicketRunnable implements Runnable{

    private int ticket=100;
    //每个窗口卖票的操作
    //窗口 永远开启
    @Override
    public void run() {
        while(true){//有票可以卖
            //出票操作


            if(ticket>0){
                 //使用sleep模拟一下出票时间 //模拟一下出票的时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在卖票:"+ticket--);
            }
        }

    }
}

public class ThreadSafe {

    public static void main(String[] args)  throws Exception{
        TicketRunnable t = new TicketRunnable();
        Thread t1 = new Thread(t,"窗口1");
        Thread t2 = new Thread(t,"窗口2");
        Thread t3 = new Thread(t,"窗口3");
        //3个窗口同时卖票
        t1.start();
        t2.start();
        t3.start();

    }
}

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

    同步代码块

    语法: synchronized(临界资源对象){ //对临界资源对象加锁
    //代码(原子操作) }

同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

示例9:

package com.alpaak.part10.demo9;

class Ticket2 implements Runnable{

    private int ticket=100;
    Object lock = new Object();
    //每个窗口卖票的操作
    //窗口 永远开启
    @Override
    public void run() {
        while(true){//有票可以卖
            synchronized(lock){//synchronized (this) {//this ---当前对象
                if(ticket>0){
                    //出票操作
                    //使用sleep模拟一下出票时间
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName()+"正在卖票:"+ticket--);
                }else {
                    break;
                }
            }
        }
    }
}


public class TicketDemo2 {
    public static void main(String[] args) {
        Ticket2 ticket2 = new Ticket2();
        Thread t1 = new Thread(ticket2,"窗口1");
        Thread t2 = new Thread(ticket2,"窗口2");
        Thread t3 = new Thread(ticket2,"窗口3");
        //3个窗口同时卖票
        t1.start();
        t2.start();
        t3.start();
    }
}

同步方法

同步方法 :使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

语法: synchronized 返回值类型 方法名称(形参列表){ //对当前对象(this)加锁
// 代码(原子操作) }

  • 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
  • 线程退出同步方法时,会释放相应的互斥锁标记。
  • 如果方式是静态,锁是类名.class。

示例10:

package com.alpaak.part10.demo10;

class Ticket3 implements Runnable{

    private int ticket=100;
    //Object lock = new Object();
    //每个窗口卖票的操作
    //窗口 永远开启
    @Override
    public void run() {
        while(true){//有票可以卖
            sellTicket();
            if(ticket<=0){
                break;
            }
        }
    }

    /**
     * 锁对象,谁调用这个方法,就是谁
     * 隐含锁对象,就是this
     *
     * 静态方法,隐含锁对象就是Ticket3.class
     */
    public synchronized  void sellTicket(){
        if(ticket>0){
            //出票操作
            //使用sleep模拟一下出票时间
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName()+"正在卖票:"+ticket--);
        }
    }
}


public class TicketDemo3 {
    public static void main(String[] args) {
        Ticket3 ticket3 = new Ticket3();
        Thread t1 = new Thread(ticket3,"窗口1");
        Thread t2 = new Thread(ticket3,"窗口2");
        Thread t3 = new Thread(ticket3,"窗口3");
        //3个窗口同时卖票
        t1.start();
        t2.start();
        t3.start();
    }
}

synchronized注意点
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

Lock

  • JDK5加入,与synchronized比较,显示定义,结构更灵活。
  • 提供更多实用性方法,功能更强大、性能更优越。

常用方法:

方法名 描述
void lock() 获取锁,如锁被占用,则等待。
boolean tryLock() 尝试获取锁(成功返回true。失败返回false,不阻塞)。
void unlock() 释放锁。

ReentrantLock:

  • Lock接口的实现类,与synchronized一样具有互斥锁功能。

    示例11:

    ```java package com.alpaak.part10.demo11;

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;

/**

  • Lock的使用 */ public class MyList { //创建锁 private Lock lock = new ReentrantLock(); private String[] str = {“A”,”B”,””,””,””}; private int count = 2;

    public void add(String value){

     //当没有锁的时候,会出现覆盖的情况
     str[count] = value;
     try {
         Thread.sleep(100);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     count++;
     System.out.println(Thread.currentThread().getName()+"添加了"+value);
    

    // lock.lock(); // try { // str[count] = value; // try { // Thread.sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } // count++; // System.out.println(Thread.currentThread().getName()+”添加了”+value); // }finally { // lock.unlock(); // } }

    public String[] getStr(){

     return str;
    

    } }

<a name="tasWO"></a>
# 七、线程通信
<a name="scjI5"></a>
## 概述
多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。<br />**为什么要处理线程间通信**<br />多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。<br />**如何保证线程间通信有效利用资源**<br />多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— **等待唤醒机制。**<br />![](https://cdn.nlark.com/yuque/0/2021/png/12417724/1625123404938-05cfcb1f-06ac-4c64-aac4-21cf399ebcfc.png#crop=0&crop=0&crop=1&crop=1&from=url&id=c2um5&margin=%5Bobject%20Object%5D&originHeight=908&originWidth=1992&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="fc2d2b74"></a>
## 等待唤醒机制
**什么是等待唤醒机制**<br />这是多个线程间的一种**协作**机制。谈到线程我们经常想到的是线程间的**竞争(race)**,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。<br />就是在一个线程进行了规定操作后,就进入等待状态(**wait()**), 等待其他线程执行完他们的指定代码过后 再将其唤醒(**notify()**);在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。<br />wait/notify 就是线程间的一种协作机制。<br />**线程通信方法**

| 方法 | 说明 |
| --- | --- |
| public final void wait() | 释放锁,进入等待队列 |
| public final void wait(long timeout) | 在超过指定的时间前,释放锁,进入等待队列 |
| public final void notify() | 随机唤醒、通知一个线程 |
| public final void notifyAll() | 唤醒、通知所有线程 |

注意:所有的等待、通知方法必须在对加锁的同步代码块中。<br />等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

1. wait:线程不再活动,不再参与调度,进入 wait set(锁池) 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个**特别的动作**,也即是“**通知(notify)**”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中<br />wait(long m):wait方法如果在指定的毫秒之后,还没有被notify唤醒,就会自动醒来<br />**sleep(long m):不会释放锁**
1. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
1. notifyAll:则释放所通知对象的 wait set 上的全部线程。
<a name="KQaco"></a>
### 示例12:
```java
package com.alpaak.part10.demo12;

/*

等待唤醒案例:
1,创建一个顾客线程(消费者):告知老板要的包子种类和数量,调用wait方法,放弃cpu的执行,进入等待状态
2,创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,通知顾客吃包子,

注意:
    顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
    同步使用的锁对象必须是唯一的,
    只有锁对象才能调用wait方法和notify方法


 */
public class Demo12 {
    public static void main(String[] args) {
        //创建锁对象,保证唯一
        Object obj =new Object();

        //创建顾客线程
        new Thread(){
            @Override
            public void run() {
               while(true){
                   //保证等待和唤醒只能有一个在执行
                   synchronized (obj){
                       System.out.println("告知老板要的包子种类和数量");
                       //进入等待
                       try {
                           obj.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }

                       //唤醒之后执行的代码
                       System.out.println("拿到包子,开始吃。。。");
                       System.out.println("---------------------");


                   }
               }
            }
        }.start();

        //创建老板线程
        new Thread(){
            @Override
            public void run() {

               while(true){
                   //花5秒钟做包子,
                   try {
                       Thread.sleep(5000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   //保证等待和唤醒只能有一个在执行
                   synchronized (obj){
                       System.out.println("包子做好了。。。。");
                       //做好包子之后,调用notify方法,通知顾客吃包子,
                       obj.notify();
                   }
               }
            }
        }.start();

    }
}

第10章 多线程 - 图3
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

    八、死锁

    多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
    如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
    第10章 多线程 - 图4

    示例13:

    ```java package com.alpaak.part10.demo13;

public class DeadLockDemo {

private static Object lock1 = new Object();//锁1,资源1
private static Object lock2 = new Object();//锁2,资源2


public static void main(String[] args) {

    //启动一个线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized(lock1){
                System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"等待锁2,资源2");
                synchronized (lock2){
                    System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
                }
            }
        }
    },"线程1").start();



    //产生死锁的线程

// new Thread(new Runnable() { // @Override // public void run() { // synchronized(lock2){ // System.out.println(Thread.currentThread().getName()+”拿到了锁2,资源2”); // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // System.out.println(Thread.currentThread().getName()+”等待锁1,资源1”); // synchronized (lock1){ // System.out.println(Thread.currentThread().getName()+”拿到了锁1,资源1”); // } // } // } // },”线程2”).start(); } }

<a name="x7QdR"></a>
## 死锁检测方法
[https://blog.csdn.net/weixin_35094408/article/details/114157598](https://blog.csdn.net/weixin_35094408/article/details/114157598)

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。<br />![](https://cdn.nlark.com/yuque/0/2021/png/12417724/1625128937399-6006e41e-896e-4dbf-91a8-3d0c0fbf9547.png#crop=0&crop=0&crop=1&crop=1&from=url&id=LUVDU&margin=%5Bobject%20Object%5D&originHeight=1442&originWidth=3203&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />**破坏死锁**<br />线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
```java
 //破坏死锁
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(lock1){
                    System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"等待锁2,资源2");
                    synchronized (lock2){
                        System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
                    }
                }
            }
        },"线程2").start();

九、线程池

概述

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:

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

第10章 多线程 - 图5

线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Java类库提供了许多静态方法来创建一个线程池:
Executors类中创建线程池的方法如下:
a、newFixedThreadPool 创建一个固定长度的线程池,当到达线程最大数量时,线程池的规模将不再变化。
b、newCachedThreadPool 创建一个可缓存的线程池,如果当前线程池的规模超出了处理需求,将回收空的线程;当需求增加时,会增加线程数量;线程池规模无限制。
c、newSingleThreadPoolExecutor 创建一个单线程的Executor,确保任务对了,串行执行
d、newScheduledThreadPool 创建一个固定长度的线程池,而且以延迟或者定时的方式来执行,类似Timer;

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

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)

获取到了一个线程池ExecutorService 对象,定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

  1. 关闭线程池(一般不做)。

    示例14:

    ```java package com.alpaak.part10.demo14;

import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit;

/**

  • 线程池的使用 */ public class ThreadPoolDemo { public static void main(String[] args) { // //创建一个包含固定数量的线程池对象 // ExecutorService executorService = Executors.newFixedThreadPool(2); // //创建一个包含单条线程的线程池 // ExecutorService executorService = Executors.newSingleThreadExecutor();

// //创建一个带缓冲区的线程池,会根据需求创建线程 // ExecutorService executorService = Executors.newCachedThreadPool();

    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);



    //创建Runnable实例对象
    MyThread r = new MyThread();

    //自己创建线程的方式

// Thread t = new Thread(r); // t.start();

// //从线程池中获取线程对象,然后调用MyThread的run方法 // executorService.submit(r); // //再获取一个线程对象, // executorService.submit(r); // executorService.submit(r); // //注意:submit方法调用后,程序并不终止,因为线程次控制了线程的关闭 // //使用完,又归还到了线程池中, // // //关闭线程池 // executorService.shutdown();

    for (int i = 0; i < 10; i++) {
        scheduledExecutorService.schedule(r,10, TimeUnit.SECONDS);//延迟10秒执行
    }

    scheduledExecutorService.shutdown();;//执行到此处并不会马上关闭连接池

// while(!scheduledExecutorService.isTerminated()){ // // } System.out.println(“Main Thread finished at”+new Date()); } } class MyThread implements Runnable{

@Override
public void run() {
    System.out.println("我要一个教练");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("教练来了:"+Thread.currentThread().getName());
    System.out.println("教完后,教练回到了游泳池");
}

}

<a name="2f7ac6dc"></a>
## Callable接口
一般情况下,使用Runnable接口、Thread实现的线程我们都是无法返回结果的。但是如果对一些场合需要线程返回的结果。就要使用用Callable、Future这几个类。Callable只能在ExecutorService的线程池中跑,但有返回结果,也可以通过返回的Future对象查询执行状态。Future 本身也是一种设计模式,它是用来取得异步任务的结果<br />看看其源码:
```java
public interface Callable<V> {
    V call() throws Exception;
}

它只有一个call方法,并且有一个返回V,是泛型。可以认为这里返回V就是线程返回的结果。
ExecutorService接口:线程池执行调度框架

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

示例15:

package com.alpaak.part10.demo15;

import java.util.Random;
import java.util.concurrent.*;

/**
 *Callable接口
 */
public class FutureTest {
    public static void main(String[] args) {
        System.out.println("main Thread begin at:" + System.nanoTime());
        //创建线程池对象
        ExecutorService executor = Executors.newCachedThreadPool();
        HandleCallable task1 = new HandleCallable("1");
        HandleCallable task2 = new HandleCallable("2");
        HandleCallable task3 = new HandleCallable("3");
        //执行
        Future<Integer> result1 = executor.submit(task1);
        Future<Integer> result2 = executor.submit(task2);
        Future<Integer> result3 = executor.submit(task3);
        executor.shutdown();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        //获取到返回的直接
        try {
            System.out.println("task1运行结果:" + result1.get());
            System.out.println("task2运行结果:" + result2.get());
            System.out.println("task3运行结果:" + result3.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("main Thread finish at:" + System.nanoTime());
    }

}

class HandleCallable implements Callable<Integer> {

    private String name;

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


    @Override
    public Integer call() throws Exception {
        System.out.println("task" + name + "开始进行计算");
        Thread.sleep(3000);
        int sum = new Random().nextInt(300);
        int result = 0;
        for (int i = 0; i < sum; i++)
            result += i;
        return result;
    }
}

十、线程安全集合

CopyOnWriteArrayList[重点]

  • 线程安全的ArrayList,加强版读写分离。
  • 写有锁,读无锁,读写之间不阻塞,优于读写锁。
  • 写入时,先copy一个容器副本、再添加新元素,最后替换引用。
  • 使用方式与ArrayList无异。

    示例16:

    ```java package com.alpaak.part10.demo16;

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

/**

  • 线程安全集合-CopyOnWriteArrayList */ public class TestCopyOnWriteArrayList { public static void main(String[] args) {

     //1创建集合
     CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<>();
     //2使用多线程操作
     ExecutorService es= Executors.newFixedThreadPool(5);
     //3提交任务
     for(int i=0;i<5;i++) {
         es.submit(new Runnable() {
    
             @Override
             public void run() {
                 for(int j=0;j<10;j++) {
                     list.add(Thread.currentThread().getName()+"...."+new Random().nextInt(1000));
                 }
             }
         });
     }
     //4关闭线程池
     es.shutdown();
     while(!es.isTerminated()) {}
     //5打印结果
     System.out.println("元素个数:"+list.size());
     for (String string : list) {
         System.out.println(string);
     }
    

    } }

    **CopyOnWriteArrayList如何做到线程安全的**<br />CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。<br />当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。<br />![](https://cdn.nlark.com/yuque/0/2021/png/12417724/1624970039600-c700ee0e-6c59-49f9-a5aa-cfdd70b11df9.png#crop=0&crop=0&crop=1&crop=1&from=url&id=Tr341&margin=%5Bobject%20Object%5D&originHeight=690&originWidth=913&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />当元素在新数组添加成功后,将array这个引用指向新数组。<br />![](https://cdn.nlark.com/yuque/0/2021/png/12417724/1624970285530-db61f145-06dd-4a77-a15a-d59ba8aa2c16.png#crop=0&crop=0&crop=1&crop=1&from=url&id=zyzd2&margin=%5Bobject%20Object%5D&originHeight=726&originWidth=908&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />`CopyOnWriteArrayList`的整个add操作都是在**锁**的保护下进行的。 这样做是为了避免在多线程并发add的时候,**复制出多个副本出来**,把数据搞乱了,导致最终的数组数据不是我们期望的。<br />`CopyOnWriteArrayList`的`add`操作的源代码如下:
    ```java
    public boolean add(E e) {
     //1、先加锁
     final ReentrantLock lock = this.lock;
     lock.lock();
     try {
         Object[] elements = getArray();
         int len = elements.length;
         //2、拷贝数组
         Object[] newElements = Arrays.copyOf(elements, len + 1);
         //3、将元素加入到新数组中
         newElements[len] = e;
         //4、将array引用指向到新数组
         setArray(newElements);
         return true;
     } finally {
        //5、解锁
         lock.unlock();
     }
    }
    

    由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
    1、如果写操作未完成,那么直接读取原数组的数据;
    2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
    3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
    可见,CopyOnWriteArrayList的读操作是可以不用加锁的。

CopyOnWriteArraySet

示例17:

package com.alpaak.part10.demo17;

import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 线程安全集合-CopyOnWriteArraySet
 */
public class TestCopyOnWriteArraySet {
    public static void main(String[] args) {
        //1创建集合
        CopyOnWriteArraySet<String> set=new CopyOnWriteArraySet<>();
        //2添加元素
        set.add("pingguo");
        set.add("huawei");
        set.add("xiaomi");
        set.add("lianxiang");
        set.add("pingguo");
        //3打印
        System.out.println("元素个数:"+set.size());
        System.out.println(set.toString());
    }
}

ConcurrentHashMap[重点]

  • 初始容量默认为16段(Segment),使用分段锁设计。
  • 不对整个Map加锁,而是为每个Segment加锁。
  • 当多个对象存入同一个Segment时,才需要互斥。
  • 最理想状态为16个对象分别存入16个Segment,并行数量16。
  • 使用方式与HashMap无异。

    示例18:

    ```java package com.alpaak.part10.demo18;

import java.util.concurrent.ConcurrentHashMap;

/**

  • 线程安全集合-ConcurrentHashMap */ public class TestConcurrentHashMap { public static void main(String[] args) {

     //1创建集合
     ConcurrentHashMap<String, String> hashMap=new ConcurrentHashMap<String, String>();
     //2使用多线程添加数据
     for(int i=0;i<5;i++) {
         new Thread(new Runnable() {
    
             @Override
             public void run() {
                 for(int k=0;k<10;k++) {
                     hashMap.put(Thread.currentThread().getName()+"--"+k, k+"");
                     System.out.println(hashMap);
                 }
             }
         }).start();
     }
    

    }

}

```