累成一条狗

Java基础--多线程

一、程序、进程、线程、管程

1、区别

(1)程序是一段静态的代码,为应用程序执行的蓝本。
(2)进程为程序的一次动态执行过程,包括代码的加载、执行以及执行完毕的一个完整过程。
(3)线程是进程中的一个执行单元,一个进程在执行过程中可以产生多个线程(至少有一个线程 )。
(4)管程(monitor) 保证同一时刻只有一个线程活动,使线程安全。JVM 同步基于 进入、退出 管程对象实现的,每个 Java 对象都有一个管程对象,管程对象随着 Java 对象创建、销毁。

2、关系

  (1)进程负责的是应用程序的空间的标识,线程负责的是应用程序的执行顺序。
  (2)进程拥有一个包含了某些资源的内存区域,多个线程间共享进程的内存。
  (3)线程的中断与恢复相比于进程可以节省系统的开销。
  (4)进程是资源分配的基本单位,线程是调度和执行的基本单位。

3、为什么使用线程

  (1)使用多线程可以减少程序的响应时间。(把耗时的线程让一个单独线程去解决)
  (2)线程创建与切换的开销比进程小。
  (3)在能运行多线程的机器上运行单线程,会造成资源的浪费。
  (4)多线程能简化程序结构,使其便于理解。

 

二、多线程

1、多线程指的是一个进程中同时存在几个执行体(线程),按照不同的执行顺序共同工作的现象。

 

2、多线程并不是同时发生,系统在任何时刻只能执行一个线程,只是java虚拟机快速的将控制从一个线程切换到另一个线程(多个线程轮流执行),造成同时发生的错觉。

 

3、每个java程序都有一个缺省的主进程,当JVM启动时,发现main方法后,会启动一个主线程(main线程),用于执行main方法。若main方法中没有创建其他线程,那么当main执行完最后一个语句时,JVM会结束java应用程序。若main方法中创建了其他进程,那么JVM会等到所有线程结束后再结束java应用程序。

 

4、同步与异步:

  (1)同步就是指一个线程要等待上一个线程执行完之后才开始执行当前的线程。异步指的是一个线程的执行不需要在意其他线程的执行。

  (2)同步要解决的问题是当多个线程访问同一个资源时,多个线程间需要以某种顺序来确保该资源在某时刻只被一个线程使用,解决办法是获取线程对象的锁,得到锁,则这个线程进入临界区(访问互斥资源的代码块),且锁未释放前,其他线程不能进入此临界区。但同步机制会带来巨大的系统开销,甚至死锁,所以要尽量避免无谓的同步。

  (3)简单的讲,同步就是A喊B去吃饭,如果B听到,就和A一起去吃饭,若B没听到,则A就一直喊,直到B听到,然后在一起去吃饭。 而异步是A喊B去吃饭,然后A自己去吃饭,不管B接下来的动作,B可能与A一起吃饭,也可能过了几个小时再去吃饭。

  (4)说的直白点,同步就是执行有先后顺序,A执行完B再执行,异步就是各干各的,A执行一部分,B执行一部分,A与B间没有联系。

 

5、并行与并发:

  (1)并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时执行。
  (2)并发:通过cpu调度算法,让各线程快速切换。使程序看上去是同时执行的,

 

6、线程安全与不安全:

  (1)线程安全:指在并发条件下,代码经过多线程的调用,各线程的调度顺序 不影响代码执行结果。
  (2)线程不安全:即指线程的调度顺序影响代码执行结果。

 

7、Java的内存模型(JMM):

  (1)并发程序中,确保数据访问的一致性以及安全性非常重要。在了解并行机制的前提下,并定义一种规则,保证多个线程间可以有效地、正确地协同工作,从而产生了JMM。
  (2)Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。JMM的核心均围绕多线程的原子性、可见性、有序性来建立的。
  (3)Java内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

 

8、原子性、可见性、有序性:

  (1)原子性:指的是一个操作是不可中断的。可以理解为一个操作要么执行成功,要么不执行。
  (2)可见性:指的是一个线程修改了某一个共享变量的值,则其他线程能立即知道这个修改。
  (3)有序性:并发执行时,程序经过 指令重排后(提高cpu处理性能),程序的执行可能会乱序。通过指定 指令重排规则,使程序的逻辑有序,即不改变代码逻辑。

注:指令重排:执行代码的顺序与编写代码的顺序不一致,即虚拟机优化代码,改变代码顺序。

 

9、线程的run()与start()方法的区别:

  (1)系统通过调用start()方法来启动一个线程,此时该线程处于就绪状态,需要等JVM调度,然后调用run()方法,线程才进入运行状态。即调用start()方法是一个异步调用的过程。

  (2)若直接调用run()方法,则相当于调用一个普通方法,不能实现多线程。即调用run()方法是一个同步的过程。

举例:
System.out.println("1");
Thread thread = new Thread(new Runnable() {

    @Override
    public void run() {
       System.out.println("2");
   }
 });
thread.start(); //启动线程,等待JVM调度,不一定会立即执行。
System.out.println("3");
此时,异步,输出1, 3, 2

System.out.println("1");
Thread thread = new Thread(new Runnable() {

    @Override
    public void run() {
       System.out.println("2");
   }
 });
thread.run(); //不启动线程,立即执行。
System.out.println("3");
此时,同步,按顺序执行,输出1, 2, 3

 

三、线程的生命周期

1、java使用Thread类及其子类的对象来表示线程。

 

2、线程的生命周期通常为 新建状态,就绪状态,运行状态,阻塞状态,死亡状态。

 

3、新建状态:

  当Thread类及其子类的对象被声明并创建后,新线程处于新建状态,此时该线程有了相应的内存空间和其他资源。

 

4、就绪状态:

  调用Thread类的start()方法,用于启动线程,使线程进入线程队列排队等待JVM调度。

 

5、运行状态:

  线程被创建后,就具备了运行条件。但该线程仅仅占有内存空间,JVM管理的线程中还没有这个线程。需要调用start()方法(从父类继承的方法),通知JVM有新的线程等待切换。切换成功后,该线程会脱离创建他的主线程,并开始新的生命周期。如果此线程是Thread的子类创建的,由于Thread类中的run()方法没有具体内容,所以其子类必须重写run()方法(run()方法包含线程运行的代码)。

 

6、阻塞状态(三种):

  如果一个线程执行了sleep(睡眠)、suspend(挂起,不推荐使用)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。
  (1)等待阻塞状态:
    运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。等待阻塞状态不会主动进入线程队列排队等待,需要由其他线程调用notify()方法通知它,才能进入线程队列排队等待。

 

  (2)同步阻塞状态:
    线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

 

  (3)其他阻塞状态:
    通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

 

7、死亡状态(两种):

  死亡状态指线程释放了实体,即释放分配给线程对象的内存。
  (1)正常运行的线程完成run()方法。
  (2)线程被强制结束run()方法。

 

四、多线程的创建

1、 方法一:继承Thread(不常用),由子类复写run方法。

  使用子类创建优缺点:可以在子类中拓展新的成员变量与新方法,使线程具备某种特性与功能。但不能再扩展其他类,因为java不支持多继承。即单继承局限性。
步骤:
1、定义类去继承Thread类;
2、复写run方法,将线程运行的代码写在run方法中。
3、通过创建Thread类的子类对象(实现多态),创建线程对象。
4、调用线程的start方法,开启线程,并执行run方法。

【定义】
class MyThread extends Thread{
    @Override
    public void run() {
    }
}
【执行】
Thread myThread1 = new MyThread();
myThread1.start();

【举例】
class Demo extends Thread {

    @Override
    public void run() {
        System.out.println("1");
    }
}

public class Test {
    public static void main(String[] args) {
        Thread demo = new Demo();
        demo.start();
    }
}

 

2、方法二:实现一个接口Runnable(常用)

  实现Runnable接口避免单继承的局限性。
步骤:
1、定义类实现Runnable接口。
2、覆盖接口中的run方法,封装线程要运行的代码。
3、通过Thread类创建对象。
4、将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。
5、调用Thread对象的start方法,开启线程,执行接口中的run方法。

【定义】
class MyRunnable implements Runnable{
    @Override
    public void run() {
    }
}
【执行】
Runnable myRunnable = new MyRunnable();
Thread myThread1 = new Thread(myRunnable);
myThread1.start();

【举例】
public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("1");
            }
        });
        thread.start();
    }
}

 

3、方法三:通过Callable和Future创建线程

  前两种的缺点:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
自从Java 1.5开始,就提供了Callable和Future接口,通过它们可以在任务执行完毕之后得到任务执行结果。
  创建Callable接口的实现类,并实现call()方法。并使用FutureTask类(实现了Runnable,Future接口)来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。通过FutureTask类的get()方法可以获取处理结果。

【定义】
class MyCallable implements Callable<Integer>{    
    @Override
    public Integer call() throws Exception {
        return null;
    }
}
【执行】
Callable<Integer> myCallable = new MyCallable();
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
Thread myThread1 = new Thread(ft);
myThread1.start();

【举例】
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Demo implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        Integer integer = 1;
        return integer;
    }

}

public class Test {
    public static void main(String[] args) {
        Callable<Integer> callable = new Demo();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

 

五、多线程常用方法:

1、线程名:

  一个线程的默认名字为“Thread-“ + 数字,可以通过函数来修改。
  (1)public final synchronized void setName(String name); // 设定线程名
  (2)public final String getName(); //获取线程名

2、线程ID:

  public long getId(); // 获取线程的ID

 

3、线程优先级:

  处于就绪状态的线程会首先进入就绪队列等待CPU资源,而同一时刻存在就绪队列中的线程可能有多个。java虚拟机(JVM)中的线程调度器负责管理线程,调度器将线程的优先级分为1~10,并使用Thread类中的类常量表示,即Thread.MIN_PRIORITY~Thread.MAX_PRIORITY。若未明确设置线程优先级,则默认为5,即Thread.NORM_PRIORITY。线程优先级不能保证线程执行的顺序,其依赖于平台。
  (1)public final void setPriority(int newPriority); //设置线程优先级,若newPriority不在1~10之间,会抛出java.lang.IllegalArgumentException异常。
  (2)public final int getPriority(); //获取线程的优先级。

4、常用方法:

  (1)public synchronized void start(); // 此方法用于启动线程,使新建状态的线程进入就绪队列排序。只有新建状态的线程才可调用start()方法,且只能调用一次。再次调用会抛出java.lang.IllegalArgumentException异常。

  (2)public void run(); //需要重写,用于定义线程对象被调用后的操作,是系统自动调用的方法。

  (3)public static native void sleep(long millis) throws InterruptedException; //线程的调度是根据优先级执行的,且总是执行高优先级。若想在高优先级未死亡时使用低优先级,可以调用sleep()方法将高优先级线程中断,参数millis的单位为毫秒。若休眠被打断,会抛出java.lang.IllegalArgumentException异常。所以必须在try-catch语句中使用sleep()方法。

  (4) public final native boolean isAlive(); //线程处于新建状态时,线程调用isAlive方法返回false。当线程调用start()方法并占有CPU资源后,此时run开始执行,在run未结束前,isAlive方法返回true。线程死亡后,isAlive方法返回false。如果一个正在运行的线程还未死亡,不能再为该线程分配新实体,因为线程只能引用最后分配的实体,其最初的实体不会被垃圾回收器回收。因为垃圾回收期认定先前的实体为运行状态,若回收会引起错误,所以不回收。

  (5)public static native Thread currentThread(); //属于Thread中的类方法,可直接使用类名调用,用于返回当前正在使用CPU资源的线程。

  (6)public final void join() throws InterruptedException; //一个线程A的运行期间,可以使用join()方法来联合线程B,即在A线程中启动并运行(join)B线程,当B线程结束后,A线程再继续运行。

  (7)public static native void yield(); //从线程从运行状态立即进入就绪状态

5、中断方法

  (1)public void interrupt(); //中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身(即不会中断线程)。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。

  (2) public static boolean interrupted(); //测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。

  (3)public boolean isInterrupted(); //测试线程是否被中断 ,不清除中断状态。

若线程在阻塞状态(通过wait, join, sleep方法)时,调用了它的interrupt()方法,
那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。
例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;
调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,
所以该“中断标记”会立即被清除为“false”,
同时,会产生一个InterruptedException的异常。

【通用处理】
@Override
public void run() {
    try {
        /*
        isInterrupted()用于判断线程的中断标记是不是为true。
        当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,
        使线程的中断标记为true,即isInterrupted()会返回true。
        此时,就会退出while循环。
        */
        // 1. isInterrupted()保证,只要中断标记为true就终止线程。
        while (!isInterrupted()) {
            // 执行任务...
        }
    } catch (InterruptedException ie) { 
        // 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
    }
}

 

六、线程同步

  同步就是指一个线程要等待上一个线程执行完之后才开始执行当前的线程。
  线程同步指的是通过人为的调控,保证多线程访问共享资源的方式是线程安全的(可以通过synchronized关键字实现)。
  线程同步实质上是一种等待机制,多个线程需要同时访问某对象时,会进入该对象的等待队列中排队,等待前面线程调用结束后,再开始访问该对象。核心理念就是等待队列 + 锁机制。

1、synchronized关键字

  (1)Java中每个对象有且仅有一个同步锁。
  (2)当调用某对象的synchronized方法时,就获取了该对象的同步锁。
  (3)不同线程对同步锁的访问是互斥的。即某个时间点,对象的同步锁只能被一个线程获得。

2、synchronized基本规则

非静态同步方法,锁定的是 实例对象 本身。
    当某个实例对象访问 非静态同步方法后,该实例对象被上锁,调用该对象的其他非静态同步方法将会被阻塞。
    其他的实例对象,可以继续调用 非静态同步方法。因为别的实例对象 与 当前实例对象 调用 非静态方法 获取的不是同一个锁。
    
静态同步方法,锁定的是 类对象 本身。
    当某个实例对象访问 静态同步方法后,该类对象被上锁,调用该对象的其他静态同步方法将被阻塞。
    其他的实例对象,调用静态同步方法也会被阻塞。因为别的实例对象 与 当前实例对象 调用 静态同步方法 获取的是同一个锁。
    
静态同步方法、非静态同步方法 锁的对象不同,调用不会被阻塞。

调用 静态方法、非静态方法,没有锁限制,可以访问。


注:
    静态方法(public static xxx)
    非静态方法、普通方法(public xxx)
    静态同步方法(public static synchronized xxx)
    非静态同步方法(public synchronized xxx)

 

3、synchronized的用法:

  (1)指定加锁对象:即给指定对象加锁,进入同步代码前要先获取给定对象的锁。

class Demo{
    Object obj = new Object();
    synchronized(obj){ //此时对obj对象有个同步锁。
    }
}

  (2)直接作用于实例方法:相当于给当前实例加锁,进入同步代码前要先获得当前实例的锁。

即
Demo demo = new Demo();
demo.show();//此时对Demo类的demo实例有个同步锁。

class Demo{
    public synchronized void show(){
    }
}

  (3)直接作用于静态方法:相当于给当前类加锁,进入同步代码前要先获得当前类的锁。

即
Demo.show();  //此时对Demo类有个同步锁,即对所有demo实例均有锁。
class Demo{
    public static synchronized void show(){
    }
}

 

4、Object方法:

  (1)public final void wait() throws InterruptedException; //让当前线程进入等待状态,直到其他线程调用此对象的notify() 或 notifyAll() 方法,当前线程被唤醒(进入“就绪状态”) 。同时,wait()也会让当前线程释放它所持有的锁。

  (2)public final native void wait(long timeout) throws InterruptedException; //在wait()方法基础上,当等待超过指定时间量也会进入就绪状态。(如果时间为0,等价于wait(),则无限等待!)

  (3)public final void wait(long timeout, int nanos) throws InterruptedException; //在wait(long)方法基础上,提供纳秒级别的时间精度。

  (4)public final native void notify(); //随机唤醒在此对象监视器上等待的单个线程。

  (5)public final native void notifyAll(); //唤醒在此对象监视器上等待的所有线程。

 

5、实现线程同步:

  (1)通过synchronized关键字,可以修饰方法以及代码块。
  (2)通过wait()方法以及notify(),notifyAll()方法。wait需写在try-catch中。
  (3)jdk1.5后推出,通过Lock接口以及其实现类ReentrantLock(重入锁)。

 

七、常见问题

1、sleep()与wait()的区别:

  (1)sleep是Thread类的静态方法,自动唤醒。而wait是Object类的方法,需通过notify方法唤醒。
  (2)sleep是让线程暂停一段时间,时间一到,自动唤醒,不涉及线程间通信,故不释放所占用的锁。而wait方法会释放自己的锁,且其synchronized数据能被其他线程调用,涉及线程间通信。
  (3)由于sleep不会释放锁,易导致死锁问题,所以一般推荐使用wait方法。

死锁:指的是两个或两个线程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,则无法向前推进。

 

2、sleep()与yield()的区别:

  (1)调用sleep方法后,其他线程的优先级不考虑,此时低优先级线程有运行机会。调用yield方法后,只有相同或更高的优先级线程才能有机会运行。
  (2)执行sleep方法后,线程会进入阻塞状态,必须等待一段时间后才会进入执行状态。执行yield方法后,线程会回到可执行状态,所以可能立刻执行。
  (3)sleep方法可能抛出InterruptedException异常,而yield没有异常。

 

3、守护线程:

  (1)java提供两种线程,一个是守护线程(daemon),一个是用户线程。
  (2)守护线程又称为服务线程,精灵线程,后台线程,指在程序运行时在后台提供一种通用服务的线程。守护线程不是程序不可或缺的部分。
  (3)若用户线程全部结束,即程序中只剩守护线程,那么JVM也就退出了,并杀死所有守护线程,即程序运行结束。
  (4)可以自己设置守护进程,需在start()方法前调用setDaemon(true)方法,若参数为false,则表示用户进程。
  (5)GC就是运行在一个守护线程上。

 

4、volatile关键字:

  (1)为了提高程序处理效率,数据不会直接与硬件进行交互,设置一个缓存区,将主存的数据拷贝到缓存中,在此处理数据,处理完成后,再写入主存中。
  (2)针对多线程使用的变量,如果未使用final或者volatile关键字修饰,很可能产生不可预知的结果。比如一个静态变量int i = 0,线程A修改 i = 1,在缓存中修改,但未立即写入主存中(理解为修改未生效), 此时线程B访问的仍是 i = 0,则导致出现错误。
  (3)使用volatile关键字修饰的变量,每次修改后,直接写入主存,其缓存中的内容无效,每次从主存中读取数据。
  (4)volatile关键字能保证可见性、有序性(禁止指令重排),不能保证原子性。

5、CAS

(1)CAS是compare and swap的缩写,即我们所说的比较交换。可以保证数据的原子性(是硬件对于并发操作的一个支持)。
(2)CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B,即 V == A ? V = B : V = V。
(3)可能会出现ABA问题,如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。为了解决ABA问题,可以在对象中额外再增加一个标记来标识对象是否有过变更,比如AtomicStampedReference/AtomicMarkableReference类。

 

 

八、同步实例

【模拟生产者消费者问题:】
问题:
1、有一个果篮,生产者可以向果篮中放置苹果,消费者每次从果篮中拿一个苹果,并花一段时间吃苹果。
2、当果篮中苹果数为0时,消费者停止拿苹果,并通知生产者放入一定数量的苹果。
3、当果篮中苹果数大于0时,通知消费者拿苹果。
4、生产者每次通过控制台输入苹果数。

【果篮类,FruitBasket.java】
/**
 * 果篮类,用于存放苹果。
 *
 */
public class FruitBasket {
    private int appleNumber;// 果篮中苹果数

    /**
     * 构造方法,根据指定苹果数初始化果篮。
     * 
     * @param appleNumber
     *            苹果数
     */
    public FruitBasket(int appleNumber) {
        this.appleNumber = appleNumber;
    }

    /**
     * 同步方法,由于修饰的是非静态方法,所以锁对象为果篮类的实例。 获得果篮中的苹果数
     * 
     * @return 苹果数
     */
    public synchronized int getAppleNumber() {
        return appleNumber;
    }

    /**
     * 同步方法,由于修饰的是非静态方法,所以锁对象为果篮类的实例。 设置果篮中的苹果数
     * 
     * @param appleNumber
     *            苹果数
     */
    public synchronized void setAppleNumber(int appleNumber) {
        this.appleNumber = appleNumber;
    }

    /**
     * 同步方法,由于修饰的是非静态方法,所以锁对象为果篮类的实例。 消费者每次从果篮中拿一个苹果,果篮中苹果数每次减1。
     * 
     */
    public synchronized void decreaseNumber() {
        appleNumber--;
    }

    /**
     * 同步方法,由于修饰的是非静态方法,所以锁对象为果篮类的实例。生产者每次放置一定的苹果数,果篮中苹果数每次增加一定数量。
     * 
     * @param number
     *            苹果数
     */
    public synchronized void increaseNumber(int number) {
        appleNumber += number;
    }
}


【生产者类,Producer.java】
import java.util.Scanner;

/**
 * 生产者,生产苹果,并放置到果篮中。 通过控制台输入放置的苹果数。
 *
 */
public class Producer implements Runnable {

    private FruitBasket fruitBasket;// 定义一个果篮,用于存放苹果数
    private Scanner scanner;// 用于获取控制台输入的数据

    /**
     * 构造方法,用于初始化一个生产者。
     * 
     * @param fruitBasket
     *            果篮类
     * @param scanner
     *            输入类
     */
    public Producer(FruitBasket fruitBasket, Scanner scanner) {
        this.fruitBasket = fruitBasket;
        this.scanner = scanner;
    }

    /**
     * 重写run方法,用于实现生产者放苹果的逻辑。
     */
    @Override
    public void run() {
        while (true) {// 循环执行
            // 以果篮实例为同步锁,每次只允许一个生产者或者消费者线程进行操作。
            synchronized (fruitBasket) {
                System.out.println("\n========================================================");
                System.out.println("当前果篮中苹果数为: " + fruitBasket.getAppleNumber());
                try {
                    System.out.println("生产者向果篮中放置苹果: ");
                    // 获取输入的苹果数
                    int number = Integer.parseInt(scanner.next());
                    // 向果篮中添加苹果
                    fruitBasket.increaseNumber(number);
                    System.out.println("放置苹果完成,当前果篮中苹果数为:  " + fruitBasket.getAppleNumber());

                    // 如果果篮中苹果数大于0,则通知消费者来拿苹果
                    if (fruitBasket.getAppleNumber() > 0) {
                        System.out.println("通知消费者来拿苹果。");
                        fruitBasket.notifyAll();// 通知所有的消费者
                        fruitBasket.wait();// 生产者等待
                    }
                } catch (NumberFormatException e) {
                    System.out.println("输入的数据格式错误,请重新输入。");
                } catch (InterruptedException e) {
                    System.out.println("系统异常");
                }
                System.out.println();
            }
        }
    }

}


【消费者类,Consumer.java】
/**
 * 消费者,每次从果篮中拿一个苹果。每拿一个苹果,需要吃一段时间。
 *
 */
public class Consumer implements Runnable {

    private FruitBasket fruitBasket;// 定义一个果篮,用于存放苹果数
    private long seconds;// 定义吃苹果时间
    private String name;// 消费者名

    /**
     * 根据果篮,休眠时间,名字来初始化一个消费者。
     * 
     * @param fruitBasket
     *            果篮
     * @param seconds
     *            吃苹果时间
     * @param name
     *            消费者名
     */
    public Consumer(FruitBasket fruitBasket, long seconds, String name) {
        this.fruitBasket = fruitBasket;
        this.seconds = seconds;
        this.name = name;
    }

    /**
     * 重写方法,用于实现消费者拿苹果的逻辑。
     */
    @Override
    public void run() {
        while (true) {// 循环执行
            // 以果篮实例为同步锁,每次只允许一个生产者或者消费者线程进行操作。
            synchronized (fruitBasket) {
                // 如果果篮中苹果数为0,
                if (fruitBasket.getAppleNumber() == 0) {
                    try {
                        // 通知生产者放置苹果。
                        fruitBasket.notifyAll();
                        fruitBasket.wait();// 消费者进行等待
                    } catch (InterruptedException e) {
                        System.out.println("系统错误");
                    }
                } else {// 如果果篮中苹果数大于0
                    System.out.println("\n========================================================");
                    System.out.println("当前果篮中苹果数为:  " + fruitBasket.getAppleNumber());
                    System.out.println(name + " 开始拿苹果");
                    fruitBasket.decreaseNumber();// 果篮中苹果数减1
                    System.out.println(name + " 拿完苹果,当前果篮中苹果数为:  " + fruitBasket.getAppleNumber());
                    try {
                        System.out.println(name + " 开始吃苹果……");
                        long start = System.currentTimeMillis();
                        Thread.sleep(seconds * 1000);// 吃苹果的时间
                        long end = System.currentTimeMillis();
                        System.out.println(name + " 吃完苹果了:  " + (end - start));
                        fruitBasket.notifyAll();// 唤醒生产者和消费者
                        fruitBasket.wait();// 当前消费者等待
                    } catch (InterruptedException e) {
                        System.out.println("系统错误");
                    }
                }
                System.out.println();
            }
        }
    }

}


【测试类,ProducerAndConsumerDemo.java】
import java.util.Scanner;

/**
 * 测试功能。 使用3个线程表示消费者,1个线程表示生产者。 在果篮中没有苹果时,消费者停止拿苹果的操作,通知生产者放置苹果(通过控制台输入)。
 * 当果篮中有苹果时,通知消费者开始拿苹果,消费者每次拿完苹果后会花一定的时间吃苹果。
 *
 */
public class ProducerAndConsumerDemo {
    // 实例化一个果篮,用于存放苹果
    private static FruitBasket fruitBasket = new FruitBasket(0);
    // 实例化一个输入实例,用于获取控制台输入
    private static Scanner scanner = new Scanner(System.in);

    /**
     * 测试入口
     * 
     * @param args
     */
    public static void main(String[] args) {
        // 实例化一个生产者
        Thread producer = new Thread(new Producer(fruitBasket, scanner));
        producer.start();// 启动生产者

        // 实例化一个消费者A
        Thread consumerA = new Thread(new Consumer(fruitBasket, 3, "consumerA"));
        consumerA.start();// 启动消费者A

        // 实例化一个消费者B
        Thread consumerB = new Thread(new Consumer(fruitBasket, 2, "consumerB"));
        consumerB.start();// 启动消费者B

        // 实例化一个消费者C
        Thread consumerC = new Thread(new Consumer(fruitBasket, 4, "consumerC"));
        consumerC.start();// 启动消费者C
    }
}

 

【模拟电影院购票问题:】
import java.util.HashSet;
import java.util.Set;

/**
 * 电影院类
 */
class Cinema {

    private Set<Integer> seats;// 用于保存当前电影院还存在的位置
    private String cinemaName;// 用于保存电影院的名字

    /**
     * 构造方法,用于构造电影院
     * 
     * @param seats
     *            电影院当前还存在的位置
     * @param cinemaName
     *            电影院的名字
     */
    public Cinema(Set<Integer> seats, String cinemaName) {
        this.seats = seats;
        this.cinemaName = cinemaName;
    }

    /**
     * 获取电影院存在的座位位置
     * 
     * @return 电影院的座位位置
     */
    public Set<Integer> getSeats() {
        return seats;
    }

    /**
     * 设置电影院的座位位置
     * 
     * @param seats
     *            电影院的座位位置
     */
    public void setSeats(Set<Integer> seats) {
        this.seats = seats;
    }

    /**
     * 获取电影院的名字
     * 
     * @return 电影院的名字
     */
    public String getCinemaName() {
        return cinemaName;
    }

    /**
     * 设置电影院的名字
     * 
     * @param cinemaName
     *            电影院的名字
     */
    public void setCinemaName(String cinemaName) {
        this.cinemaName = cinemaName;
    }

    /**
     * 进行购票操作
     * 
     * @param buySeats
     *            需要购买的座位
     * @return true 表示购票成功 false 表示购票失败
     */
    public boolean buyTicket(Set<Integer> buySeats) {
        // 复制一份电影院座位表
        Set<Integer> copy = new HashSet<Integer>();
        copy.addAll(seats);

        // 减去被购买的座位
        copy.removeAll(buySeats);

        // 存在座位
        if (seats.size() - copy.size() == buySeats.size()) {
            seats.removeAll(buySeats);
            return true; // 成功购票
        }
        return false;// 不存在座位,购票失败
    }

    /**
     * 输出电影院剩余座位表
     */
    public void showTicket() {
        System.out.println("电影院当前剩余座位表如下:");
        for (Integer integer : seats) {
            System.out.print(integer + " ");
        }
        System.out.println();
        System.out.println();
    }
}

/**
 * 顾客类
 */
class Customer implements Runnable {

    private Set<Integer> buySeatSet; // 用于保存需要购买的座位
    private Cinema cinema;// 用于保存电影院

    /**
     * 构造方法,初始化顾客
     * 
     * @param buySeatSet
     *            购买的座位数
     * @param cinema
     *            电影院
     */
    public Customer(Set<Integer> buySeatSet, Cinema cinema) {
        this.buySeatSet = buySeatSet;
        this.cinema = cinema;
    }

    /**
     * 获取需要购买的座位
     * 
     * @return 需要购买的座位
     */
    public Set<Integer> getBuySeatSet() {
        return buySeatSet;
    }

    /**
     * 设置需要购买的座位
     * 
     * @param buySeatSet
     *            需要购买的座位
     */
    public void setBuySeatSet(Set<Integer> buySeatSet) {
        this.buySeatSet = buySeatSet;
    }

    /**
     * 获取电影院
     * 
     * @return 电影院
     */
    public Cinema getCinema() {
        return cinema;
    }

    /**
     * 设置电影院
     * 
     * @param cinema
     *            电影院
     */
    public void setCinema(Cinema cinema) {
        this.cinema = cinema;
    }

    @Override
    public void run() {
        synchronized (cinema) { // 对电影院加个锁,每次只允许一个顾客成功买票
            if (buySeatSet.size() > cinema.getSeats().size()) {
                System.out.println("电影院余票不足, 当前余票数为: " + cinema.getSeats().size());
            } else {
                if (cinema.buyTicket(buySeatSet)) {
                    System.out.println(Thread.currentThread().getName() + "购票成功!");
                } else {
                    System.out.println("当前位置不存在," + Thread.currentThread().getName() + "购票失败!");
                }
            }
            cinema.showTicket();
        }
    }
}

/**
 * 测试类,用来模拟电影院购票操作
 */
public class TicketPurchaseDemo {

    public static void main(String[] args) {
        Set<Integer> seats = new HashSet<Integer>();
        for (int i = 1; i <= 10; i++) {
            seats.add(i);
        }
        Cinema cinema = new Cinema(seats, "Wanda");
        cinema.showTicket();

        Set<Integer> buySeats1 = new HashSet<Integer>();
        buySeats1.add(1);
        buySeats1.add(3);
        new Thread(new Customer(buySeats1, cinema), "孙悟空").start();

        Set<Integer> buySeats2 = new HashSet<Integer>();
        buySeats2.add(1);
        buySeats2.add(2);
        new Thread(new Customer(buySeats2, cinema), "猪八戒").start();

        Set<Integer> buySeats3 = new HashSet<Integer>();
        buySeats3.add(2);
        buySeats3.add(4);
        new Thread(new Customer(buySeats3, cinema), "唐三藏").start();
    }
}

 

九、Lambda表达式简单使用

1、采用函数式接口(接口中只有一个方法),避免匿名内部类定义过多。步骤为先实现接口中的方法,然后再调用该方法。
2、规则:

【格式:】
    ()->{}。
若出现一个参数的情况,可以将()省略。
若出现一行代码的情况,可以将{}省略。
对于多个参数的情况,可以省略参数类型,但()不能省略。
若{}中只有一行代码,且为return语句,则可省略return。

【举例:】
interface Demo1 {
    void show();
}

interface Demo2 {
    int show(int a, int b);
}

public class LambdaDemo {
    public static void main(String[] args) {
        Demo1 demo1 = () -> {
            System.out.println("Demo1");
        };
        demo1.show();

        Demo1 demo12 = () -> System.out.println("Demo2");
        demo12.show();

        Demo2 demo3 = (int a, int b) -> {
            return a + b;
        };
        System.out.println(demo3.show(3, 4));

        Demo2 demo4 = (a, b) -> a + b;
        System.out.println(demo4.show(3, 4));
    }
}

 

十、TimerTask与Timer

1、TimerTask抽象类

  java.util.TimerTask,TimerTask是一个抽象类,实现了Runnable接口。内部定义了一个抽象的run方法(public void run();)。其中run方法由子类实现,run方法中为需要被周期性运行的任务(即周期性的代码)。

2、Timer类

  java.util.Timer,有一个schedule方法,用于实现需要被周期性运行的程序。(定时任务,比如设置闹钟)

格式:    
    schedule(TimerTask task, long delay, long period);
其中    
    task为具体实现TimerTask抽象类的run方法的子类。
    delay表示schedule方法运行后到开始调度任务的延迟时间(单位为毫秒)。
    period表示调度任务的周期,即每隔period时间执行一次代码块(单位为毫秒)。
import java.util.Timer;
import java.util.TimerTask;

class TimerTaskDemo extends TimerTask {

    @Override
    public void run() {
        System.out.println("TimerTaskDemo.....");
        System.out.println("当前系统时间为:" + System.currentTimeMillis());
        System.out.println();
    }
}

public class TimerDemo {
    public static void main(String[] args) {
        System.out.println("当前系统时间为:" + System.currentTimeMillis());
        // timer对象启动后,首先经过1000毫秒,第一次执行run()。然后每个2000毫秒执行一次run()。
        Timer timer = new Timer();
        timer.schedule(new TimerTaskDemo(), 1000, 2000);

        try {
            Thread.sleep(4000);//暂停4000毫秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        timer.cancel();// 取消Timer定时器任务
        System.out.println("Timer Game Over.....");
        System.out.println("当前系统时间为:" + System.currentTimeMillis());
    }
}

【结果为:】
当前系统时间为:1561554413988
TimerTaskDemo.....
当前系统时间为:1561554414989

TimerTaskDemo.....
当前系统时间为:1561554416989

Timer Game Over.....
当前系统时间为:1561554417990

 

 

十一、高级多线程控制类

1、ThreadLocal类

  (1)用于保存线程的独立变量,使用ThreadLocal维护变量时,为每个使用该变量的线程提供独立的变量副本,即每个线程可以随意更改自己的变量副本,不会影响其他线程的变量副本。
  (2)Thread内部有一个ThreadLocalMap类型的变量,属于轻量级的Map,使用基本与map类似,但其桶内存放的是entry,而不是entry链表。
  (3)主要方法:
    public void set(T value); //在map集合里维护一个 Thread.currentThread()->value 键值对。
    public T get(); //从map集合中取出 Thread.currentThread() 的value。
  (4)实现类:
    ThreadLocal,会开辟一个新的变量副本。
    InheritableThreadLocal,其extends ThreadLocal,会拷贝上一个线程的变量。

public class ThreadLocalDemo {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    private static ThreadLocal<Double> threadLocal2 = new InheritableThreadLocal<Double>();

    public static void main(String[] args) {
        /**
         * 测试ThreadLocal的get,set方法
         */
        // 设置main线程的名字为MainDemo
        Thread.currentThread().setName("MainDemo");
        // 参数为Integer型,所以初始值为null,所以输出MainDemo-->null
        System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
        // 设置main线程保存的值为2
        threadLocal.set(2);
        // 设置值为2,所以输出MainDemo-->2
        System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());

        /**
         * 此行代码用于测试构造方法与run方法是由哪个线程调用
         */
        new Thread(new ThreadDemo(), "ThreadDemo").start();

        /**
         * 测试ThreadLocal,InheritableThreadLocal的区别
         */
        // 设置main线程保存的值为100
        threadLocal.set(100);
        // 设置值为100,所以输出MainDemo-->100
        System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
        // 启动一个线程ThreadLocalDemo
        new Thread(() -> {
            // ThreadLocal会重新创造一个变量副本,所以输出ThreadLocalDemo-->null
            System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
            threadLocal.set(50);
            new Thread(() -> {
                // ThreadLocal会重新创造一个变量副本,所以输出ThreadLocalDemo1-->null
                System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get());
            }, "ThreadLocalDemo1").start();
        }, "ThreadLocalDemo").start();

        // 设置main线程值为100.0
        threadLocal2.set(100.0);
        // 设置值为100.0,所以输出MainDemo-->100.0
        System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get());
        // 启动一个线程InheritableThreadLocalDemo
        new Thread(() -> {
            // 其会拷贝上一个线程的值,上一个线程为main线程,所以输出为InheritableThreadLocalDemo-->100.0
            System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get());
            threadLocal2.set(50.0);
            new Thread(() -> {
                // 其会拷贝上一个线程的值,上一个线程为InheritableThreadLocalDemo线程,所以输出为InheritableThreadLocalDemo-->50.0
                System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get());
            }, "InheritableThreadLocalDemo1").start();
        }, "InheritableThreadLocalDemo").start();
    }

    public static class ThreadDemo implements Runnable {
        public ThreadDemo() {
            // 初始化时,由main线程调用,所以输出MainDemo-->2
            System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
        }

        @Override
        public void run() {
            // 运行时,由当前线程调用,会重新创建一个变量副本,所以输出ThreadDemo-->null
            System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
        }
    }
}

 

Java1.5提供了一个非常高效实用的多线程包(java.util.concurrent)。

2、ThreadPoolExecutor 类

(1)java.util.concurrent.ThreadPoolExecutor 类就是一个线程池实现类。其最上层的接口为Executor,其有个核心方法 void execute(Runnable command)。Executor接口有一个子接口ExecutorService,ExecutorService的实现类为AbstractExecutorService,而ThreadPoolExcutor是AbstractExecutorService的子类。

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

(2)ThreadPoolExecutor 构造方法

【构造方法:】
  public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue) {
     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
          Executors.defaultThreadFactory(), defaultHandler);
 }
 
 public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           ThreadFactory threadFactory) {
     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
          threadFactory, defaultHandler);
 }
 
 public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           RejectedExecutionHandler handler) {
     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
          Executors.defaultThreadFactory(), handler);
 }
 
 public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

【参数含义:】
corePoolSize:核心线程池的大小,如果核心线程池有空闲位置,此时新的任务就会使用核心线程池并新建一个线程执行,执行完毕后不会销毁线程,线程会进入缓存队列等待再次被运行。
maximunPoolSize:线程池能创建最大的线程数量。如果核心线程池和缓存队列都已经满了,新的任务进来就会创建新的线程来执行。但是数量不能超过maximunPoolSize,否侧会采取拒绝接受任务策略。
keepAliveTime:非核心线程能够空闲的最长时间,超过时间,线程终止。这个参数默认只有在线程数量超过核心线程池大小时才会起作用。只要线程数量不超过核心线程大小,就不会起作用。
unit:时间单位,和keepAliveTime配合使用。
workQueue:缓存队列,用来存放等待被执行的任务。
threadFactory:线程工厂,用来创建线程。
handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略。

(3)线程池大小:
  线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有 3 种形态:

当前线程池大小 :表示线程池中实际工作者线程的数量。
最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;
核心线程大小 (corePoolSize ):表示最小工作的线程数。

如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程。
如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝。

(4)为什么使用线程池?
  提供一个线程队列,队列中保存所有等待状态的线程,减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  可以根据系统的承受能力,调整线程池中工作线程的数目,防止消耗过多的内存而导致死机。

(5)工具类(Executors)

public class Executors {
    // 创建一个固定大小的线程池
    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>());
    }
    
    // 创建一个定长线程池,支持定时及周期性任务执行。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
}

 

posted on 2019-06-26 00:29  累成一条狗  阅读(1358)  评论(1编辑  收藏  举报

导航