多线程

备注:若有不正之处,请多谅解并欢迎批评指正。转载请标明链接:https://www.cnblogs.com/pmbb/p/11446341.html 

1. 进程和线程的介绍

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

 

 

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

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

 

 什么是多线程呢?即就是一个程序中有多个线程在同时执行。

通过下图来区别单线程程序与多线程程序的不同:

l 单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。

l 多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。

 

 

2. 进程和线程之间的区别与联系

https://blog.csdn.net/leikun153/article/details/79571208

1.2 程序运行原理

l 分时调度

所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

l 抢占式调度

优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

1.2.1 抢占式调度详解

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

 

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。

其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

主线程

jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。

那么,能否实现一个主线程负责执行其中一个循环,再由另一个线程负责其他代码的执行,最终实现多部分代码同时执行的效果?

能够实现同时执行,通过Java中的多线程技术来解决该问题。

 

2. 线程实现方式之继承自Thread类

 该如何创建线程呢?通过API中搜索,查到Thread类。通过阅读Thread类中的描述。Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

 l 构造方法

 

 l 常用方法

 

继续阅读,发现创建新执行线程有两种方法。

l 一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。

l 另一种方法是声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

1.5 创建线程方式一继承Thread类

创建线程的步骤:

1 定义一个类继承Thread。

2 重写run方法。

3 创建子类对象,就是创建线程对象。

4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。

l 测试类

public class Demo01 {

public static void main(String[] args) {

//创建自定义线程对象

MyThread mt = new MyThread("新的线程!");

//开启新线程

mt.start();

//在主方法中执行for循环

for (int i = 0; i < 10; i++) {

System.out.println("main线程!"+i);

     }

   }

}

l 自定义线程类

public class MyThread extends Thread {

//定义指定线程名称的构造方法

public MyThread(String name) {

//调用父类的String参数的构造方法,指定线程的名称

super(name);

}

/**

 * 重写run方法,完成该线程执行的逻辑

 */

@Override

public void run() {

for (int i = 0; i < 10; i++) {

System.out.println(getName()+":正在执行!"+i);

      }

   }

}

思考:线程对象调用 run方法和调用start方法区别?

线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

1.5.1 继承Thread类原理

我们为什么要继承Thread类,并调用其的start方法才能开启线程呢?

继承Thread类:因为Thread类用来描述线程,具备线程应该有功能。那为什么不直接创建Thread类的对象呢?如下代码:

Thread t1 = new Thread();

t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。

 

创建线程的目的是什么?

是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。

对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。

Thread类run方法中的任务并不是我们所需要的,只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可。所以进行了重写run方法动作。

 

1.5.2 多线程的内存图解

多线程执行时,到底在内存中是如何运行的呢?

以上个程序为例,进行图解说明:

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

 

 

 

 当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

 

1.5.3 获取线程名称

 开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?该如何获取呢?既然是线程的名字,按照面向对象的特点,是哪个对象的属性和谁的功能,那么我们就去找那个对象就可以了。查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。既然找到了,我们就可以试试。

 

 

l Thread.currentThread()获取当前线程对象

l Thread.currentThread().getName();获取当前线程对象的名称

class MyThread extends Thread {  //继承Thread

MyThread(){

super();

}

//复写其中的run方法

public void run(){

for (int i=1;i<=10 ;i++ ){

System.out.println(Thread.currentThread().getName()+",i="+i);

     }

  }

}

class ThreadDemo {

public static void main(String[] args) {

//创建两个线程任务

MyThread d = new MyThread();

MyThread d2 = new MyThread();

d.run();//没有开启新线程, 在主线程调用run方法

d2.start();//开启一个新线程,新线程调用run方法

   }

}

 

 

通过结果观察,原来主线程的名称:main;自定义的线程:Thread-0,线程多个时,数字顺延。如Thread-1......

进行多线程编程时,不要忘记了Java程序运行是从主线程开始,main方法就是主线程的线程执行内容。

 

3. 线程实现方式之实现Runnable接口

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

为何要实现Runnable接口,Runable是啥玩意呢?继续API搜索。

查看Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。

 接口中的方法

 

 l Thread类构造方法

 

创建线程的步骤。

1、定义类实现Runnable接口。

2、覆盖接口中的run方法。。

3、创建Thread类的对象

4、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。

5、调用Thread类的start方法开启线程。

l 代码演示:

public class Demo02 {

public static void main(String[] args) {

//创建线程执行目标类对象

Runnable runn = new MyRunnable();

//将Runnable接口的子类对象作为参数传递给Thread类的构造函数

Thread thread = new Thread(runn);

Thread thread2 = new Thread(runn);

//开启线程

thread.start();

thread2.start();

for (int i = 0; i < 10; i++) {

System.out.println("main线程:正在执行!"+i);

         }

    }

}

l 自定义线程执行任务类

public class MyRunnable implements Runnable{

//定义线程要执行的run方法逻辑

@Override

public void run() {

for (int i = 0; i < 10; i++) {

System.out.println("我的线程:正在执行!"+i);

       }

   }

}

1.6.1 实现Runnable的原理

为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。

创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

1.6.2 实现Runnable的好处

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

1.7 线程的匿名内部类使用

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

l 方式1:创建线程对象时,直接重写Thread类中的run方法

new Thread() {

public void run() {

for (int x = 0; x < 40; x++) {

System.out.println(Thread.currentThread().getName()

+ "...X...." + x);

}

}

}.start();

l 方式2:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法

Runnable r = new Runnable() {

public void run() {

for (int x = 0; x < 40; x++) {

System.out.println(Thread.currentThread().getName()

+ "...Y...." + x);

}

}

};

new Thread(r).start();

4. 线程实现方式之线程池

 线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

 

 

 

 我们详细的解释一下为什么要使用线程池?

 

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

2.2 使用线程池方式--Runnable接口

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

l Executors:线程池创建工厂类

l public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象

l ExecutorService:线程池类

l Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

l Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

 

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

l 创建线程池对象

l 创建Runnable接口子类对象

l 提交Runnable接口子类对象

l 关闭线程池

代码演示:

l Runnable接口实现类

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

public class Test {

public static void main(String[] args) {

//创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象

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

//自己创建线程对象的方式
//Thread t = new Thread(r);
//t.start(); ---> 调用MyRunnable中的run()

//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);

//再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);

//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中

//关闭线程池
//service.shutdown();

}

}
public class MyRunnable implements Runnable{

//定义线程要执行的run方法逻辑
@Override
public void run() {

    System.out.println("我要一个教练");
    try {

        Thread.sleep(2000);

        } catch (InterruptedException e) {

        e.printStackTrace();

        }

        System.out.println("教练来了: " +Thread.currentThread().getName());

        System.out.println("教我游泳,交完后,教练回到了游泳池");

}

}

2.3 使用线程池方式Callable接口

l Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。

l ExecutorService:线程池类

l <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行线程中的call()方法

l Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

 

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

l 创建线程池对象

l 创建Callable接口子类对象

l 提交Callable接口子类对象

l 关闭线程池

代码演示:

l Callable接口实现类,call方法可抛出异常、返回线程任务执行完毕后的结果

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

public class Test {

public static void main(String[] args) {

//创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象

//创建Callable对象
MyCallable c = new MyCallable();

//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(c);

//再获取个教练
service.submit(c);
service.submit(c);

//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中

//关闭线程池
//service.shutdown();

}

}
import java.util.concurrent.Callable;

public class MyCallable implements Callable {

@Override
public Object call() throws Exception {

System.out.println("我要一个教练:call");

Thread.sleep(2000);

System.out.println("教练来了: " +Thread.currentThread().getName());

System.out.println("教我游泳,交完后,教练回到了游泳池");

return null;

}

}

2.4 线程池练习:返回两个数相加的结果

要求:通过线程池中的线程对象,使用Callable接口完成两个数求和操作

l Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

l V get() 获取Future对象中封装的数据结果

代码演示:

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

public class Test {

public static void main(String[] args) throws InterruptedException, ExecutionException {

//创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(2);

//创建一个Callable接口子类对象

//MyCallable c = new MyCallable();
MyCallable c = new MyCallable(100, 200);

MyCallable c2 = new MyCallable(10, 20);

//获取线程池中的线程,调用Callable接口子类对象中的call()方法, 完成求和操作
//<Integer> Future<Integer> submit(Callable<Integer> task)

// Future 结果对象

Future<Integer> result = threadPool.submit(c);

//此 Future 的 get 方法所返回的结果类型

Integer sum = result.get();

System.out.println("sum=" + sum);
//再演示

result = threadPool.submit(c2);

sum = result.get();

System.out.println("sum=" + sum);

//关闭线程池(可以不关闭)

}

}

l Callable接口实现类

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {

//成员变量
int x = 5;
int y = 3;

//构造方法
public MyCallable(){

}

public MyCallable(int x, int y){
        this.x = x;
        this.y = y;
}

@Override
public Integer call() throws Exception {
          return x+y;
}

}

5. 线程的生命周期

一.线程的生命周期及五种基本状态

关于Java中线程的生命周期,首先看一下下面这张较为经典的图:

 

 

上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。主要包括:

Java线程具有五中基本状态

新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就     绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

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

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 

二. Java多线程的就绪、运行和死亡状态

就绪状态转换为运行状态:当此线程得到处理器资源;

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

更多详情:https://www.jianshu.com/p/468c660d02da

6. 线程中常用方法的使用

1、public void start()  使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

2、public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

3、public final void setName(String name) 改变线程名称,使之与参数 name 相同

4、public final void setPriority(int piority) 更改线程的优先级。

5、public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。

6、public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。

7、public void interrupt() 中断线程。

8、public final boolean isAlive() 测试线程是否处于活动状态。

9、public static void static yield() 暂停当前正在执行的线程对象,并执行其他线程。

10、public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

11、public static Thread currentThread() 返回对当前正在执行的线程对象的引用。

 

静态方法:

 1、currentThread()方法

 public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName());
    }

结果:

main

因为是在main函数中运行的,所以得到的名字是main

2、sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。

sleep方法有两个重载版本

 Thread.sleep(int millis);//参数为休眠的毫秒
        Thread.sleep(int millis, int nanos);//第一个为休眠的毫秒,第二个为纳秒

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。如:

/**
 * 线程的sleep方法
 *
 */
public class SleepMethod {
    private int i = 10;
    private Object object = new Object();

    public static void main(String[] args) {
        SleepMethod sm = new SleepMethod();
        MyThread t1 = sm.new MyThread();
        MyThread t2 = sm.new MyThread();
        t1.start();
        t2.start();
    }

    class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                i++;

                try {
                    System.out.println("I:" + i);
                    System.out.println("Thread Name:" + Thread.currentThread().getName() + ",进入睡眠");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread Name:" + Thread.currentThread().getName() + "睡眠结束");
                i++;
                System.out.println("I" + i);
            }
        }
    }
}

运行结果为:

I:11
Thread Name:Thread-0,进入睡眠
Thread Name:Thread-0睡眠结束
I:12
I:13
Thread Name:Thread-1,进入睡眠
Thread Name:Thread-1睡眠结束
I:14

当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。

注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。

3、yield()方法

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

/**
 * yield方法
 *
 */
public class YieldMethod {
    class MyThread extends Thread {
        @Override
        public void run() {

            long beginTime=System.currentTimeMillis();
            int count=0;
            for(int i=0;i<50000000;i++){
                count = count+(i+1);
                Thread.yield();
            }
            long endTime=System.currentTimeMillis();
            System.out.println("用时:"+(endTime-beginTime)+"(ms)");

        }
    }
    public static void main(String[] args) {
        Thread t1=new YieldMethod().new MyThread();
        t1.start();
    }
}

结果:

用时:26003(ms)

注释掉 Thread.yilde(); 之后:

用时:50(ms)

对象方法:

1、start()方法

start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

2、run()方法

run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

3、getId()方法

getId()的作用是取得线程的唯一标识

/**
 * getId方法
 *
 */
public class GetidMethod {
    public static void main(String[] args) {
        Thread t1 = Thread.currentThread();
        System.out.println(t1.getId());
    }
}

输出:

1  

4、isAlive()方法

方法isAlive()的功能是判断当前线程是否处于活动状态

public class IsAliveMethod {
    public static void main(String[] args) {
        Thread t1 = new IsAliveMethod().new MyThread();
        System.out.println("begin:" + t1.isAlive());
        t1.start();
        System.out.println("end:" + t1.isAlive());

    }

    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("IsAlive:" + this.isAlive());
        }
    }
}

运行结果:

begin:false
end:true
IsAlive:true

方法isAlive()的作用是测试线程是否偶处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
有个需要注意的地方  

 System.out.println("IsAlive:" + this.isAlive());

虽然上面的实例中打印的值是true,但此值是不确定的。打印true值是因为myThread线程还未执行完毕,所以输出true。如果代码改成下面这样,加了个sleep休眠:

public static void main(String[] args) {
        Thread t1 = new IsAliveMethod().new MyThread();

        try {
            System.out.println("begin:" + t1.isAlive());
            t1.start();
            t1.sleep(1000);
            System.out.println("end:" + t1.isAlive());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

结果:

begin:false
IsAlive:true
end:false

因为mythread对象已经在1秒之内执行完毕。

5、join()方法 

 在很多情况下,主线程创建并启动了线程,如果子线程中药进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。

/**
 * join方法
 *
 */
public class JoinMethod extends Thread {
    public static void main(String[] args) {
        new JoinMethod("JoinMethod").start();
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                
                try {
                    JoinMethod t1 = new JoinMethod("JoinedMethod main Thread:");
                    t1.start();
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + ":\t" + i);
        }
    }

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

运行结果:

main:0
JoinMethod:    0
JoinMethod:    1
main:1
JoinMethod:    2
main:2
JoinMethod:    3
JoinMethod:    4
main:3
main:4
JoinedMethod main Thread::    0
JoinedMethod main Thread::    1
JoinedMethod main Thread::    2
JoinedMethod main Thread::    3
JoinedMethod main Thread::    4
main:5
main:6
main:7
main:8
main:9

由上可以看出main主线程等待joined thread线程先执行完了才结束的。如果把th.join()这行注释掉,运行结果如下:

main:0
JoinMethod:    0
main:1
JoinMethod:    1
main:2
JoinMethod:    2
main:3
JoinMethod:    3
main:4
JoinMethod:    4
main:5
main:6
main:7
main:8
main:9
JoinedMethod main Thread::    0
JoinedMethod main Thread::    1
JoinedMethod main Thread::    2
JoinedMethod main Thread::    3
JoinedMethod main Thread::    4

6、getName()setName() 用来得到或者设置线程名称

 7、getPriority()setPriority() 用来获取和设置线程优先级。

8、setDaemon()isDaemon() 用来设置线程是否成为守护线程和判断线程是否是守护线程。

守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

7. 多线程中的临界资源问题分析

1.产生原因
临界资源:被多个线程同时访问的资源
如果有多个线程同时访问同一份资源,这个资源对应的值有可能会出现值不准确的情况【临界资源产生的原因:在多个线程访问同一份资源的时候,如果一个线程在取值的过程中,时间片又被其他线程抢走了,临界资源问题就产生了】

2.解决方案
当多个线程同时访问同一份资源的时候,如果其中的一个线程抢到了时间片,如果给这个资源“上一把锁“,这个时候其他剩余的线程只能在锁外面进行等待

3.锁
对象锁:任意的对象都可以充当一把锁
类锁:把任意一个类当做锁,格式:类名.class

4.使用锁来解决临界资源问题
1>同步代码块
语法:
synchronized(锁) {
//需要访问的临界资源
}
说明:
a.程序执行到了这个代码段中,就用锁锁住了临界资源,这个时候,其他的线程将不能执行代码段中的代码,只能在锁外面进行等待
b.当执行完代码段中的代码时,会自动解锁,然后剩下的线程就可以开始争抢cpu时间片
c.一定要保证不同的线程看到的是一把锁,否则解决临界资源问题没有任何意义

同步代码块和对象锁的使用
同步代码块和类锁的使用

2>同步方法
语法:
sychronized 访问权限修饰符 返回值类型 函数名(参数列表) {
//需要访问的临界资源
}

说明:
a.如果一个线程走到这个方法内部,就会用锁来锁住临界资源,其他的线程将不能进入到这个方法的内部
b.隐式锁,如果这个方法是静态方法,锁是类锁【当前类】,如果是一个非静态方法,则是一个对象锁【this】


3>显式锁【同步锁】
通过使用ReentrantLock这个类来进行锁的操作,实现了Lock接口
使用ReentrantLock来进行显式的加锁和解锁
lock():加锁
unlock():解锁

 

先看一个没有同步的例子:

代码如下:

public class Ticket implements Runnable{
    
    //初始车票数共10张(共享资源)
    int ticketNum=10;
    
    @Override
    public void run() {
        //当车票数还有余票,进行买票操作
        if (ticketNum>0) {
            try {
                Thread.sleep(10);
                //输出线程名称(相对于买票人),并计算余票
                System.out.println("currentName: "+Thread.currentThread().getName()
                        +"     余票: "+ --ticketNum);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
        }
        
    }
 
}
public class OutTicket {
 
    public static void main(String[] args) {
        //创建共享资源
        Ticket ticket = new Ticket();
        //创建模拟买票人并开启买票即线程
        Thread thread1 = new Thread(ticket, "老一");
        Thread thread2 = new Thread(ticket, "老二");
        Thread thread3 = new Thread(ticket, "老三");
        Thread thread4 = new Thread(ticket, "老四");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
 
}

输出结果(某次):

currentName: 老四     余票: 8
currentName: 老二     余票: 9
currentName: 老一     余票: 7
currentName: 老三     余票: 9

正常情况下,四个人去买票,余票应该会逐个-1,而不会出现余票相同的情况。这就是多线程同时访问同一资源,造成的数据不准确问题。

为了解决上面的问题,需要引入线程同步的方式实现,当一个线程(人)要使用火车票这个资源时,我们就交给它一把锁,等它把整个买票流程走完之后再把锁给另外一个要用这个资源的线程,反复如此……这样就可以确保数据的准确性和唯一性。

接下来详细说明:

Java中共有两种锁,可以实现线程同步问题,分别是synchronized和ReentrantLock。

synchronized关键字

synchronized简介

  1. synchronized实现同步的基础:java中每个对象都可以作为锁对象。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁,否则线程会一直处于阻塞状态。
  2. synchronized实现同步的表现形式分为两种:同步代码块和同步方法。

synchronized原理

同步代码块:任何一个对象都有一个监视器(Monitor)与之关联,线程执行监视器指令时,会尝试获取对象对应的监视器的所有权,即尝试获得对象的锁。

同步方法:使用synchronized关键字修饰的方法,称之为同步方法。

两者的本质都是对一个对象的监视器的获取。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获取该对象的监视器才能进入同步代码块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,线程状态变为阻塞状态。当成功获取监视器的线程释放了锁后,会唤醒在阻塞同步队列的线程,使其重新尝试对监视器的获取。

synchronized特点

当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器
当前线程在同步代码块或同步方法中出现了未处理的Error或Exception,导致了代码的异常终止,此时线程的同步监视器也会被释放
当前线程在执行同步代码块或同步方法时,执行了同步监视器对象的wait方法,导致当前线程的停止,此时也会释放同步监视器
在执行同步方法或同步代码块,调用了Thread.sleep()、yield()方法来暂停线程,此时线程不会释放同步监视器
针对上面买票的问题(线程同步问题),可以使用同步代码块或同步方法的进行解决:

同步代码块:

public class Ticket implements Runnable{
    
    //初始车票数共10张(共享资源)
    int ticketNum=10;
    
    @Override
    public void run() {
        synchronized (this) {
            //当车票数还有余票,进行买票操作
            if (ticketNum>0) {
                try {
                    Thread.sleep(10);
                    //输出线程名称(相对于买票人),并计算余票
                    System.out.println("currentName: "+Thread.currentThread().getName()
                            +"     余票: "+ --ticketNum);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        }
        
    }
 
}

或同步方法:

public class Ticket implements Runnable{
    
    //初始车票数共10张(共享资源)
    int ticketNum=10;
    
    @Override
    public synchronized void run() {
        
            //当车票数还有余票,进行买票操作
            if (ticketNum>0) {
                try {
                    Thread.sleep(10);
                    //输出线程名称(相对于买票人),并计算余票
                    System.out.println("currentName: "+Thread.currentThread().getName()
                            +"     余票: "+ --ticketNum);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        
    }
 
}

输出结果:

currentName: 老三     余票: 9
currentName: 老四     余票: 8
currentName: 老一     余票: 7
currentName: 老二     余票: 6

从结果上可以看出,使用了同步代码块或者同步方法后,没有出现余票重复的问题,同时保证了余票在上一次的基础上逐个-1,保证数据的准确性和唯一性,解决下线程同步的问题。

ReentrantLock锁

在java 5之前,一直靠synchronized关键字来实现锁功能的,处理多线程并发的问题;而在java 5之后新增了Lock接口来实现锁的功能,同时也Lock接口提供ReentrantLock实现类(可重入锁)。

与synchronized关键字相比,ReentrantLock使用时需要显式的获取或释放锁,而synchronized可以隐式获取和释放锁,也就是说,在正常使用情况下,ReentrantLock需要手动操作锁的获取和释放,synchronized可以自动的获取和释放,从操作性上synchronized是相对便捷的,居然ReentrantLock是手动的,那么也有它的优势,就是可以自定义一些其他的操作,比如中断锁的获取及超时获取锁等多种特性。

下面是关于Lock接口一些主要方法:

void lock(): 执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁。
boolean tryLock(): 如果锁可用,则获取锁,并立即返回true,否则返回false. 该方法和lock()的区别在于,tryLock()只是"试图"获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而lock()方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行. 通常采用如下的代码形式调用tryLock()方法:
void unlock(): 执行此方法时,当前线程将释放持有的锁. 锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生.
Condition newCondition(): 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁。

ReentrantLock的使用

ReentrantLock的使用并不复杂,通常是加锁(获取锁)、释放同步锁即可

public class ReentrantLockTest {
 
    //定义锁对象
    private ReentrantLock lock=new ReentrantLock();
    //定义需要保证的线程安全的方法
    public void method1(){
        //获取锁,加锁
        lock.lock();
        try {
            //需要保证线程安全的代码
        } finally{
            //使用finally来保证锁的释放
            lock.unlock();
        }
    }
 
}

这一结构确保任何时刻只有一个线程进入临界区(临界区是指共享资源的代码区),一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们则被阻塞直到第一个线程释放锁对象。把解锁的操作放在finally中是十分必要的,如果在临界区发生了异常,锁是必须要释放的,否则其他线程将会永远阻塞。

使用ReentrantLock锁解决线程同步问题

针对上面买票的问题,下面可重入锁(ReentrantLock)解决

public class Ticket implements Runnable{
    
    //初始车票数共10张(共享资源)
    int ticketNum=10;
 
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
            
            //当车票数还有余票,进行买票操作
            if (ticketNum>0) {
                try {
                    Thread.sleep(10);
                    lock.lock();
                    try{
                        //输出线程名称(相对于买票人),并计算余票
                        System.out.println("currentName: "+Thread.currentThread().getName()
                                +"     余票: "+ --ticketNum);
                    }finally{
                        lock.unlock();
                    }
                    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        
    }
 
}

重入锁

重入锁,是指同一线程外层函数在获取锁之后,可以在外层内调用其他函数(称之为内层递归函数)也可再次获取该锁对象,是不受影响的。简单点就是,已经有锁对象可以再次获取自己的内部锁。

java内置锁synchronized和ReentrantLock都是可重入锁。

public class SynchronizedTest {
    public void method1() {
        synchronized (SynchronizedTest.class) {
            System.out.println("方法1获得ReentrantTest的锁运行了");
            method2();
        }
    }
    public void method2() {
        synchronized (SynchronizedTest.class) { System.out.println("方法1里面调用的方法2重入锁,也正常运行了"); } } public static void main(String[] args) { new SynchronizedTest().method1(); } }

上面是synchronized的重入锁特性,在调用了method1方法时,已经获取到SynchronizeTest对象锁,如果此时在method1方法内部调用method2方法时,由于method1方法本身已经具有了该锁了,可以再次获取。

public class ReentrantLockTest {
    private Lock lock = new ReentrantLock();
    public void method1() {
        lock.lock();
        try {
            System.out.println("方法1获得ReentrantLock锁运行了");
            method2();
        } finally { lock.unlock(); } } public void method2() { lock.lock(); try { System.out.println("方法1里面调用的方法2重入ReentrantLock锁,也正常运行了"); } finally { lock.unlock(); } } public static void main(String[] args) { new ReentrantLockTest().method1(); } }

上面是ReentrantLock的重入特性,和synchronized是一样的。

公平锁

CPU在调度线程资源时是在等待线程队列里随机挑选一个线程,由于这种随机性所以无法保证线程的先到先得的特点(synchronized控制的锁就是这种非公平锁)。这种非公平现象,有可能造成一些线程(优先级低的线程)都无法获取CPU资源的执行权,而优先级高的线程会不断加强自己执行资源。要解决这种饥饿非公平问题,需要引入公平锁。

公平锁:可以保证线程的执行先后顺序,可以避免非公平现象的产生,但效率会比较低,因为要按顺序执行,需要维护一个有序队列。

公平锁的实现,只需在ReentrantLock的构造函数传入true即可,false则是非公平锁,无参构造函数默认是false

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
 
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

上面买票过程可以看出是无序的,从调用顺序应该从老一到老五是依次的,下面使用公平锁实现顺序效果:

public class Ticket implements Runnable {
 
    private ReentrantLock lock = new ReentrantLock(true);
 
    @Override
    public void run() {
 
        // 当车票数还有余票,进行买票操作
        if (ticketNum > 0) {
 
            lock.lock();
            try {
                // 输出线程名称(相对于买票人),并计算余票
                System.out.println("currentName: "
                        + Thread.currentThread().getName() + "     余票: "
                        + --ticketNum); } finally { lock.unlock(); } } } }

输出结果:

currentName: 老一     余票: 9
currentName: 老二     余票: 8
currentName: 老三     余票: 7
currentName: 老四     余票: 6

synchronized和ReentrantLock的比较

Lock一个接口,提供ReentrantLock实现类,而synchronized是个关键字,是java内置线程同步。
synchronized在发生异常时,会自动的释放线程占用锁对象,不会导致死锁的现象发生,而Lock在发生异常时,如果没有主动的通过unLock方法释放锁对象,则可能会造成死锁的发生,因此在是使用Lock时需要在finally块中释放锁。
Lock可以让等待锁的线程中断,而synchronized则不行,会一直等待下去,直到有唤醒的操作。
Lock可以判断线程是否成功获取锁对象,而synchronized则不行。


ReentrantLock中的一些方法:

isFair()      //判断锁是否是公平锁
 
isLocked()    //判断锁是否被任何线程获取了
 
isHeldByCurrentThread()   //判断锁是否被当前线程获取了
 
hasQueuedThreads()   //判断是否有线程在等待该锁

性能比较

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而 当竞争资源非常激烈时(即有大量线程同时竞争),此时ReentrantLock的性能要远远优于synchronized 。所以说,在具体使用时要根据适当情况选择。

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock对象,性能更高一些。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

拓展:https://blog.csdn.net/hzw2017/article/details/80640426

 

8. 多线程讲解和理解

首先我们要了解什么是串行、并发、并行

串行:一个线程执行到底,相当于单线程。

并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在外人看来就像是多个线程同时执行。

并行:多个线程在不同的cpu中同时执行。

 

并发与并行的区别:

并发严格的说不是同时执行多个线程,只是线程交替执行且速度很快,相当于同时执行。

而并行是同时执行多个线程,也就是多个cpu核心同时执行多个线程。

 

在实际开发中,我们不需要关心是否是并发还是并行,因为cpu会帮我们处理多线程,开发中可以认为多线程就是同时执行多个线程。

线程池:在一定范围内增加线程数量的确可以提升系统的处理能力,但是过多的创建线程将会降低系统的处理速度,如果无限制的创建线程,将会使系统崩溃。

多线程

 

 

  • Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command)
  • ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,以及可跟踪一个或多个异步任务执行状况返回Future的方法
  • AbstractExecutorService:ExecutorService执行方法的默认实现
  • ScheduledExecutorService:一个可定时调度任务的接口
  • ScheduledThreadPoolExecutor:ScheduledExecutorService的实现,一个可定时调度任务的线程池
  • ThreadPoolExecutor:线程池,可以通过调用Executors以下静态工厂方法来创建线程池并返回一个ExecutorService对象

多线程详解:https://blog.csdn.net/xp_zyl/article/details/81531795(超级详细)

9. 多线程在单例中的应用

摘要:
  
  本文首先概述了单例模式产生动机,揭示了单例模式的本质和应用场景。紧接着,我们给出了单例模式在单线程环境下的两种经典实现:饿汉式 和 懒汉式,但是饿汉式是线程安全的,而懒汉式是非线程安全的。在多线程环境下,我们特别介绍了五种方式来在多线程环境下创建线程安全的单例,即分别使用 synchronized方法、synchronized块、静态内部类、双重检查模式 和 ThreadLocal 来实现懒汉式单例,并总结出实现效率高且线程安全的懒汉式单例所需要注意的事项。

一. 单例模式概述

 单例模式(Singleton),也叫单子模式,是一种常用的设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候,整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,显然,这种方式简化了在复杂环境下的配置管理。

  特别地,在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。事实上,这些应用都或多或少具有资源管理器的功能。例如,每台计算机可以有若干个打印机,但只能有一个 Printer Spooler (单例) ,以避免两个打印作业同时输出到打印机中。再比如,每台计算机可以有若干通信端口,系统应当集中(单例) 管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

  综上所述,单例模式就是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种方法。

二. 单例模式及其单线程环境下的经典实现

单例模式应该是23种设计模式中最简单的一种模式了,下面我们从单例模式的定义、类型、结构和使用要素四个方面来介绍它。

1、单例模式理论基础

定义: 确保一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实例)。

类型: 创建型模式

结构:

 

 

 特别地,为了更好地理解上面的类图,我们以此为契机,介绍一下类图的几个知识点:

  • 类图分为三部分,依次是类名、属性、方法;
  • 以<<开头和以>>结尾的为注释信息;
  • 修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见;
  • 带下划线的属性或方法代表是静态的。

三要素:

  • 私有的构造方法;

  • 指向自己实例的私有静态引用;

  • 以自己实例为返回值的静态的公有方法。

2、单线程环境下的两种经典实现

在介绍单线程环境中单例模式的两种经典实现之前,我们有必要先解释一下 立即加载 和 延迟加载 两个概念。

  • 立即加载 : 在类加载初始化的时候就主动创建实例;

  • 延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。

 在单线程环境下,单例模式根据实例化对象时机的不同,有两种经典的实现:一种是 饿汉式单例(立即加载),一种是 懒汉式单例(延迟加载)。饿汉式单例在单例类被加载时候,就实例化一个对象并交给自己的引用;而懒汉式单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。代码示例分别如下:


饿汉式单例:

// 饿汉式单例
public class Singleton1 {

    // 指向自己实例的私有静态引用,主动创建
    private static Singleton1 singleton1 = new Singleton1();

    // 私有的构造方法
    private Singleton1(){}

    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton1 getSingleton1(){
        return singleton1;
    }
}

我们知道,类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

懒汉式单例:

// 懒汉式单例
public class Singleton2 {

    // 指向自己实例的私有静态引用
    private static Singleton2 singleton2;

    // 私有的构造方法
    private Singleton2(){}

    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton2 getSingleton2(){
        // 被动创建,在真正需要使用时才去创建
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

总之,从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。

3、单例模式的优点

  我们从单例模式的定义和实现,可以知道单例模式具有以下几个优点:

  • 在内存中只有一个对象,节省内存空间;

  • 避免频繁的创建销毁对象,可以提高性能;

  • 避免对共享资源的多重占用,简化访问;

  • 为整个系统提供一个全局访问点。

4、单例模式的使用场景

 由于单例模式具有以上优点,并且形式上比较简单,所以是日常开发中用的比较多的一种设计模式,其核心在于为整个系统提供一个唯一的实例,其应用场景包括但不仅限于以下几种:

  • 有状态的工具类对象;
  • 频繁访问数据库或文件的对象;
5、单例模式的注意事项

  在使用单例模式时,我们必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,否则将会实例化一个新对象。此外,在多线程环境下使用单例模式时,应特别注意线程安全问题,我在下文会重点讲到这一点。

三. 多线程环境下单例模式的实现

 在单线程环境下,无论是饿汉式单例还是懒汉式单例,它们都能够正常工作。但是,在多线程环境下,情形就发生了变化:由于饿汉式单例天生就是线程安全的,可以直接用于多线程而不会出现问题;但懒汉式单例本身是非线程安全的,因此就会出现多个实例的情况,与单例模式的初衷是相背离的。下面我重点阐述以下几个问题:

  • 为什么说饿汉式单例天生就是线程安全的?

  • 传统的懒汉式单例为什么是非线程安全的?

  • 怎么修改传统的懒汉式单例,使其线程变得安全?

  • 线程安全的单例的实现还有哪些,怎么实现?

  • 双重检查模式、Volatile关键字 在单例模式中的应用

  • ThreadLocal 在单例模式中的应用

特别地,为了能够更好的观察到单例模式的实现是否是线程安全的,我们提供了一个简单的测试程序来验证。该示例程序的判断原理是:

 开启多个线程来分别获取单例,然后打印它们所获取到的单例的hashCode值。若它们获取的单例是相同的(该单例模式的实现是线程安全的),那么它们的hashCode值一定完全一致;若它们的hashCode值不完全一致,那么获取的单例必定不是同一个,即该单例模式的实现不是线程安全的,是多例的。注意,相应输出结果附在每个单例模式实现示例后。

 

若看官对上述原理不够了解,请移步我的博客《Java 中的 ==, equals 与 hashCode 的区别与联系》

public class Test {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new TestThread();
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }

}

class TestThread extends Thread {
    @Override
    public void run() {
        // 对于不同单例模式的实现,只需更改相应的单例类名及其公有静态工厂方法名即可
        int hash = Singleton5.getSingleton5().hashCode();  
        System.out.println(hash);
    }
}
1、为什么说饿汉式单例天生就是线程安全的?
// 饿汉式单例
public class Singleton1 {

    // 指向自己实例的私有静态引用,主动创建
    private static Singleton1 singleton1 = new Singleton1();

    // 私有的构造方法
    private Singleton1(){}

    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton1 getSingleton1(){
        return singleton1;
    }
}/* Output(完全一致): 
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
 *///:~

我们已经在上面提到,类加载的方式是按需加载,且只加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

2、传统的懒汉式单例为什么是非线程安全的?
// 传统懒汉式单例
public class Singleton2 {

    // 指向自己实例的私有静态引用
    private static Singleton2 singleton2;

    // 私有的构造方法
    private Singleton2(){}

    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton2 getSingleton2(){
        // 被动创建,在真正需要使用时才去创建
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}/* Output(不完全一致): 
        1084284121
        2136955031
        2136955031
        1104499981
        298825033
        298825033
        2136955031
        482535999
        298825033
        2136955031
 *///:~

 上面发生非线程安全的一个显著原因是,会有多个线程同时进入 if (singleton2 == null) {…} 语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。

 3、实现线程安全的懒汉式单例的几种正确姿势

1)、同步延迟加载 — synchronized方法

// 线程安全的懒汉式单例
public class Singleton2 {

    private static Singleton2 singleton2;

    private Singleton2(){}

    // 使用 synchronized 修饰,临界资源的同步互斥访问
    public static synchronized Singleton2 getSingleton2(){
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}/* Output(完全一致): 
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
 *///:~

该实现与上面传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton2()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例。

  从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,那我们考虑使用同步代码块来实现。

  更多关于 synchronized 关键字的介绍, 请移步我的博文《Java 并发:内置锁 Synchronized》

2)、同步延迟加载 — synchronized块
// 线程安全的懒汉式单例
public class Singleton2 {

    private static Singleton2 singleton2;

    private Singleton2(){}


    public static Singleton2 getSingleton2(){
        synchronized(Singleton2.class){  // 使用 synchronized 块,临界资源的同步互斥访问
            if (singleton2 == null) { 
                singleton2 = new Singleton2();
            }
        }
        return singleton2;
    }
}/* Output(完全一致): 
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
 *///:~

该实现与上面synchronized方法版本实现类似,此不赘述。从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率仍然比较低,事实上,和使用synchronized方法的版本相比,基本没有任何效率上的提高。

3)、同步延迟加载 — 使用内部类实现延迟加载
// 线程安全的懒汉式单例
public class Singleton5 {

    // 私有内部类,按需加载,用时加载,也就是延迟加载
    private static class Holder {
        private static Singleton5 singleton5 = new Singleton5();
    }

    private Singleton5() {

    }

    public static Singleton5 getSingleton5() {
        return Holder.singleton5;
    }
}
/* Output(完全一致): 
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
 *///:~

如上述代码所示,我们可以使用内部类实现线程安全的懒汉式单例,这种方式也是一种效率比较高的做法,它与饿汉式单例的区别就是:这种方式不但是线程安全的,还是延迟加载的,真正做到了用时才初始化。

当客户端调用getSingleton5()方法时,会触发Holder类的初始化。由于singleton5是Hold的类成员变量,因此在JVM调用Holder类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。在这种情形下,其他线程虽然会被阻塞,但如果执行类构造器方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行类构造器,因为 在同一个类加载器下,一个类型只会被初始化一次,因此就保证了单例。

四. 单例模式与双重检查(Double-Check idiom)

使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率,对应的代码清单如下:

// 线程安全的懒汉式单例
public class Singleton3 {

    //使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例
    private static volatile Singleton3 singleton3;

    private Singleton3() {
    }

    public static Singleton3 getSingleton3() {
        // Double-Check idiom
        if (singleton3 == null) {
            synchronized (Singleton3.class) {       // 1
                // 只需在第一次创建实例时才同步
                if (singleton3 == null) {       // 2
                    singleton3 = new Singleton3();      // 3
                }
            }
        }
        return singleton3;
    }
}/* Output(完全一致): 
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
 *///:~

如上述代码所示,为了在保证单例的前提下提高运行效率,我们需要对 singleton3 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是我们必须注意一点:必须使用volatile关键字修饰单例引用。
那么,如果上述的实现没有使用 volatile 修饰 singleton3,会导致什么情形发生呢? 为解释该问题,我们分两步来阐述:

(1)、当我们写了 new 操作,JVM 到底会发生什么?

首先,我们要明白的是: new Singleton3() 是一个非原子操作。代码行singleton3 = new Singleton3(); 的执行过程可以形象地用如下3行伪代码来表示:

memory = allocate();        //1:分配对象的内存空间
ctorInstance(memory);       //2:初始化对象
singleton3 = memory;        //3:使singleton3指向刚分配的内存地址

但实际上,这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,也就是说其真实执行顺序可能是下面这种:

memory = allocate();        //1:分配对象的内存空间
singleton3 = memory;        //3:使singleton3指向刚分配的内存地址
ctorInstance(memory);       //2:初始化对象

这段伪代码演示的情况不仅是可能的,而且是一些 JIT 编译器上真实发生的现象。

(2)、重排序情景再现 

 了解 new 操作是非原子的并且可能发生重排序这一事实后,我们回过头看使用 Double-Check idiom 的同步延迟加载的实现:

  我们需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 singleton3 来引用此对象。这行代码存在的问题是,在 Singleton 构造函数体执行之前,变量 singleton3 可能提前成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程将得到的是一个不完整(未初始化)的对象,会导致系统崩溃。下面是程序可能的一组执行步骤:

 1、线程 1 进入 getSingleton3() 方法;
  2、由于 singleton3 为 null,线程 1 在 //1 处进入 synchronized 块;
  3、同样由于 singleton3 为 null,线程 1 直接前进到 //3 处,但在构造函数执行之前,使实例成为非 null,并且该实例是未初始化的;
  4、线程 1 被线程 2 预占;
  5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 得到一个不完整(未初始化)的 Singleton 对象;
  6、线程 2 被线程 1 预占。
  7、线程 1 通过运行 Singleton3 对象的构造函数来完成对该对象的初始化。
显然,一旦我们的程序在执行过程中发生了上述情形,就会造成灾难性的后果,而这种安全隐患正是由于指令重排序的问题所导致的。让人兴奋地是,volatile 关键字正好可以完美解决了这个问题。也就是说,我们只需使用volatile关键字修饰单例引用就可以避免上述灾难。

  特别地,由于 volatile关键字的介绍和 类加载及对象初始化顺序两块内容已经在我之前的博文中介绍过,再此只给出相关链接,不再赘述。

  更多关于volatile关的介绍, 请移步我的博文《 Java 并发:volatile 关键字解析》

  更多关于类加载及对象初始化顺序的介绍, 请移步我的博文《 Java 继承、多态与类的复用》

五. 单例模式 与 ThreadLocal

借助于 ThreadLocal,我们可以实现双重检查模式的变体。我们将临界资源instance线程私有化(局部化),具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为线程局部范围内的操作,对应的代码清单如下:

public class Singleton {

    // ThreadLocal 线程局部变量,将单例instance线程私有化
    private static ThreadLocal<Singleton> threadlocal = new ThreadLocal<Singleton>();
    private static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance() {

        // 第一次检查:若线程第一次访问,则进入if语句块;否则,若线程已经访问过,则直接返回ThreadLocal中的值
        if (threadlocal.get() == null) {
            synchronized (Singleton.class) {
                if (instance == null) {  // 第二次检查:该单例是否被创建
                    instance = new Singleton();
                }
            }
            threadlocal.set(instance); // 将单例放入ThreadLocal中
        }
        return threadlocal.get();
    }
}/* Output(完全一致): 
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
*///:~

借助于 ThreadLocal,我们也可以实现线程安全的懒汉式单例。但与直接双重检查模式使用,本实现在效率上还不如后者。

  更多关于ThreadLocal 的介绍, 请移步我的博文《 Java 并发:深入理解 ThreadLocal》

六. 小结

本文首先介绍了单例模式的定义和结构,并给出了其在单线程和多线程环境下的几种经典实现。特别地,我们知道,传统的饿汉式单例无论在单线程还是多线程环境下都是线程安全的,但是传统的懒汉式单例在多线程环境下是非线程安全的。为此,我们特别介绍了五种方式来在多线程环境下创建线程安全的单例,

 当然,实现懒汉式单例还有其他方式。但是,这五种是比较经典的实现,也是我们应该掌握的几种实现方式。从这五种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:

  • 尽量减少同步块的作用域;

  • 尽量使用细粒度的锁。

 

10. 死锁介绍以及案例演示

死锁:当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。比如,线程1已经持有了A锁并想要获得B锁的同时,线程2持有B锁并尝试获取A锁,那么这两个线程将永远地等待下去。

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

java 死锁产生的四个必要条件:

  • 1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。

解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现。

而如果不恰当的使用了锁,且出现同时要锁多个对象时,会出现死锁情况,如下:

import java.util.Date;
 
public class LockTest {
   public static String obj1 = "obj1";
   public static String obj2 = "obj2";
   public static void main(String[] args) {
      LockA la = new LockA();
      new Thread(la).start();
      LockB lb = new LockB();
      new Thread(lb).start();
   }
}
class LockA implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockA 开始执行");
         while(true){
            synchronized (LockTest.obj1) {
               System.out.println(new Date().toString() + " LockA 锁住 obj1");
               Thread.sleep(3000); // 此处等待是给B能锁住机会
               synchronized (LockTest.obj2) {
                  System.out.println(new Date().toString() + " LockA 锁住 obj2");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
class LockB implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockB 开始执行");
         while(true){
            synchronized (LockTest.obj2) {
               System.out.println(new Date().toString() + " LockB 锁住 obj2");
               Thread.sleep(3000); // 此处等待是给A能锁住机会
               synchronized (LockTest.obj1) {
                  System.out.println(new Date().toString() + " LockB 锁住 obj1");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

以上代码运行输出结果为:

Tue May 05 10:51:06 CST 2015 LockB 开始执行
Tue May 05 10:51:06 CST 2015 LockA 开始执行
Tue May 05 10:51:06 CST 2015 LockB 锁住 obj2
Tue May 05 10:51:06 CST 2015 LockA 锁住 obj1

此时死锁产生。

那么要怎么预防死锁呢?下面介绍几个常见方法:

1、避免一个线程同时获取多个锁

2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源

3、尝试使用定时锁,使用lock.tryLock来代替使用内置锁。

11. 生产者与消费者设计模式

 ★简介

   生产者消费者模式并不是GOF提出的23种设计模式之一,23种设计模式都是建立在面向对象的基础之上的,但其实面向过程的编程中也有很多高效的编程模式,生产者消费者模式便是其中之一,它是我们编程过程中最常用的一种设计模式。

  在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。

 单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图。

 

 

 为了不至于太抽象,我们举一个寄信的例子(虽说这年头寄信已经不时兴,但这个例子还是比较贴切的)。假设你要寄一封平信,大致过程如下:

    1、你把信写好——相当于生产者制造数据

    2、你把信放入邮筒——相当于生产者把数据放入缓冲区

    3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区

    4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据

 ★优点

   可能有同学会问了:这个缓冲区有什么用捏?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?搞出这么一个缓冲区作甚?

    其实这里面是大有讲究的,大概有如下一些好处。

◇解耦

  假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。

    接着上述的例子,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。

◇支持并发(concurrency)

生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。

    使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种,后面的帖子会讲两种并发类型下的应用)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。

    其实当初这个模式,主要就是用来处理并发问题的。

    从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。不管是哪种方法,都挺土的。

 ◇支持忙闲不均

   缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。

    为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。

    费了这么多口水,希望原先不太了解生产者/消费者模式的同学能够明白它是怎么一回事。接下来说说数据单元。

 ★啥是数据单元

  何谓数据单元捏?简单地说,每次生产者放到缓冲区的,就是一个数据单元;每次消费者从缓冲区取出的,也是一个数据单元。对于前一个帖子中寄信的例子,我们可以把每一封单独的信件看成是一个数据单元。

    不过光这么介绍,太过于简单,无助于大伙儿分析出这玩意儿。所以,后面咱们来看一下数据单元需要具备哪些特性。搞明白这些特性之后,就容易从复杂的业务逻辑中分析出适合做数据单元的东西了。

★数据单元的特性

 分析数据单元,需要考虑如下几个方面的特性:

 ◇关联到业务对象

 首先,数据单元必须关联到某种业务对象。在考虑该问题的时候,你必须深刻理解当前这个生产者/消费者模式所对应的业务逻辑,才能够作出合适的判断。

    由于“寄信”这个业务逻辑比较简单,所以大伙儿很容易就可以判断出数据单元是啥。但现实生活中,往往没这么乐观。大多数业务逻辑都比较复杂,当中包含的业务对象是层次繁多、类型各异。在这种情况下,就不易作出决策了。

    这一步很重要,如果选错了业务对象,会导致后续程序设计和编码实现的复杂度大为上升,增加了开发和维护成本。

◇完整性

  所谓完整性,就是在传输过程中,要保证该数据单元的完整。要么整个数据单元被传递到消费者,要么完全没有传递到消费者。不允许出现部分传递的情形。

    对于寄信来说,你不能把半封信放入邮筒;同样的,邮递员从邮筒中拿信,也不能只拿出信的一部分。

 ◇独立性

 所谓独立性,就是各个数据单元之间没有互相依赖,某个数据单元传输失败不应该影响已经完成传输的单元;也不应该影响尚未传输的单元。

    为啥会出现传输失败捏?假如生产者的生产速度在一段时间内一直超过消费者的处理速度,那就会导致缓冲区不断增长并达到上限,之后的数据单元就会被丢弃。如果数据单元相互独立,等到生产者的速度降下来之后,后续的数据单元继续处理,不会受到牵连;反之,如果数据单元之间有某种耦合,导致被丢弃的数据单元会影响到后续其它单元的处理,那就会使程序逻辑变得非常复杂。

    对于寄信来说,某封信弄丢了,不会影响后续信件的送达;当然更不会影响已经送达的信件。

◇颗粒度

 前面提到,数据单元需要关联到某种业务对象。那么数据单元和业务对象是否要一一对应捏?很多场合确实是一一对应的。

    不过,有时出于性能等因素的考虑,也可能会把N个业务对象打包成一个数据单元。那么,这个N该如何取值就是颗粒度的考虑了。颗粒度的大小是有讲究的。太大的颗粒度可能会造成某种浪费;太小的颗粒度可能会造成性能问题。颗粒度的权衡要基于多方面的因素,以及一些经验值的考量。

    还是拿寄信的例子。如果颗粒度过小(比如设定为1),那邮递员每次只取出1封信。如果信件多了,那就得来回跑好多趟,浪费了时间。

    如果颗粒度太大(比如设定为100),那寄信的人得等到凑满100封信才拿去放入邮筒。假如平时很少写信,就得等上很久,也不太爽。

    可能有同学会问:生产者和消费者的颗粒度能否设置成不同大小(比如对于寄信人设置成1,对于邮递员设置成100)。当然,理论上可以这么干,但是在某些情况下会增加程序逻辑和代码实现的复杂度。后面讨论具体技术细节时,或许会聊到这个问题。

    好,数据单元的话题就说到这。希望通过本帖子,大伙儿能够搞明白数据单元到底是怎么一回事。

    [2]:队列缓冲区

    经过前面两个帖子的铺垫,今天终于开始聊一些具体的编程技术了。由于不同的缓冲区类型、不同的并发场景对于具体的技术实现有较大的影响。为了深入浅出、便于大伙儿理解,咱们先来介绍最传统、最常见的方式。也就是单个生产者对应单个消费者,当中用队列(FIFO)作缓冲。

    关于并发的场景,在之前的帖子“进程还线程?是一个问题!”中,已经专门论述了进程和线程各自的优缺点,两者皆不可偏废。所以,后面对各种缓冲区类型的介绍都会同时提及进程方式和线程方式。

生产者消费者实现思路

 

 

实现

通过上述的分析后,我们来用最基本的Java代码实现一下

我们先来定义一下Consumer 和Producer ,他们的逻辑比较简单,这里我们只循环十次模拟一下生产消费的场景。Buffer 为缓冲区,我们待会再看

/**
 * 消费者
 */
class Consumer extends Thread {
    private Buffer buffer;
    private int number;

    public Consumer(Buffer b, int number) {
        buffer = b;
        this.number = number;
    }

    public void run() {
        int value;
        for (int i = 0; i < 10; i++) {
            // 从缓冲区中获取数据
            value = buffer.get();
            try {
                // 模拟消费数据
                sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("消费者 #" + this.number + " got: " + value);
        }
    }
}

/**
 * 生产者
 */
class Producer extends Thread {
    private Buffer buffer;
    private int number;

    public Producer(Buffer b, int number) {
        buffer = b;
        this.number = number;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                // 模拟生产数据
                sleep(500);
            } catch (InterruptedException e) {
            }
            
            // 将数据放入缓冲区
            buffer.put(i);
            System.out.println("生产者 #" + this.number + " put: " + i);
        }
    }
}

可以看到 Consumer 和Producer 没有什么逻辑,只是对缓冲区的读写操作,下面我们来重点看一下 Buffer的实现

/**
 * 缓冲区
 */
class Buffer {
    private List<Integer> data = new ArrayList<>();
    private static final int MAX = 10;
    private static final int MIN = 0;

    public synchronized int get() {
        while (MIN == data.size()) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        Integer i = data.remove(0);
        notifyAll();
        return i;
    }

    public synchronized void put(int value) {
        while (MAX == data.size()) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        data.add(value);
        notifyAll();
    }
}

测试类

public class ProducerConsumer {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        Producer p1 = new Producer(buffer, 1);
        Consumer c1 = new Consumer(buffer, 1);
        p1.start();
        c1.start();
    }
}

分析
put(): 生产者向缓冲区写入数据的操作,当MAX == data.size()时,就是我们刚刚所说的生产者速度过快,消费者速度过慢的情况,这个时候身为 "缓冲区" 要平衡一下,调用 Object.wait(),让当前生产者线程进入挂起状态,等待消费者消费数据后将其唤醒

MAX > data.size()时,向ArrayList中添加数据,并尝试唤醒正在等待的消费者(这一步是必须的)

get(): 消费者向缓冲区读数据的操作,和上述逻辑相反,当MIN == data.size()时,这时消费者速度太快,生产者太慢,队列中已经没有数据了。"缓冲区" 再一次站了出来,通过wait(),让当前消费者线程进入挂起状态,等待生产者生产数据后将其唤醒

MIN < data.size()时,取出ArrayList中第一条数据,并尝试唤醒正在等待的生产者

 

上述使用最基本的Java代码实现生产者消费者模式,实际开发中我们可能会使用BlockingQueueReentrantLockThreadPoolExecutor这些更成熟的轮子,但是一通百通

关于上述案例的思考
    1. 为什么缓冲区的判断条件是 while(condition) 而不是 if(condition)
      答:防止线程被错误的唤醒
      举例:当有两个消费者线程wait() 时,此时生产者在队列里放入了一条数据,并调用notifyAll(), 两个消费者线程被唤醒,第一个消费者成功取出队列中数据,而第二个消费者此时就是被错误的唤醒了,程序抛出异常,所以此处使用 while(condition)循环检查

    2. Java中要求wait()方法为什么要放在同步块中?
      答:防止出现Lost Wake-Up
      举例:如果队列没有同步限制,消费者和生产者并发执行,很可能出现这种情况,消费者这时候检查了条件正准备wait(),这时候上下文切换到了生产者,生产者咔咔一顿操作向队列中添加了数据,并唤醒了消费者,而此时消费者并没有wait(),这个通知就丢掉了,然后消费者wait() 就这样睡去了...

    3. 为什么缓冲区一定要使用阻塞队列实现?
      同理就是为了防止出现Lost Wake-Up

想了解更多:https://blog.csdn.net/u011109589/article/details/80519863

 

 

 

 

 

 

 

 

 

 

参考网址:

https://www.cnblogs.com/jmsjh/p/7762034.html

生命周期:https://blog.csdn.net/houbin0912/article/details/77969563

线程常用方法:https://www.cnblogs.com/x-you/p/8608604.html

临界资源问题:https://www.cnblogs.com/lsp-lsp/p/7344403.html

https://blog.csdn.net/qq_20050621/article/details/82148641

synchronized和ReentrantLock:https://blog.csdn.net/hzw2017/article/details/80628334

多线程理解:https://blog.csdn.net/xp_zyl/article/details/81531795

多线程在单例中的应用:https://blog.csdn.net/justloveyou_/article/details/64127789

死锁:https://www.runoob.com/java/thread-deadlock.html

https://www.cnblogs.com/easen/p/7526976.html

生产与设计模式;https://blog.csdn.net/u011109589/article/details/80519863

https://www.jianshu.com/p/bc361d7ae821

转载于:https://www.cnblogs.com/pmbb/p/11446341.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值