多线程和线程池入门

多线程入门

1.多线程的基本概念

熟悉什么是进程,线程,程序,并行和并发,线程的分类等概念。

1.1 程序,进程和线程

程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程(生命周期)。如:运行中的QQ,运行中的MP3播放器。

程序是静态的,进程是动态的,进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小

一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

1.2 单核,多核以及并发并行

 单核CPU和多核CPU的理解:

  • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。

  • 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)

  • 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

并行与并发

  • 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

  • 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

线程的分类:

一种是守护线程,一种是用户线程

2.多线程的创建和使用

2.1 创建多线程的方式

创建多线程的方式有四种,分别是:

  1. 创建一个类继承Thread类并重写run()方法;

  2. 创建一个类实现Runnable接口并重写run()方法

  3. 创建一个类实现Callable接口并重写call()方法

  4. 实现线程池的方式

    3和4的实现方式在后面线程池中进行讲解。

一,方式1的示例:创建一个类继承Thread类并重写run()方法;

package com.ethan.threads;

/**
 * 创建多线程的方式
 * 方式1:继承Thread类:
 * 1.创建一个继承Thread类的子类MyThread类并重写run()方法,将该线程执行的任务写在run()方法中
 * 2.创建MyThread类的对象并调用其start()方法启动线程
 *
 * 例子:线程1遍历1000以内的所有偶数,主线程遍历1000以内的奇数。
 */
public class ThreadsCreate {
    public static void main(String[] args) {
        MyThread th1 = new MyThread();
        th1.start();
        for (int i = 0; i < 1000; i++) {
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName()+": "+i);
            }
        }

    }

}

class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+": "+i);
            }
        }
    }
}

Notes:

  1. 开启一个新线程需要调用start()方法,而不是run();
  2. 一个线程对象start()只能被调用一次执行,如果当前线程已经在执行,想要再创建一个当前任务的线程,需要重新new一个当前线程的对象,而不能直接再调用start()方法,注:每个线程执行时候会被添加一个状态用来判断当前线程的状态;

二,方式2的示例:创建一个类实现Runnable接口并重写run()方法

​ 我们如果有一个业务例子:创建三个窗口卖票,总票数为100张,实现三个窗口线程去卖100张票,如果按照方式一的话,那么需要将票数变量设置为static类型的静态变量,这时候一般来说不太合适。所以这时候我们引出

第二种创建线程的方式:

  1. 创建一个实现了Runnable接口的类,并实现Runnable中的抽象run()方法;

  2. 创建该实现类的对象,将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;

  3. 通过Thread类的对象调用start(),可以满足创建多个线程操作同一个对象

    比较前两种创建线程的方式

    开发中优先选择实现Runnable接口的方式的原因:

    1. 第二种实现接口的方式没有类的单继承性的局限性;
    2. 第二种实现接口的方式更适合来处理多个线程有共享数据的情况
package com.ethan.threads;

/**
 * 例子:创建三个窗口卖票,总票数为100张
 * 存在线程安全问题  待解决
 */
public class WinTest {

    public static void main(String[] args) {
        WindowsThreads winTh1= new WindowsThreads();
        Thread windows1 = new Thread(winTh1);
        Thread windows2 = new Thread(winTh1);
        Thread windows3 = new Thread(winTh1);

        windows1.setName("窗口1");
        windows2.setName("窗口2");
        windows3.setName("窗口3");

        windows1.start();
        windows2.start();
        windows3.start();

    }
}

class WindowsThreads implements Runnable {

    private int tickets = 10000;
    @Override
    public void run() {
        while (true){
            if (tickets > 0){
                System.out.println(Thread.currentThread().getName()+": 卖出了第"+tickets+"票");
                tickets--;
            }else {
                break;
            }
        }
    }
}

Notes:

  • 第二种实现接口的方式创建线程运用了设计模式的静态代理模式
  • 第二种实现接口的方式创建线程可以满足多个线程操作同一个对象的数据的要求,当然要注意线程安全的问题

2.2 线程的方法解析

线程的几个常用方法其实涉及到了线程的生命周期,优先级等概念。

yield():	暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法
join():		当某个程序执行过程中调用其他线程的 join() 方法时,当前的调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。低优先级的线程也可以获得执行
stop():		已过时。当执行此方法时,强制结束当前线程。
sleep():	令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。

3. 线程的优先级和调度

线程被CPU调用的概率或者说调用的先后顺序其实是有优先级的,所以Thread类定义了线程的优先级等级的静态变量。

/**
* The minimum priority that a thread can have.
*/
	public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
	public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
	public final static int MAX_PRIORITY = 10;

涉及的方法

getPriority() 返回线程优先值

setPriority(int newPriority) 改变线程的优先级

说明

  • 线程创建时继承父线程的优先级

  • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

Java的调度方法

同优先级线程组成先进先出队列(先到先服务),使用时间片策略

对高优先级,使用优先调度的抢占式策略

4. 线程的生命周期

JDK中用Thread.State类(Thread类中的内部类)定义了线程的几种状态,要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态

  • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;

  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源;

  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能;

  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态;

  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束;

5. 线程的同步(*)

​ 该部分涉及到线程的安全问题,非常重要。

​ 上述方式二的例子,模拟三个窗口同时卖总数为100张的票,如何保证每个窗口卖出的票是正确的,而不是出现有重票,错票的线程不安全的问题。在java中,我们通过同步机制,用来解决线程的安全问题

5.1 同步机制中的锁的概念

​ 防止两个任务访问相同的资源(其实就是多个线程访问并操作同一个资源)。 防止这种冲突的方法就是当资源被一个线程操作使用时,在资源上加锁。第一个访问某项资源的任务(线程)必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,即使当前的任务被阻塞,其他的线程也得等待该线程操作完该资源释放锁之后才能拿到这个资源进行操作,而在其被解锁之时,另一个任务就可以锁定并使用它了。

​ java中的关键字synchronized就是用来加锁的。

注意:

  • 线程的同步解决了线程不安全的问题,但是在操作同步代码时候,这时候只能有一个线程参与,其他线程等待,其实此时相当于单线程的过程,效率低。

5.2 线程安全的解决方式(两种)

注意:

​ 以下两种解决线程安全的方式:同步代码块和同步方法是JDK5之前的方式,在死锁问题下有新的解决线程安全的方式,是在JDK5出来的,详细的看下面Lock锁部分

解决方式一:同步代码块

​ 对那些操作了共享数据的代码包括起来加上同步机制。

语法如下:

 synchronized (同步监视器lock){
     //需要被同步的代码
 }

说明:

1. 操作共享数据的代码,就是需要被同步的代码,因为该代码里有需要加锁同步的数据;
2. 共享数据就是多个线程任务都会操作的数据变量,比如实例的tickets变量;
  1. 同步监视器lock,其实就是同步锁,锁的定义是:可以是任意一个类的对象,但是其限定的要求:必须是多个线程都共有的对象,一般可以直接使用this作为锁。
package com.ethan.threads;

public class WinTest {

    public static void main(String[] args) {
        WindowsThreads winTh1= new WindowsThreads();
        Thread windows1 = new Thread(winTh1);
        Thread windows2 = new Thread(winTh1);
        Thread windows3 = new Thread(winTh1);

        windows1.setName("窗口1");
        windows2.setName("窗口2");
        windows3.setName("窗口3");
        
        windows1.start();
        windows2.start();
        windows3.start();

    }
}
/**
 * 
 *  synchronized (同步监视器lock){
 *      //需要被同步的代码
 *  }
 * 
 * 说明:1.操作共享数据的代码,就是需要被同步的代码,因为该代码里有需要加锁同步的数据
 *      2.共享数据就是多个线程任务都会操作的数据变量,比如实例的tickets变量
 *      3.同步监视器lock,其实就是同步锁,锁的定义是:可以是任意一个类的对象,但是其限定的要求:必须是多个线程都共有的对象;
 * 
 */
class WindowsThreads implements Runnable {

    private int tickets = 100;
    private Object lock = new Object();
    @Override
    public void run() {
        while (true){
            synchronized (lock){
                if (tickets > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+": 卖出了第"+tickets+"票");
                    tickets--;
                }else {
                    break;
                }
            }

        }
    }
}

对于锁的一些常用的使用方式:

  1. 对于实现了Runnable接口的方式二来说,可以考虑使用this充当同步监视器的锁;
  2. 对于继承了Thread类的方式一来说,慎用this充当同步监视器的锁,但是只要满足这个对象是多个任务(线程)都共有该对象即可!!,可以考虑使用this.getClass()对象来充当同步监视器的锁;
package com.ethan.threads;

public class WinTest {

    public static void main(String[] args) {
        WindowsThreads winTh1= new WindowsThreads();
        Thread windows1 = new Thread(winTh1);
        Thread windows2 = new Thread(winTh1);
        Thread windows3 = new Thread(winTh1);

        windows1.setName("窗口1");
        windows2.setName("窗口2");
        windows3.setName("窗口3");
        
        windows1.start();
        windows2.start();
        windows3.start();

    }
}
class WindowsThreads implements Runnable {

    private int tickets = 100;
    @Override
    public void run() {
        while (true){
            //直接使用this对象当作同步锁
            synchronized (this){
                if (tickets > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+": 卖出了第"+tickets+"票");
                    tickets--;
                }else {
                    break;
                }
            }

        }
    }
}

package com.ethan.threads;


public class ThreadsCreate {
    public static void main(String[] args) throws InterruptedException {
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        MyThread th3 = new MyThread();

        th1.setName("窗口1");
        th2.setName("窗口2");
        th3.setName("窗口3");
        th1.start();
        th2.start();
        th3.start();


    }

}

class MyThread extends Thread{
    private static int tickets = 100;
    @Override
    public void run() {
            while (true){
                //学习过反射知道this.getClass(),类也是对象,而且是唯一的。
                synchronized (this.getClass()){
                    if (tickets > 0){
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+": 卖出了第"+tickets+"票");
                        tickets--;
                    }else {
                        break;
                    }
                }

            }
    }
}

解决方式二:同步方法

​ 将需要同步的代码块声明成一个方法,并加上synchronized的关键字。

关于同步方法的总结:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  2. 非静态的同步方法,同步监视器是:this。对应实现接口生成线程的方式;
  3. 静态的同步方法,同步监视器是:当前类本身的类对象,对应继承生成线程的方式;

实现接口的方式一:静态的同步方法

package com.ethan.lockThread;


class WindowsThreads2 extends Thread {
    //注意变量也是static的
    private static int tickets = 100;
    @Override
    public void run() {
        while (true){
            ticketsShow();
            if (tickets <=0) break;
        }
    }
    //注意是静态方法的,同步监视器默认是WindowsThreads2.class
    private static synchronized void ticketsShow(){
        if (tickets > 0){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+": 卖出了第"+tickets+"张票");
            tickets--;
        }
    }

}

public class Windows1LockTest {

    public static void main(String[] args) {
        WindowsThreads2 wh1 = new WindowsThreads2();
        WindowsThreads2 wh2 = new WindowsThreads2();
        WindowsThreads2 wh3= new WindowsThreads2();


        wh1.start();
        wh2.start();
        wh3.start();
    }
}

实现接口的方式二:非静态的同步方法

package com.ethan.lockThread;

class WindowsThreads implements Runnable {

    private int tickets = 500;
    @Override
    public void run() {
        while (true){
            ticketsShow();
            if (tickets <=0) break;
        }
    }
    //同步监视器默认是this
    private synchronized void ticketsShow(){
        if (tickets > 0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+": 卖出了第"+tickets+"票");
            tickets--;
        }
    }

}

public class Windows2LockTest {

    public static void main(String[] args) {
        WindowsThreads win1 = new WindowsThreads();
        Thread th1 = new Thread(win1);
        Thread th2 = new Thread(win1);
        Thread th3 = new Thread(win1);

        th1.start();
        th2.start();
        th3.start();
    }
}

5.3 同步的范围

1、如何找问题,即代码是否存在线程安全?(非常重要)

(1)明确哪些代码是多线程运行的代码;

(2)明确多个线程是否有共享数据;

(3)明确多线程运行代码中是否有多条语句操作共享数据;

2、如何解决呢?(非常重要)

  • 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中

3、切记:

 范围太小:没锁住所有有安全问题的代码;

 范围太大:没发挥多线程的功能;

5.4 释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

5.5 不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行;
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器);
  • 应尽量避免使用suspend()和resume()来控制线程;

6. 线程的死锁问题

6.1 死锁的定义和示例

先看两个示例:

​ 两个s1和s2分别当作两把同步锁,那么在两个线程中,当对方都分别执行了第一段同步的代码,也就是线程1,拿着s1这把锁,执行了前半段,然后睡眠4秒,线程b可能此时拿着s2执行了前半段然后睡眠4秒,都睡眠四秒后,线程1和线程2都需要拿到对方手里的锁,但是对方都没法释放这把锁,于是两个线程在那干瞪眼等着,谁都无法继续执行下去,这就是死锁。

概念:

​ 不同的线程分别占用对方需要的同步资源(其实就是同步锁)不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

package com.ethan.lockThread;

public class DeadLockThreadTest {

    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");

                    System.out.println("th1的前半段:"+s1);
                    System.out.println("th1的前半段:"+s2);
                    try {
                        Thread.sleep(4000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println("th1的后半段:"+s1);
                        System.out.println("th1的后半段:"+s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    System.out.println("th2的前半段:"+s1);
                    System.out.println("th2的前半段:"+s2);
                    try {
                        Thread.sleep(4000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println("th2的后半段:"+s1);
                        System.out.println("th2的后半段:"+s2);
                    }
                }
            }
        }).start();
    }
}

package com.ethan.lockThread;

class A {
	public synchronized void foo(B b) {//同步监视器a对象
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步监视器a对象
		System.out.println("进入了A类的last方法内部");
	}
}

class B {
	public synchronized void bar(A a) {//同步监视器b对象
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步监视器b对象
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}

	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();
		dl.init();
	}
}

解决方法:

专门的算法、原则

尽量减少同步资源的定义

尽量避免嵌套同步

6.2 Lock锁(解决线程安全)

​ 上面的线程同步部分已经写出了两种解决线程同步的方法,JDK5新增了Lock锁的方式解决线程安全问题。从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

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

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

注意,锁的限定要求仍要满足该锁对象是多个线程共有的对象,否则仍然是线程不安全的。

class A{
    //仍然要保证该锁对象是多个线程共有的对象
	private final ReentrantLock lock = new ReenTrantLock();
	public void m(){
        //手动加锁
		lock.lock();
		try{
		//保证线程安全的代码,需要同步的代码
		finally{
            //手动释放锁
			lock.unlock(); 
		}
    }
}

示例:两个储户分别向同一个账户进行存钱,重点:一定要注意Lock锁对象一定是多个线程共同拥有的

package com.ethan.lockThread;


import java.util.concurrent.locks.ReentrantLock;

/**
 * 账户类,含有余额,存钱的方法含有共享数据cashMoney
 */
class Account{
    private double cashMoney = 0;
    private ReentrantLock lock = new ReentrantLock(true);
    public Account(double cashMoney) {
        this.cashMoney = cashMoney;
    }

    public double getCashMoney() {
        return cashMoney;
    }

    //存钱的操作,每次存入amt
    public void deposit(double amt) {
        try {
            lock.lock();
            if (cashMoney<3000){
                cashMoney+=amt;
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+": 此时账户余额为:"+cashMoney);
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}
/**
 * 储户类,可以有多个储户(多个线程),向同一个账户Account进行存钱
 */
class Customers implements Runnable{
    private Account acc;

    public Customers(Account acc) {
        this.acc = acc;
    }

    @Override
    public void run() {
        while (true){
            if (acc.getCashMoney()>3000) break;
            acc.deposit(100);
        }
    }
}
public class AccountTest {

    public static void main(String[] args) {
        Account acc = new Account(0);
        Customers c1 = new Customers(acc);
        Customers c2 = new Customers(acc);
        Thread th1 = new Thread(c1);
        Thread th2 = new Thread(c2);
        th1.setName("储户1");
        th2.setName("储户2");
        th1.start();
        th2.start();
    }
}

6.3 synchronized与Lock的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放;

  2. Lock只有代码块锁,synchronized有代码块锁和方法锁;

  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类);

  • 一个非常重要的点:

    ​ 不论synchronized的同步代码块还是同步方法,以及lock锁,都要记住一个核心的概念:

都存在锁,而且这个锁必须满足多个线程都必须共同拥有该锁的对象,否则是无法解决线程安全的。

  • 优先使用顺序:

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

7. 线程的通信

线程间的通信其实就是几个线程间共同操作共享数据时候,如何使得某个线程操作共享数据到某个阶段,然后交给其他线程处理共享数据的过程。

​ 举个生活中的例子,就像大厨先把一条鱼去了鳞片然后切成了片状,但是这时候鱼不能直接让大厨接着去把鱼煮成鱼汤,而是需要大厨的帮手先把鱼拿去清洗干净并且放在盘子里进行添加一些食材进行腌制,这个过程大厨跟帮手就是两个线程,两者需要对同一个共享数据(鱼)进行交叉搭配处理,才能把鱼做成最后的鱼汤,所以鱼(共享数据)在某个阶段需要交替给不同的人(线程)去处理,这就是线程间的通信,主要是涉及到几个方法以及每个方法执行后有哪些操作。

wait() notify() notifyAll()

  • wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行;

  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待;

  • notifyAll ():唤醒正在排队等待资源的所有线程结束等待;

  • 这三个方法只有在synchronized方法或synchronized代码块中才能使用,原因是因为这三个方法的调用者必须是同步代码块或者同步方法中的同步监视器,否则会报java.lang.IllegalMonitorStateException异常;

  • lock锁的方式目前是无法用这三个方法,在JUC中会学习;

  • 因为这三个方法必须是被锁对象(被同步监视器)调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明,而不是Thread类中定义的。

  • notify与notifyAll没有太多的区别,只是notify仅唤醒一个线程并允许它去获得锁,notifyAll是唤醒所有等待这个对象的线程并允许它们去获得对象锁,只要是在synchronied块中的代码,没有对象锁是寸步难行的。其实唤醒一个线程就是重新允许这个线程去获得对象锁并向下运行。顺便说一下notifyall,虽然是对每个wait的对象都调用一次notify,但是这个还是有顺序的,每个对象都保存这一个等待对象链,调用的顺序就是这个链的顺序。其实启动等待对象链中各个线程的也是一个线程,在具体应用的时候,需要注意一下。

sleep()和wait()方法的异同

​ 都会将当前线程进入阻塞状态。

  • sleep()方法:是Thread类的静态方法,不涉及到线程间同步概念,仅仅为了让一个线程自身获得一段沉睡时间。sleep可以在任何地方使用。并且阻塞过程中不会释放锁(同步监视器);
  • wait()方法:是Object类的方法,解决的问题是线程间的同步,该过程包含了同步锁的获取和释放,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify()方法才会重新激活调用者。虽然此时该线程进入阻塞状态,但是同步监视器会被释放。 在调用wait的时候,线程自动释放其占有的对象锁,同时不会去申请对象锁。当线程被唤醒的时候,它才再次获得了去获得对象锁的权利。
  • 注意:线程调用notify()之后,只有该线程完全从 synchronized代码里面执行完毕后,monitor才会被释放,被唤醒线程才可以真正得到执行权。

7.1 理解同步监视器对象和多线程的通信过程(*):

​ 我们学习完线程的同步肯定有下面的困惑:

  1. 为什么这三个方法是在Object类中定义的?

  2. 为什么这三个方法只能在synchronized方法或synchronized代码块中才能使用?

  3. 这三个方法调用时候整个多线程中各个线程的状态和他们是如何跟同步代码块的同步监视器进行关联的?

  4. 为什么同步监视器必须被所有线程共有?

    通过下面一篇博文来理解上述的疑问,非常重要!!!

​ 在Java中,所有对象都能够被作为"监视器monitor",指的是每个对象默认自身拥有一个独占锁,一个入口队列和一个等待队列的实体entity,所有对象的非同步方法都能够在任意时刻被任意线程调用,此时不需要考虑加锁的问题。

​ 我们知道,对于一个对象的同步方法来说,在任意时刻有且仅有一个拥有该对象独占锁(其实就是同步代码块/同步代码中那个显式或者隐式的同步监视器对象)的线程能够调用它们。

​ 我们知道,一个同步方法是被某个线程独占的。如果在线程A调用某一对象M(这个对象M其实就是同步代码块/同步代码中的同步监视器)的同步方法时,这个对象M的独占锁是被其他线程B拥有着,那么当前线程A将处于阻塞状态并且会将该线程A添加到这个对象M的入口队列中,此时线程A等待这个对象M在其他线程B中执行notify()/notifyAll ()方法后,该线程A会被“唤醒”,然后去跟其他线程争夺对象M的独占锁,如果争夺到,那么线程A就会在其阻塞处继续执行其代码;

只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。其实通俗理解就是只有当某个线程拥有同步监视器这个对象时候,那么这个线程才能够知晓其他线程的状态,并且通过这个对象去控制其他线程的操作,比如将其他线程唤醒,为什么这个对象可以去控制其他线程的唤醒notify()和notifyAll()呢,因为这个对象是被所有对象所共有的,这也解释了同步监视器为什么要被所有线程共有的原因,这一点通常不会被程序员注意,因为程序验证通常是在对象的同步方法或同步代码块中调用它们的。如果尝试在未获取对象锁时调用这三个方法,会抛出"java.lang.IllegalMonitorStateException:current thread not owner"异常

当一个线程正在某一个对象的同步方法中运行时调用了这个对象的wait()方法,那么这个线程将释放该对象的独占锁并被放入这个对象的等待队列。注意,wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。

当某线程调用某对象的notify()或notifyAll()方法时,任意一个(对于notify())或者所有(对于notifyAll())在该对象的等待队列中的线程,将被转移到该对象的入口队列。接着这些队列(译者注:可能只有一个)将竞争该对象的锁,最终获得锁的线程继续执行。如果没有线程在该对象的等待队列中等待获得锁,那么notify()和notifyAll()将不起任何作用。在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。

对于处于某对象的等待队列中的线程,只有当其他线程调用此对象的notify()或notifyAll()方法时才有机会继续执行。

​ 调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:"特殊状态已经被设置"。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。

7.2 生产者消费者问题(*)

​ 生产者线程向缓冲区中写入数据,消费者线程从缓冲区中读取数据。消费者线程需要等待直到生产者线程完成一次写入操作。生产者线程需要等待消费者线程完成一次读取操作。假设wait(),notify(),notifyAll()方法不需要加锁就能够被调用。此时消费者线程调用wait()正在进入状态变量的等待队列(译者注:可能还未进入)。在同一时刻,生产者线程调用notify()方法打算向消费者线程通知状态改变。那么此时消费者线程将错过这个通知并一直阻塞。因此,对象的wait(),notify(),notifyAll()方法必须在该对象的同步方法或同步代码块中被互斥地调用。

package com.ethan.ThreadSum;


/**
 * 难理解的点在于,生产者消费者的操作,不论是消费还是生产,
 * 其实保证数据的安全性都是在缓冲对象其实也就是同步锁的对象中操作的(同步监视器对象的同步方法)
 * 然后生产者消费者的线程run()中,包含了缓冲区的同步方法。
 */

class Clerk{
    private int productCount;

    public Clerk(int productCount) {
        this.productCount = productCount;
    }
    //生产商品,必须保证共享对象的安全性
    public synchronized void produceProduct(){
        if(productCount < 20){//商品少于20,开始生产
            productCount++;
            System.out.println(Thread.currentThread().getName()+"开始生产第"+productCount+"个商品~");
            notify();//已经有了商品了,唤醒消费者进行消费
        }else {
            try {
                wait();//商品大于20个,等待中,阻塞中,无法继续生产
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //卖掉商品
    public synchronized void comsumeProduct(){
       if(productCount > 0){//商品大于0,可以购买消费
           System.out.println(Thread.currentThread().getName()+"开始消费第"+productCount+"个商品~");
           productCount--;
           notify();//消费一个商品,生产者可以继续生产新的产品
       }else {
           try {
               wait();//商品等于0,无法消费,等待商品上架
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }
}


class Producer implements Runnable{
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println("生产者开始生产产品~");
        while (true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }   
}

class Consumer implements Runnable{
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("消费者开始消费产品~");
        while (true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.comsumeProduct();
        }
    }

}
public class ProductTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk(3);
        Producer p = new Producer(clerk);
        Consumer c = new Consumer(clerk);
        Thread p1 = new Thread(p);
        Thread c1 = new Thread(c);
        Thread c2 = new Thread(c);

        p1.setName("生产者1");
        c1.setName("消费者1");
        c2.setName("消费者2");

        p1.start();
        c1.start();
        c2.start();
    }
}

8. 线程池的创建和使用(*)

开发中多用以下两种方式。

8.1 方式三:实现Callable接口

使用Callable接口实现多线程的步骤:

  1. 创建实现Callable接口的类并重写call()方法,创建该类的对象;
  2. 创建FutureTask对象,并将该类的对象传入FutureTask的构造方法中。注意:FutureTask实现了Runnable接口和Future接口;
  3. 实例化Thread对象,并在构造方法中传入FurureTask对象;
  4. 启动Thread对象的start()方法;
package com.ethan.ThreadSum;


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

class CallThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
                sum +=i;
            }
        }
        return sum;
    }
}
public class CallableThreadTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallThread ct = new CallThread();
        FutureTask<Integer> ft = new FutureTask(ct);
        Thread th = new Thread(ft);
        th.start();
        Integer sum = ft.get();
        System.out.println("总和为:"+sum);


    }
}

与使用Runnable相比, Callable功能更强大些。

 相比run()方法,可以有返回值;

 方法可以抛出异常,被外面的操作捕获到异常的信息;

 支持泛型的返回值;

 需要借助FutureTask类,可以获取call()方法的返回结果;

8.2 方式四:线程池

8.2.1 线程池的基本概念

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

我们先来看看线程池带来了哪些好处。

  1. 降低资源的消耗。线程本身是一种资源,创建和销毁线程会有CPU开销;创建的线程也会占用一定的内存。
  2. 提高任务执行的响应速度。任务执行时,可以不必等到线程创建完之后再执行。
  3. 提高线程的可管理性。线程不能无限制地创建,需要进行统一的分配、调优和监控。

接下来,我们看看不使用线程池有哪些坏处。

  1. 频繁的线程创建和销毁会占用更多的CPU和内存
  2. 频繁的线程创建和销毁会对GC产生比较大的压力
  3. 线程太多,线程切换带来的开销将不可忽视
  4. 线程太少,多核CPU得不到充分利用,是一种浪费

8.2.2 Java线程池API的简单理解

  • Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
  • 然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
  • 抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
  • 然后ThreadPoolExecutor继承了类AbstractExecutorService。ThreadPoolExecutor是核心类,在ThreadPoolExecutor类中有几个非常重要的方法:
execute()
submit()
shutdown()
shutdownNow()
  • execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
  • submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用execute()方法,只不过它利用了Future来获取任务执行结果。
  • shutdown()和shutdownNow()是用来关闭线程池的。

看一下ThreadPoolExecutor类中一些比较重要成员变量

*corePoolSize* - 池中所保存的线程数,包括空闲线程。

*maximumPoolSize*-池中允许的最大线程数。

*keepAliveTime* - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

*unit* - keepAliveTime 参数的时间单位。

*workQueue* - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。

*threadFactory* - 执行程序创建新线程时使用的工厂。

*handler* - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

ThreadPoolExecutor类是线程池的一个非常具体的实现,最核心的线程池类,然后他跟Executors.newFixedThreadPool(5);这些有具体功能的线程池的关系就是如下源码所示:具体实现来看,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1,1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());

8.2.3 最基本的创建线程池的示例:

package com.ethan.ThreadPool;


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


class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        MyRunnable m1 = new MyRunnable();
        MyRunnable m2 = new MyRunnable();
        MyRunnable m3 = new MyRunnable();
        MyRunnable m4 = new MyRunnable();
        MyRunnable m5 = new MyRunnable();
        MyRunnable m6 = new MyRunnable();
        MyRunnable m7 = new MyRunnable();
        MyRunnable m8 = new MyRunnable();
        MyRunnable m9 = new MyRunnable();
        executor.execute(m1);
        executor.execute(m2);
        executor.execute(m3);
        executor.execute(m4);
        executor.execute(m5);
        executor.execute(m6);
        executor.execute(m7);
        executor.execute(m8);
        executor.execute(m9);
       
    }
}
package com.baizhi.threadpools;

import java.util.concurrent.*;

public class TestThreadPool {

    public static void main(String[] args) {

        //创建线程池对象
        //参数1: 线程池中核心线程数量
        //参数2: 线程池中最大线程数
        //参数3: 线程池中线程的空闲时间
        //参数4: 决定线程中线程的空闲时间的单位 TimeUnit
        //参数5: 线程等待队列
        //参数6: 创建线程工厂
        //参数7: 用于被拒绝任务的处理程序
        //     策略一: 直接拒绝 new ThreadPoolExecutor.AbortPolicy()     RejectedExecutionHandler  线程拒绝处理器
        //     策略二: 在调用execute方法的程序中执行该任务 new ThreadPoolExecutor.CallerRunsPolicy()  如果程序已经结束则丢弃任务
        //     策略三: new ThreadPoolExecutor.DiscardOldestPolicy()  丢弃等待队列中的请求,然后重试 execute ,除非执行程序被关闭,在这种情况下,任务被丢弃。
        //     策略四: new ThreadPoolExecutor.DiscardPolicy()    丢弃最后一次无法执行的任务  静默丢弃
        ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(1);
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                                        1,  //核心线程池数量         
                                    2,  //线程池最大线程数量       
                                       10, //空闲10
                                    TimeUnit.SECONDS,   //单位  秒
                                    workQueue,          //线程等待队列         
                                    threadFactory,      //创建新线程工厂
                                    new ThreadPoolExecutor.DiscardPolicy()); //拒绝处理器

        //调用线程池对象中execute方法执行线程任务
       threadPoolExecutor.execute(new Runnable() {
           public void run() {
               System.out.println("任务1    "+Thread.currentThread().getName());
           }
       });
        threadPoolExecutor.execute(new Runnable() {
            public void run() {
                System.out.println("任务2    "+Thread.currentThread().getName());
            }
        });

        threadPoolExecutor.execute(new Runnable() {
            public void run() {
                System.out.println("任务3    "+Thread.currentThread().getName());
            }
        });

        threadPoolExecutor.execute(new Runnable() {
            public void run() {
                System.out.println("任务4    "+Thread.currentThread().getName());
            }
        });

    }

}

class MyRunnable implements  Runnable{
    public void run() {
        System.out.println("任务.......线程名称:"+Thread.currentThread().getName());
        /*try {
            //Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
    }
}
posted @ 2021-07-26 22:02  ethanSung  阅读(119)  评论(0编辑  收藏  举报