Java 多线程基础(黑马视频笔记)

线程实现方式

线程的基本概念

我们先学习两组概念。首先是并发与并行

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

其次,进程和线程的区别是:

  • 进程:一个内存中运行的一个应用程序就是一个进程。每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程。进程也是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序,就是一个进程从创建,到运行,消亡的过程

    所有的应用程序都需要进到内存中执行(临时存储RAM)。进入到内存的程序叫进程。结束进程就是将进程从内存中清除了

  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程中可以有多个线程,这样的应用程序称为多线程程序,多线程之间互不影响

线程一般有两种调度方式:

  1. 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间
  2. 抢占式调度:优先让优先级高的线程使用CPU,若线程优先级相同,则随机选择一个

Java中多线程采用抢占式调度。

多线程的创建

创建新执行线程有两种方法:

  1. 将类声明为 Thread 的子类,该子类重写 Thread 的 run() 方法,分配并启动该子类的实例
  2. 声明实现 Runnable 接口的类,该类实现 run() 方法,然后可以分配该类的实例,在创建线程时作为一个参数传递并启动
Thread 类

Java 使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。

Java 中通过继承 Thread 类来创建并启动多线程的步骤如下:

  1. 创建一个 Thread 类的子类
  2. 在 Thread 类的子类中重写 Thread 的 run() 方法,设置线程的任务(开启线程要做什么)
  3. 创建 Thread 类的子类对象
  4. 调用 Thread 类中的方法 start() 方法,开启新的线程,执行 run() 方法
    其中,void start():使该线程开始执行,Java 虚拟机调用该线程的 run() 方法。结果是两个线程并发运行,当前线程(主线程)和另一个线程(创建的新线程,执行其run方法)
    注意:多次启动一个线程是非法的。特别是当线程已经执行完毕后,不能再重新启动。
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("run:" + i);
        }
    }
}

/*创建多线程的第一种方式:创建Thread类的子类*/
public class Main {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("main:" + i);
        }
    }
}

输出结果是随机抢占的:

main:0
main:1
main:2
run:0
main:3
run:1
main:4
run:2
main:5
main:6
run:3
main:7
run:4
......

这里再理解一下多线程的原理:
当JVM执行main方法时,会开辟一条main方法通向CPU的路径,这个路径叫做main线程,主线程。CPU通过这个线程执行main方法
当我们new了一个Thread类的子类对象并调用run方法后,JVM会再开辟一条通向CPU的新线程(路径),用来执行run方法
对于CPU而言,就有了两条线程,两个线程会一起抢夺CPU的执行权,CPU会随意选择一条路径执行,我们无法控制CPU

上述代码中,多线程的内存图解如下所示:
由图我们可以看出,当创建了一个Thread类的子类对象并调用run方法后,会在内存中开辟一个新的栈空间,用来执行该run方法。
多个线程在不同的栈空间中执行,因此多个线程之间互不影响。
在这里插入图片描述
Thread 类的常用方法

  1. 获取当前线程的名称
  • 使用 Thread 类中的 getName() 方法
    public String getName():获取当前线程的名称
  • 先获取到当前正在执行的线程,使用线程中的方法 getName()获取线程名称
    static Thread currentThraed():返回对当前正在执行的线程对象的引用
public class MyThread extends Thread{
    @Override
    public void run() {
        //方法一:使用Thread类中的getName()方法
        String name = getName();
        System.out.println(name);

        //方法二:使用静态方法currentThread()获取到当前正在执行的线程,使用线程中的方法getName()获取线程名称
        Thread th = Thread.currentThread();
        System.out.println(th);
    }
}
public class ThreadGetName {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start(); //Output:Thread-0
        new MyThread().start(); //Output:Thread-1
    }
}

输出结果:

Thread-0
Thread-1
Thread[Thread-0,5,main]
Thread[Thread-1,5,main]
  1. 设置线程名称
  • 使用 Thread 中的方法 setName()
    void setName(String name):改变线程名称,使之与参数 name 相同
  • 创建一个带参数的构造方法,参数传递线程的名称,调用父类的带参构造方法,将线程名称传递给父类,让父类 Thread 给自线程设置名称
    Thread(String name):分配新的 Thread 对象
public class MyThread extends Thread{
    public MyThread() {

    }
    //方法二:创建带参构造方法,参数传递线程的名称,调用父类带参构造方法,将线程名称传递给父类
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        //获取线程名称
        System.out.println(Thread.currentThread().getName());
    }
}

public class ThreadSetName {
    public static void main(String[] args) {
        //方法一:使用Thread中的方法setName()
        MyThread mt = new MyThread();
        mt.setName("小明");
        mt.start();

        new MyThread("旺财").start();
    }
}
  1. sleep() 方法
    public static void sleep(long millis):使当前正在执行的程序以指定的毫秒数暂停(暂时停止执行)。毫秒数结束后线程继续执行
public class Main {
    public static void main(String[] args) {
        //模拟秒表
        for (int i=1; i<=60; i++) {
            System.out.println(i);
            //使用Thread类的sleep()方法让程序睡眠一秒
            try {
                Thread.sleep(1000); //Thread.sleep()方法本身存在异常,所以需要进行异常处理
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//程序的运行结果为每隔一秒输出一个i
Runnable 接口

创建多线程程序的第二种方法:实现 Runnable 接口

java.lang.Runnable:
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个 run 的无参构造方法

java.lang.Thread 类中的构造方法:
Thread(Runnable target):分配新的 Thread 对象
Thread(Runnable target, String name):分配新的 Thread 对象

创建多线程步骤:

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

public class Main {
    public static void main(String[] args) {
        //3.创建一个Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        //4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t = new Thread(run);
        //5.调用Thread类中的start()方法,开启新的线程,执行run()方法
        t.start();

        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}
/*输出结果为:
main--->0
main--->1
Thread-0--->0
Thread-0--->1
Thread-0--->2
main--->2
Thread-0--->3
......
*/

以上两种创建多线程的方式,有什么区别:
采用 Runnable 接口创建多线程的好处:

  1. 避免了单继承的局限性。一个类只能继承一个父类,若类继承了 Thread 类,就不能继承其他类。但实现 Rannable 接口,依然可以继承其他类,实现其他接口。
  2. 增强了程序的扩展性,降低了程序的耦合性(解耦)。实现 Runnable 接口的方式,将设置线程任务和
    开启新线程进行了分离(解耦)。创建不同的实例,实现不同的线程任务。
匿名内部类方式

匿名内部类的作用:简化代码
格式:

new 父类/接口 () {
	//重写父类/接口方法
}
public class InnerClassThread {
    public static void main(String[] args) {
        //Thread方法
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        }.start();
        
        //Runnable方法
        //Runnable r = new RunnableImpl();    //多态
        Runnable r = new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        };
        new Thread(r).start();
        
        //简化接口的方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        }).start();
    }
}

线程同步机制(线程安全问题)

当多线程访问同一资源时,且多个线程对资源有写的操作,则容易产生线程安全问题。

例:举例模拟电影票三个窗口,同时售卖1-100号的电影票

/*如电影院中,多个窗口同时卖1-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--;
            }
        }
    }
}

public class Main {
    //模拟卖票,三个线程同时开启,对共享票进行出售
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

在上述案例中,三个线程同时对共享数据 ticket 进行读写操作,最终的输出结果显示,出现了重复票源及非法票源,即出现了线程安全问题:

//上述案例输出结果:
Thread-0正在卖第100张票
Thread-1正在卖第100张票
Thread-2正在卖第100张票
Thread-2正在卖第97张票
Thread-1正在卖第97张票
Thread-0正在卖第96张票
...
Thread-0正在卖第1张票
Thread-2正在卖第0张票
Thread-1正在卖第-1张票

Java中提供了同步机制:synchronized
当某个线程修改共享资源时,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作。保证了数据的同步性,解决线程不安全的问题。

有三种方法完成同步操作:

  • 同步代码块
  • 同步方法
  • 锁机制

同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区域的资源进行互斥访问。

格式:
synchronized (同步锁对象) {
//需要同步操作的代码
}

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

  1. 锁对象可以是任意类型
  2. 要保证多个线程使用的锁对象相同

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

如上述卖票案例,修改 run() 方法如下,即可解决线程安全问题:

/*第一种方法:采用同步代码块*/
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--;
                }
            }
        }
    }
}
public class Main {
    //模拟卖票,三个线程同时开启,对共享票进行出售
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

原理:
线程t0获取锁对象,进入同步代码块执行
线程t1发现没有锁对象,就会进入阻塞状态,等待t0线程归还锁对象
同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁,无法进入同步代码块

上述方法的不足之处是:程序需要频繁的判断锁,获取锁,释放锁,程序的效率会降低

同步方法

使用 synchronized 修饰的方法为同步方法。保证某线程执行该方法时,其他线程只能在方法外等候。

格式:
public synchronized void method() {
//可能会产生线程安全问题的代码
}

如上述卖票案例,定义一个同步方法,即可解决线程安全问题:

/*第二种方法:采用同步方法*/
public class RunnableImpl implements Runnable{
    private int ticket = 100;   //多个线程共享票源
    @Override
    public void run() {
        System.out.println("this:" + this); //this:com.example.demo.Demo03Thread.Demo05.RunnableImpl@33c7353a
        while(true) {
            playTicket();
        }
    }

    //定义一个同步方法
    public synchronized void playTicket(){
        if(ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}
public class Main {
    //模拟卖票,三个线程同时开启,对共享票进行出售
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:" + run);   //run:com.example.demo.Demo03Thread.Demo05.RunnableImpl@33c7353a
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

通过上述代码中的输出测试可以得到验证:
同步方法的锁对象,其实就是实现类对象(即上述的new RunnableImpl()),也就是this。

静态同步方法

静态同步方法的锁对象不能是 this,this 是创建对象后产生的,但静态方法优先于对象。
静态同步方法的锁对象是本类的 class 属性,也是 class 文件对象(反射)

public class RunnableImpl implements Runnable{
    private static int ticket = 100;
    @Override
    public void run() {
        while(true) {
            playTicket();
        }
    }

    public static /*synchronized*/ void playTicket(){
        /*这种写法会产生错误。因为 this 是在静态方法之后产生的*/
        //synchronized (this) {
        synchronized (RunnableImpl.class){
            if(ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

Lock 锁

java.util.concurrent.locks.Lock 接口,提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作。

Lock 锁也称为同步锁,Lock 中的方法:
void lock():获取锁
void unlock():释放锁
java.util.concurrent.locks.ReentrantLock implements Lock 接口

Lock 锁的使用步骤:

  1. 在成员位置创建一个 ReentrantLock 对象
  2. 在可能会出现安全问题的代码前调用 Lock 接口的 lock() 方法获取锁
  3. 在可能会出现安全问题的代码后调用 Lock 接口的 unlock() 方法释放锁

如下代码示例,建议将 unlock() 放入 finally 中,这样无论程序是否发生异常,都会释放锁对象,提高程序效率。

/*第三种方法:采用Lock锁*/
public class RunnableImpl implements Runnable{
    private static int ticket = 100;
    //1. 在成员位置创建一个 ReentrantLock 对象
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        //2. 在可能会出现安全问题的代码前调用 Lock 接口的 lock() 方法获取锁
        l.lock();
        while(true) {
            if(ticket > 0) {
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //3. 在可能会出现安全问题的代码后调用 Lock 接口的 unlock() 方法释放锁
                    l.unlock();
                }
            }
        }
    }
}
public class Main {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

线程状态

java.lang.Thread.State 中给出了六种线程状态

线程的状态图如下:
在这里插入图片描述
其中,阻塞状态指:具有CPU的执行资格,等待CPU空闲时执行
休眠状态指:放弃CPU的执行权,CPU空闲也不执行

Timed Waiting(计时等待)

在之前的案例中,我们在 run() 方法中通过 sleep(),强制当前正在运行的线程休眠(暂停执行),以“减慢进程”。
当调用 sleep() 方法时,当前执行的线程就会进入休眠状态,也就是所谓的 Timed Waiting(计时等待)。

注意:

  • 进入Timed Waiting 状态的一种常见情况是调用 sleep() 方法,单独的线程也可以调用

  • 为了让其他线程有机会执行,可以将 Thread.sleep() 的调用放 run() 之内。这样才能保证该线程在执行过程中睡眠

  • sleep 与锁无关,线程睡眠到期自然苏醒,并返回到 Runnable(可运行)状态

    在这里插入图片描述

BLOCKED(锁阻塞)

如线程A和线程B使用同一锁,如果线程A获取到锁,线程A进入到 Runnable 状态,那么线程B就会进入到Blocked锁阻塞状态
这是由 Runnable 状态进入Blocked状态,除此之外,Waiting 和 Time Waiting 状态也会在某种情况下进入阻塞状态。

在这里插入图片描述

Waiting (无限等待)

Object 中的方法:
void wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void notify():唤醒在此对象监视器上等待的单个线程。会继续执行 wait() 方法之后的代码

/*等待唤醒案例:线程之间的通信*/
public class WaitAndNotify {
    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(); //调用wait()方法,放弃cpu的执行,进入到waiting状态(无限等待)
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //唤醒之后执行的代码
                        System.out.println("包子已经做好了,开吃");
                        System.out.println("====================");
                    }
                }
            }
        }.start();
        //创建老板线程
        new Thread(){
            @Override
            public void run() {
                while(true) {
                    //花了5s做包子
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj) {
                        System.out.println("老板5s之后做好的包子,并告知顾客");
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}
//告诉老板需要的包子数量和种类
//老板5s之后做好的包子,并告知顾客
//包子已经做好了,开吃
//====================
//告诉老板需要的包子数量和种类
//老板5s之后做好的包子,并告知顾客
//包子已经做好了,开吃
//====================
//......

wait 有一个带参数的方法 wait(long m),线程进入到 TimeWaiting(计时等待)有两种方法:

  1. 使用 sleep(long m) 方法,在毫秒值结束之后,线程睡醒进入到 Runnable/Blocked 状态
  2. 使用 wait(long m) 方法,wait 方法如果在毫秒值结束之后还没有被 notify 唤醒,就会自动醒来,线程睡醒进入到 Runnable/Blocked 状态

唤醒除了 notify() 方法之外,还有方法 notifyAll() 方法,若有多个等待线程,notify() 只能随机唤醒其中的一个线程,notifyAll() 可以唤醒所有等待中的线程

等待唤醒机制

多个线程并发执行时,在默认情况下CPU是随机切换线程的,若需要多个线程共同完成某一任务,并且有规律执行,则需要多线程之间协调通信,以达到多线程共同操作同一份共享数据,避免对同一共享数据的争夺 。
我们需要一定手段使各个线程有效利用资源——等待唤醒机制。

等待唤醒机制常用到的三种方法,即上面学到的三种:

  • wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费CPU资源,也不会竞争锁,这时线程的状态为 WAITING,需要 notify 在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列中
  • notify
  • notifyAll

即使只通知了一个等待的线程,被通知的线程也不能立刻恢复执行,因为此刻它已经不持有锁,需要再次尝试获取锁,成功后才能在当初调用 wait 方法之后的地方恢复执行

注意:

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

下面是一个店家卖包子,顾客吃包子的实例,实现了等待唤醒机制:

//包子类
public class BaoZi {
    String pi;  //包子皮属性
    String xian;    //包子馅属性
    boolean flag = false;   //包子的状态,有包子true和没有包子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 {
                        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 + "包子");
                try {
                    Thread.sleep(3000); //生产包子需要三秒钟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                bz.flag = true; //唤醒顾客线程,让顾客吃包子
                bz.notify();
                System.out.println("包子铺已经生产好了" + bz.pi + bz.xian + "包子,顾客可以开吃了");
            }
        }
    }
}

//顾客类
public class GuKe extends Thread{
    private BaoZi bz;   //创建包子对象,作为锁对象

    public GuKe(BaoZi bz) {
        this.bz = bz;
    }
    //设置线程任务,制作包子
    @Override
    public void run() {
        while(true) {
            synchronized (bz) {
                if(bz.flag == false) {  //没有包子,则等待生产包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //被唤醒之后执行的代码,吃包子
                System.out.println("顾客正在吃:" + bz.pi + bz.xian + "的包子");
                bz.flag = false;
                bz.notify();    //唤醒包子铺线程,继续生产包子
                System.out.println("顾客已经把" + bz.pi + bz.xian + "的包子吃完了,包子铺开始生产包子");
                System.out.println("====================");
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BaoZi bz = new BaoZi(); //创建包子对象
        new BaoZiPu(bz).start();    //创建包子铺线程
        new GuKe(bz).start();   //创建顾客线程
    }
}

线程池

若并发的线程数量很多,并且每个线程都是执行一段很短的时间就结束了,这样频繁创建线程会大大降低系统的效率,因为频繁创建线程和销毁线程都需要消耗时间。

Java中可以通过线程池实现线程的复用,其中的线程可以反复使用
若线程池中已无空闲线程,则任务队列中的剩余任务等待执行,等待其他某个任务执行完毕后,归还线程到线程池,再从线程池中获取线程,执行任务

线程池的底层原理其实就是一个容器,一个集合(比如ArrayList,HashSet,LinkedList,HashMap),应该优先选用LinkedList< Thread >
当程序第一次启动时,创建多个线程并保存到一个集合中
当要使用线程时,从集合中取出线程使用 Thread t = linked.removeFirst();
当使用完毕线程后,将线程归还给线程池 linked.addLast(t);
在JDK1.5之后,jdk内置了线程池,可直接使用

java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executors 类中的静态方法:

  • static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池
    参数:int nThreads,创建线程池中包含的线程数量
    返回值:ExecutorService 接口,返回的是 ExecutorService 接口的实现类对象,可以使用 ExecutorService 接口接收(面向接口编程)

java.util.concurrent.ExecutorService:线程池接口,用来从线程池中获取线程,调用 start() 方法执行线程任务
其中包含方法:
submit(Runnable task):提交一个 Runnable 任务用于执行
void shutdown():关闭/销毁线程池

线程池的使用步骤:

  1. 使用线程池的工厂类 Executors 中提供的静态方法 newFixedThreadPool() 生产一个指定线程数量的线程池
  2. 创建一个类,实现 Runnable 接口,重写 run 方法,设置线程任务
  3. 调用 ExecutorService 中的方法 submit(),传递线程任务(实现类),开启线程,执行 run 方法
  4. 可以调用 ExecutorService 中的方法 shutdown(),销毁线程池。(不建议执行)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
    public static void main(String[] args) {
        //1.使用线程池的工厂类Executors中提供的静态方法newFixedThreadPool()生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExecutorService中的方法submit(),传递线程任务(实现类),开启线程,执行run方法
        //线程池会一直开启,使用完线程后,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunnableImpl());  //pool-1-thread-1创建了一个新线程执行
        es.submit(new RunnableImpl());  //pool-1-thread-2创建了一个新线程执行
        es.submit(new RunnableImpl());  //pool-1-thread-1创建了一个新线程执行

        //(不建议执行)4.可以调用 ExecutorService 中的方法 shutdown(),销毁线程池
        es.shutdown();
        es.submit(new RunnableImpl());  //会抛出java.util.concurrent.RejectedExecutionException异常,线程池被销毁,不能获取线程
    }
}

//2. 创建一个类,实现 Runnable 接口,重写 run 方法,设置线程任务
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+ "创建了一个新线程执行");
    }
} 

Lambda 表达式

函数式编程思想概述
数学中,函数即有输入量,有输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法,强调做什么,而不是以什么形式做

冗余的 Runnable 代码
比如我们在采用 Runnable 的匿名内部类实现多线程时,

//创建Runnable接口实现类
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "新线程创建");
    }
}
public class Main {
    public static void main(String[] args) {
        Runnable r = new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "新线程创建");
            }
        };
        new Thread(r);

        new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "新线程创建");
            }
        }).start();
    }
}

我们通过以上方法可以发现:

  • Thread 类需要 Runnable 接口作为参数,其中的抽象方法 run 是用来指定线程任务内容的核心
  • 为了指定 run 方法,不得不需要 Runnable 接口的实现类
  • 为了省去定义 Runnable 实现类,不得不采用匿名内部类
  • 必须要覆盖重写 run 方法,所以方法名称/参数及返回值必须重写
  • 而实际上,只有方法体才是关键所在。我们的最终目的是将 run 方法传递给 Thread 类,创建对象只是受限于面向对象语而不得不采取的一种手段方式

在Java8(JDK1.8)中,加入了 Lambda 表达式

Lambda 标准格式
Lambda 格式: (参数列表) -> {一些重写方法的代码}

如Runnable 接口中 run 方法的定义:public static void run(),即制定了一种做事情的方案,无参数,无返回值,代码块(方法体)为方案的具体执行步骤。
同样的语义体现在 Lambda 语法中:
() -> System.out.println("新线程创建");

  • 小括号() 即 run 方法的参数(无),代表不需要任何条件
  • 中间箭头表示将前面的参数传递给后面的代码
  • 后面的输出语句为业务逻辑代码

如,通过 Lambda 表达式实现多线程:

/*使用Lambda实现多线程*/
public class LambdaMain {
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "新线程创建");
        }
        ).start();
    }
}

Lambda 的无参数无返回值

public interface Cook {
    public abstract void makeFood();
}
public class Main {
    public static void main(String[] args) {
        invokeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("吃饭了");
            }
        });

        //使用Lambda表达式
        invokeCook(()->{System.out.println("吃饭了");});
        //使用Lambda省略格式
        //invokeCook(()->System.out.println("吃饭了"));
    }

    public static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

Lambda 的有参数有返回值

import java.util.Arrays;
import java.util.Comparator;

/*使用数组存储多个Person对象,对数组中的对象通过Arrays.sort()对年龄进行排序*/
public class Main {
    public static void main(String[] args) {
        Person[] arr = {
                new Person("小王", 23),
                new Person("小李", 14),
                new Person("小张", 27),
        };
/*        Arrays.sort(arr, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        });*/

        //使用Lambda表达式
        Arrays.sort(arr, (Person o1, Person o2)->{
            return o1.getAge() - o2.getAge();
        });
        //使用Lambda省略格式
        //Arrays.sort(arr, (o1, o2)-> o1.getAge() - o2.getAge());
        for (Person person : arr) {
            System.out.println(person);
        }
    }
}
public class Person {
    private String name;
    private int age;

    public Person(){
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public class Main {
    public static void main(String[] args) {
        invokeCalc(10, 20, new Calculator() {
            @Override
            public int calc(int a, int b) {
                return a+b;
            }
        });

        //使用Lambda表达式
        invokeCalc(10, 20, (int a, int b) -> {
            return a+b;
        });
    }
    public static void invokeCalc(int a, int b, Calculator c) {
        int sum =c.calc(a, b);
        System.out.println(sum);
    }
}

public interface Calculator {
    public abstract int calc(int a, int b);
}

Lambda 省略格式
Lamda 在标准格式基础上,可以进一步省略,省略写法的规则为:

  1. 小括号 () 内的参数类型可省略
  2. 若小括号内有且仅有一个参数,则小括号可省略
  3. 若大括号 {} 内有且仅有一个语句,则无论是否有返回值,都可以省略大括号,return关键字及语句分号(注意:要省略则这三个一起省略,否则会报错)

如上述例子中,Lambda 表达式可以省略为:

invokeCalc(10, 20, (a, b) -> a+b);

Lambda 的使用前提

注意:

  1. 使用 Lambda 必须具有接口,且接口中有且只有一个抽象方法
  2. 使用 Lambda 必须具有上下文推断
    即,方法的参数或局部变量类型必须为 Lambda 对应的接口类型,才能使用 Lambda 作为该接口的实例

有且仅有一个抽象方法的接口,称为“函数式接口”

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java并发编程 背景介绍 并发历史 必要性 进程 资源分配的最小单位 线程 CPU调度的最小单位 线程的优势 (1)如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率 (2)建模简单:通过使用线程可以讲复杂并且异步的工作流进一步分解成一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置交互 (3)简化异步事件的处理:服务器应用程序在接受来自多个远程客户端的请求时,如果为每个连接都分配一个线程并且使用同步IO,就会降低开发难度 (4)用户界面具备更短的响应时间:现代GUI框架中大都使用一个事件分发线程(类似于中断响应函数)来替代主事件循环,当用户界面用有事件发生时,在事件线程中将调用对应的事件处理函数(类似于中断处理函数) 线程的风险 线程安全性:永远不发生糟糕的事情 活跃性问题:某件正确的事情迟早会发生 问题:希望正确的事情尽快发生 服务时间过长 响应不灵敏 吞吐率过低 资源消耗过高 可伸缩性较低 线程的应用场景 Timer 确保TimerTask访问的对象本身是线程安全的 Servlet和JSP Servlet本身要是线程安全的 正确协同一个Servlet访问多个Servlet共享的信息 远程方法调用(RMI) 正确协同多个对象中的共享状态 正确协同远程对象本身状态的访问 Swing和AWT 事件处理器与访问共享状态的其他代码都要采取线程安全的方式实现 框架通过在框架线程中调用应用程序代码将并发性引入应用程序,因此对线程安全的需求在整个应用程序中都需要考虑 基础知识 线程安全性 定义 当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的 无状态对象一定是线程安全的,大多数Servlet都是无状态的 原子性 一组不可分割的操作 竞态条件 基于一种可能失效的观察结果来做出判断或执行某个计算 复合操作:执行复合操作期间,要持有锁 锁的作用 加锁机制、用锁保护状态、实现共享访问 锁的不恰当使用可能会引起程序性能下降 对象的共享使用策略 线程封闭:线程封闭的对象只能由一个线程拥有并修改 Ad-hoc线程封闭 栈封闭 ThreadLocal类 只读共享:不变对象一定是线程安全的 尽量将域声明为final类型,除非它们必须是可变的 分类 不可变对象 事实不可变对象 线程安全共享 封装有助于管理复杂度 线程安全的对象在其内部实现同步,因此多个接口可以通过公有接口来进行访问 保护对象:被保护的对象只能通过特定的锁来访问 将对象封装到线程安全对象中 由特定锁保护 保护对象的方法 对象的组合 设计线程安全的类 实例封闭 线程安全的委托 委托是创建线程安全类的最有效策略,只需要让现有的线程安全类管理所有的状态 在现有线程安全类中添加功能 将同步策略文档化 基础构建模块 同步容器类 分类 Vector Hashtable 实现线程安全的方式 将状态封装起来,对每个公有方法都进行同步 存在的问题 复合操作 修正方式 客户端加锁 迭代器 并发容器 ConcurrentHashMap 用于替代同步且基于散列的Map CopyOnWriteArrayList 用于在遍历操作为主要操作的情况下替代同步的List Queue ConcurrentLinkedQueue *BlockingQueue 提供了可阻塞的put和take方法 生产者-消费者模式 中断的处理策略 传递InterruptedException 恢复中断,让更高层的代码处理 PriorityQueue(非并发) ConcurrentSkipListMap 替代同步的SortedMap ConcurrentSkipListSet 替代同步的SortedSet Java 5 Java 6 同步工具类 闭锁 *应用场景 (1)确保某个计算在其需要的所有资源都被初始化后才能继续执行 (2)确保某个服务在其所依赖的所有其他服务都已经启动之后才启动 (3)等待知道某个操作的所有参与者都就绪再继续执行 CountDownLatch:可以使一个或多个线程等待一组事件发生 FutureTask *应用场景 (1)用作异步任务使用,且可以使用get方法获取任务的结果 (2)用于表示一些时间较长的计算 状态 等待运行 正在运行 运行完成 使用Callable对象实例化FutureTask类 信号量(Semaphore) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量 管理者一组虚拟的许可。acquire获得许可(相当于P操作),release释放许可(相当于V操作) 应用场景 (1)二值信号量可用作互斥体(mutex) (2)实现资源池,例如数据库连接池 (3)使用信号量将任何一种容器变成有界阻塞容器 栅栏 能够阻塞一组线程直到某个事件发生 栅栏和闭锁的区别 所有线程必须同时到达栅栏位置,才能继续执行 闭锁用于等待事件,而栅栏用于等待线程 栅栏可以重用 形式 CyclicBarrier 可以让一定数量的参与线程反复地在栅栏位置汇集 应用场景在并行迭代算法中非常有用 Exchanger 这是一种两方栅栏,各方在栅栏位置上交换数据。 应用场景:当两方执行不对称的操作(读和取) 线程池 任务与执行策略之间的隐形耦合 线程饥饿死锁 运行时间较长的任务 设置线程池的大小 配置ThreadPoolExecutor 构造参数 corePoolSize 核心线程数大小,当线程数= corePoolSize的时候,会把runnable放入workQueue中 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了” keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。 workQueue 保存任务的阻塞队列 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务 threadFactory 创建线程的工厂 handler 拒绝策略 unit 是一个枚举,表示 keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值 线程的创建与销毁 管理队列任务 饱和策略 AbortPolicy DiscardPolicy DiscardOldestPolicy CallerRunsPolicy 线程工厂 在调用构造函数后再定制ThreadPoolExecutor 扩展 ThreadPoolExecutor afterExecute(Runnable r, Throwable t) beforeExecute(Thread t, Runnable r) terminated 递归算法的并行化 构建并发应用程序 任务执行 在线程中执行任务 清晰的任务边界以及明确的任务执行策略 任务边界 大多数服务器以独立的客户请求为界 在每个请求中还可以发现可并行的部分 任务执行策略 在什么(What)线程中执行任务? 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)? 有多少个(How Many)任务能并发执行? 在队列中有多少个(How Many)任务在等待执行? 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝? 在执行一个任务之前或之后,应该进行什么(What)动作? 使用Exector框架 线程池 newFixedThreadPool(固定长度的线程池) newCachedThreadPool(不限规模的线程池) newSingleThreadPool(单线线程池) newScheduledThreadPool(带延迟/定时的固定长度线程池) 具体如何使用可以查看JDK文档 找出可利用的并行性 某些应用程序中存在比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性 任务的取消和关闭 任务取消 停止基于线程的服务 处理非正常的线程终止 JVM关闭 线程池的定制化使用 任务和执行策略之间的隐性耦合 线程池的大小 配置ThreadPoolExecutor(自定义的线程池) 此处需要注意系统默认提供的线程池是如何配置的 扩展ThreadPoolExector GUI应用程序探讨 活跃度(Liveness)、性能、测试 避免活跃性危险 死锁 锁顺序死锁 资源死锁 动态的锁顺序死锁 开放调用 在协作对象之间发生的死锁 死锁的避免与诊断 支持定时的显示锁 通过线程转储信息来分析死锁 其他活跃性危险 饥饿 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。 糟糕的响应性 如果由其他线程完成的工作都是后台任务,那么应该降低它们的优先级,从而提高前台程序的响应性。 活锁 要解决这种活锁问题,需要在重试机制中引入随机性(randomness)。为了避免这种情况发生,需要让它们分别等待一段随机的时间 性能与可伸缩性 概念 运行速度(服务时间、延时) 处理能力(吞吐量、计算容量) 可伸缩性:当增加计算资源时,程序的处理能力变强 如何提升可伸缩性 Java并发程序中的串行,主要来自独占的资源锁 优化策略 缩
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值