1、线程通信
生产者与消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。
该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
生产者与消费者问题中其实隐含了两个问题:
● 线程安全问题:因为生产者与消费者共享数据缓冲区,不过这个问题可以使用同步解决。
● 线程的协调工作问题:
要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知(notify)正在等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让消费者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题。
Object类中提供了wait()、notify()、notifyAll()方法,这三个方法并不属于Thread类,那是因为这三个方法必须有同步监视器对象来调用,而同步监视器对象可以是任意类型的对象,因此它们只能声明在Object类中。
案例:快餐店的厨师、服务员、取餐台
1、一个生产者、一个消费者
案例:有加餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务器从这个工作台取出快餐给顾客。现在有一个厨师和一个服务员。
package com.bdit.part04;
public class Workbench {
private static final int MAX_VALUE = 10;
private int num;
public synchronized void put() {
if(num >= MAX_VALUE){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
//加入睡眠时间是放大问题现象,去掉同步和wait等,可观察问题
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println("厨师制作了一份快餐,现在工作台上有:" + num + "份快餐");
this.notify();
}
public synchronized void take() {
if(num <=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
//加入睡眠时间是放大问题现象,去掉同步和wait等,可观察问题
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
System.out.println("服务员取走了一份快餐,现在工作台上有:" + num + "份快餐");
this.notify();
}
}
package com.bdit.part04;
public class Cook extends Thread{
private Workbench workbench;
public Cook(Workbench workbench) {
super();
this.workbench = workbench;
}
public void run(){
for (int i = 1; i <= 10; i++) {
workbench.put();
}
}
}
package com.bdit.part04;
public class Waiter extends Thread{
private Workbench workbench;
public Waiter(Workbench workbench) {
super();
this.workbench = workbench;
}
public void run(){
for (int i = 1; i <= 10;i++) {
workbench.take();
}
}
}
package com.bdit.part04;
public class TestOneAndOne {
public static void main(String[] args) {
Workbench bench = new Workbench();
Cook c = new Cook(bench);
Waiter w = new Waiter(bench);
c.start();
w.start();
}
}
2、多个生产者与多个消费者
案例:有加餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务器从这个工作台取出快餐给顾客。现在有多个厨师和多个服务员。
package com.bdit.part04;
public class Workbench {
private static final int MAX_VALUE = 10;
private int num;
public synchronized void put() {
while(num >= MAX_VALUE){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
//加入睡眠时间是放大问题现象,去掉同步和wait等,可观察问题
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println("厨师制作了一份快餐,现在工作台上有:" + num + "份快餐");
this.notifyAll();
}
public synchronized void take() {
while(num <=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
//加入睡眠时间是放大问题现象,去掉同步和wait等,可观察问题
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
System.out.println("服务员取走了一份快餐,现在工作台上有:" + num + "份快餐");
this.notifyAll();
}
厨师和服务员线程的代码不变
package com.bdit.part04;
public class TestManyAndMany {
public static void main(String[] args) {
Workbench bench = new Workbench();
Cook c1 = new Cook(bench);
Cook c2 = new Cook(bench);
Waiter w1 = new Waiter(bench);
Waiter w2 = new Waiter(bench);
c1.start();
c2.start();
w1.start();
w2.start();
}
}
2、单例(单态)设计模式
单例设计模式,是软件开发中最常用的设计模式之一,它是指某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。例如:代表JVM运行环境的Runtime类。
通常有饿汉式和懒汉式两种。
2.1、饿汉式单例设计模式
所谓饿汉式,就是在类初始化时,直接创建对象。
优势:因为Java的类加载和初始化的机制可以保证线程安全,所以这类形式的单例设计模式不存在线程安全问题。
【Java加载类是通过一个ClassLoader类加载器来完成的】
劣势:不管你暂时是否需要该实例对象,都会创建,使得类初始化时间加长。
1、直接实例化饿汉式(简洁直观)
package com.bdit.part05;
public class Singleton1 {
public static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
}
2、枚举式(最简洁)
package com.bdit.part05;
public enum Singleton2 {
INSTANCE
}
3、静态代码块饿汉式(适合复杂实例化)
package com.bdit.part05;
import java.io.IOException;
import java.util.Properties;
public class Singleton3 {
public static final Singleton3 INSTANCE;
private String info;
static{
try {
Properties pro = new Properties();
pro.load(Singleton3.class.getClassLoader().getResourceAsStream("single.properties"));
INSTANCE = new Singleton3(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Singleton3(String info){
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
2.2、懒汉式单例设计模式
所谓懒汉式:即延迟创建对象,直到用户来获取这个对象时,再创建。
优势:不用不创建,用时再创建
劣势:有线程安全问题
1、有线程安全问题的形式
线程不安全(适用于单线程)
package com.bdit.part05;
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
public static Singleton4 getInstance(){
if(instance == null){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton4();
}
return instance;
}
}
package com.bdit.part05;
public class TestSingleton4 {
private static Singleton4 instance1;
private static Singleton4 instance2;
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
instance1 = Singleton4.getInstance();
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
instance2 = Singleton4.getInstance();
}
};
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
}
2、无线程安全问题的形式(适用于多线程)
package com.bdit.part05;
public class Singleton5 {
private static Singleton5 instance;
private Singleton5(){
}
public static Singleton5 getInstance(){
if(instance == null){
synchronized (Singleton5.class) {
if(instance == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton5();
}
}
}
return instance;
}
}
3、静态内部类形式(适用于多线程)
package com.bdit.part05;
public class Singleton6 {
private Singleton6(){
}
private static class Inner{
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance(){
return Inner.INSTANCE;
}
}
3、容器类线程安全问题
JDK5.0之后,新增了java.util.concurrent这个包,其中包括了一些确保线程安全的容器类,如ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet,它们在效率与安全性上取得了较好的平衡。
4、线程池
4.1、线程池思想概述
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会存在一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁的创建线程和销毁线程需要时间的。
那么有没有一种办法使得线程可以重复使用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务
在Java中可以通过线程池来达到这样的效果。
4.2、线程池概念
● 线程池:其实就是一个容纳多个线程的容器,其中的线程可以重复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
● 由于线程池中有很多操作都是与优化资源相关的
● 合理利用线程池能够带来的好处:
-
降低资源消耗,减少了创建和销毁线程的次数
-
提高响应速度,当任务到达时,任务可以不需要等待线程创建就可以立即执行
-
提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而导致服务器宕机。
4.3、线程池的应用
java里面线程池的父接口是java.util.concurrent. Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是java.util.concurrent. ExecutorService
要配置一个线程池是比较复杂的,尤其是对线程池的原理不很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池,官方也建议使用Executors工厂类来创建线程池对象。
Executors类由个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,创建的是有界限的线程池,也就是线程池中的个数可以指定最大数量。
获取到了一个线程池对象,那么怎么使用呢?
public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。
使用线程池的步骤:
-
创建线程池对象
-
创建Runnable接口子类对象
-
提交Runnable接口子类对象
-
关闭线程池(一般不做)
package com.bdit;
import javax.swing.plaf.TableHeaderUI;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
*/
public class Demo11 {
public static void main(String[] args) {
//创建线程池对象
ExecutorService service= Executors.newFixedThreadPool(2);
//创建Runnable实例对象
MyRunnable4 myRunnable4=new MyRunnable4();
//从线程池中获取线程对象,执行MyRunnable4中的run方法
service.submit(myRunnable4);
//再次调用
service.submit(myRunnable4);
service.submit(myRunnable4);
//关闭线程池
service.shutdown();
}
}
class MyRunnable4 implements Runnable{
@Override
public void run() {
System.out.println("我是一个线程");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",线程池");
}
}