JavaSE-死锁,线程通信,生产者消费者模式,线程池

1.死锁

        1.1 概述

         死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象。若无外力作用,他们将无法进行下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在等待的进程称为死锁进程。

         1.2 原理

                 1. 某个线程执行完成,需要先后嵌套锁定两个对象,在这个过程中先锁定了第一个对象

                 2. 另一个线程执行完成也需要先后嵌套锁定两个对象,在这个过程中,先锁定了第二个对象.

                 3. 第一个线程执行中,要执行到第二个对象的时候,发现第二个对象被锁定了,于是进入等待状态,等待交出锁。

                 4. 第二个线程执行中,要执行到第一个对象的时候,发现被锁定,进入等待状态

                 5. 此时两个线程都在等待对方交出锁,从而导致死锁。

        1.3 代码实现

public class DeadLock {
	public static void main(String[] args) {
		Object o1 = new Object();
		Object o2 = new Object();
		Thread t1 = new Thread(new Lock(o1, o2));
		Thread t2 = new Thread(new Lock1(o1, o2));
		
		t1.setName("t1");
		t2.setName("t2");
		
		t1.start();
		t2.start();
	}
}

class Lock implements Runnable{
	Object o1;
	Object o2;
	public Lock(Object o1,Object o2){
		this.o1 = o1;
		this.o2 = o2;
	}
	
	@Override
	public void run() {
        // 嵌套锁定
		synchronized(o1) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("锁定线程: " + Thread.currentThread().getName());
			synchronized (o2) {
				System.out.println("锁定线程: " + Thread.currentThread().getName());
			}
		}
		System.out.println("执行结束");
	}
	
}

class Lock1 implements Runnable{
	Object o1;
	Object o2;
	public Lock1(Object o1,Object o2){
		this.o1 = o1;
		this.o2 = o2;
	}
	
	@Override
	public void run() {
        // 嵌套锁定
		synchronized(o2) {
			System.out.println("锁定线程: " + Thread.currentThread().getName());
			synchronized (o1) {
				System.out.println("锁定线程: " + Thread.currentThread().getName());
			}
		}
		System.out.println("执行结束");
	}
	
}

2. 线程通信

         2.1 概述

                 线程通讯是指多个线程之间相互传递信息,以实现协作和同步的过程。

                 线程通讯的目的是为了保证数据的一致性和线程的安全性

                Object类中的方法:

                 wait() : 让当前线程进入等待状态(挂起),并释放锁,当被唤醒后接着挂起时的位置继续执行。如果是无参的,则不会自动唤醒,也可以传入long类型的值,表示多少毫秒之后被唤醒

                 notify() : 唤醒在该对象中挂起的任意一个线程

                 notifyAll() : 唤醒在该对象中挂起的所有线程

                 wait 和 sleep的区别:

                 sleep : 让当前线程进入睡眠状态,和是否加锁没有关系,即使在加锁的方法中也不会释放锁。

                 wait : 让当前线程进入挂起等待状态,并释放锁,必须是加锁的成员方法中。

        2.2 使用方式

public class Thread_00 {
	public static void main(String[] args) {
		PrintNum num = new PrintNum();
		Thread t1 = new Thread(new PrintThread(num));
		Thread t2 = new Thread(new PrintThread(num));
		t1.setName("odd");
		t2.setName("even");
		t1.start();
		
		// 为保证t1先执行,睡眠100毫秒后在执行t2
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		t2.start();
	}
}

class PrintThread implements Runnable{
	PrintNum num;
	public PrintThread(PrintNum num) {
		this.num = num;
	}
	public void run() {
		while(true) {
			num.print();
		}
	};
}
// 间隔打印奇数和偶数,当t1线程进入后,打印count,count加一后,唤醒其它等待的线程,t1挂起。
// 紧接着t2进入,t2打印count后,count加一,t2唤醒t1,t2挂起,重复执行以上操作。
class PrintNum{
	private int count = 1;
	 public synchronized void print(){
		 System.out.println(Thread.currentThread().getName() + " : " + count);
		 count++;
		 // 唤醒在该对象中挂起的所有线程
		 this.notifyAll();
		 try {
			 Thread.sleep(500);
			 // 打印后间隔500毫秒后,挂起
			this.wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	 }
}

3.生产者和消费者

        3.1 概述

                 生产者和消费者是一种通过容器来解决生产者和消费者之间耦合问题的一种设计模式。

 生产者和消费者不直接进行通讯,而是通过容器(通常是一个队列),来进行通讯,生产者产生数据后不需要等待消费者的处理,直接将数据交给容器,消费者也不直接从生产者那里取数据,而是直接从容器里拿。容器相当于一个缓冲区,平衡了消费者和生产者的处理能力。

                 为什么要使用生产者消费者模式:

                 在多线程开发中,如果生产者的处理速度很快,而消费者的处理速度很慢,那么生产者就需要等待消费者处理完,在进行生产。反之如果消费者的处理速度大于生产者,那么消费者就必须等待生产者生产。为了解决这种生产消费能力不均衡的问题,才有了生产者消费者模式。一个简单的例子,快递员给我们送快递, 我们就充当着消费者,快递员就充当着生产者,而快递暂存点就是中间缓冲区,平衡了我们和快递员时间冲突的问题。

         3.2 使用

public class Producer_Consumer {
	public static void main(String[] args) {
		SynStack ss = new SynStack();
		Thread producer = new Thread(new Producer(ss));
		Thread producer1 = new Thread(new Producer(ss));
		Thread consumer = new Thread(new Consumer(ss));
		Thread consumer1 = new Thread(new Consumer(ss));
		
		producer.start();
		producer1.start();
		consumer.start();
		consumer1.start();
	}
	
	
}

// 生产者线程
class Producer implements Runnable{
	SynStack ss;
	public Producer(SynStack ss) {
		this.ss = ss;
	}
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			ss.push((char)('a' + i));
		}
	}
}

// 消费者线程
class Consumer implements Runnable{
	
	SynStack ss;
	public Consumer(SynStack ss) {
		this.ss = ss;
	}
	@Override
	public void run() {
		for (int i = 0; i < 40; i++) {
			ss.pop();
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}





// 创建业务类,向数组中添加和取出数据
class SynStack{
	// 记录操作次数
	private int count = 0;
	// 创建数组,保存数据,充当缓冲区
	private char[] arr = new char[10];
	
	// 向数组中添加数据的方法
	public synchronized void push(char ch) {
		// 判断数组是否已经满了,当满了的时候挂起执行添加操作的生产线程
		while(count == arr.length) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 什么时候唤醒消费者?
		// 要被唤醒的一定是被挂起的消费者,当count等于0,即容器中没有了数据时,消费者才会被挂起,
		// 所以当count等于0时才有需要被唤醒的消费者。当前所在线程为生产者线程,生产者要进行生产了,
		// 容器中即将要有数据了,被挂起的消费者也就不需要在一直等待了,这时唤醒消费者,让消费者准备消费。
		if(count == 0) {
			this.notifyAll();
		}
		
		// 执行添加操作(生产)
		arr[count] = ch;
		count++;
		System.out.println("生产了" + ch + ",剩余" + count + "个数据");
		
	}
	
	// 从数组中取出数据的方法
	public synchronized char pop() {
		// 判断数组是否为空,当数组中没有数据时,挂起执行取出操作的消费线程
		while(count == 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 什么时候唤醒生产者?
		// 要被唤醒的一定是被挂起的生产者,当count等于数组长度时,即容器中数据满了时,生产者才会被挂起,
		// 所以当count等于数组长度时才有需要被唤醒的消费者。当前所在线程为消费者线程,消费者要进行消费了,
		// 容器中要有空位置了,被挂起的生产者也就不需要在一直等待了,这时唤醒生产者,让生产者准备生产。
		if(count == arr.length) {
			this.notifyAll();
		}
		// 执行取出数据操作(消费)
		count--;
		char ch = arr[count];
		System.out.println("消费了" + ch + ",剩余" + count + "个数据");
		return ch;
	}
}

4.线程池

         4.1  线程池的作用

                 限制系统中执行线程的数量,根据系统的环境情况,可以自动或手动的设置线程数量,达到运行的最佳效果。

                 少了浪费系统资源,多了造成系统拥挤降低效率,用线程池控制线程的数量,超出数量的线程排队等候。

                 当一个任务执行完毕后,再从队列中取最前面的任务执行,若队列中没有等待的线程,线程池这一资源处于等待。

                 当一个新的任务需要运行时,如果线程池中有等待中的工作线程,就可以开始执行了,否则进入等待队列。

         4.2  为什么要用线程池

                 1. 减少了创建和销毁现成的次数,每个工作线程都可以被重复利用,可执行多个任务。

                 2. 可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而导致服务器瘫痪。(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就就越多,消耗过多后就会造成死机)

          4.3 几种创建方式

//创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,
//则新建线程。线程池的规模不存在限制。(数量不固定的线程池)
public class NewSingleThreadExecutor_00 {
	public static void main(String[] args) {
		// 创建线程池
		ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			final int index = i;
			try {
				Thread.sleep(index * 1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 向线程池中添加线程
			cachedThreadPool.execute(new Runnable() {
				
				@Override
				public void run() {
					// 每个线程睡眠随机的1~3秒后,打印线程对象名和index,循环执行
					while (true) {
						try {
							Thread.sleep((long)(Math.random() * 3 + 1) * 1000);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName());
						System.out.println(index);
					}
				}
			});
		}
	}
}
//创建一个固定长度线程池,可控制线程最大并发数,
//超出的线程会在队列中等待。(固定数量的线程池)
public class NewSingleThreadExecutor_01 {
	public static void main(String[] args) {
		// 创建固定长度线程池,当前线程池并行数量为3,最多允许3个线程并行,超出的会等待
		ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
		for (int i = 0; i < 10; i++) {
			final int index = i;
			// 向线程池中添加线程
			fixedThreadPool.execute(new Runnable() {

				@Override
				public void run() {
					System.out.println("线程名称为:" + Thread.currentThread().getName() + "执行" + index);
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

				}
			});
		}
	}
}
//创建一个固定长度线程池,支持定时及周期性任务执行。(定时线程池)
public class NewSingleThreadExecutor_02 {
	public static void main(String[] args) {
		//Test1();
		Test2();
	}
	
	public static void Test1(){
		// 创建定时线程池,长度为5
		ScheduledExecutorService scheduThreadPool = Executors.newScheduledThreadPool(5);
		// 参数:1.线程对象 , 2. 延迟初始化(延迟执行) , 3. 计时单位SECONDS(秒) 
		scheduThreadPool.schedule(new Runnable() {
			
			@Override
			public void run() {
				System.out.println("延时3秒");
			}
		},3,TimeUnit.SECONDS);
		// 关闭线程池
		scheduThreadPool.shutdown();
	}
	
	public static void Test2(){
		// 创建定时线程池,长度为5
		ScheduledExecutorService scheduThreadPool = Executors.newScheduledThreadPool(5);
		// 参数:1.线程对象 , 2. 延迟初始化(延迟执行) ,3.间隔时间  4. 计时单位SECONDS(秒) 
		scheduThreadPool.scheduleAtFixedRate(new Runnable() {
			
			@Override
			public void run() {
				System.out.println("延时1秒,间隔3秒");
			}
		},1,3,TimeUnit.SECONDS);
	}
}

/*
 * 单线程化线程池(newSingleThreadExecutor)的优点,串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
 * 此线程池保证所有任务的执行顺序按照任务的提交顺序执行
 */
public class NewSingleThreadExecutor_03 {
	public static void main(String[] args) {
		// 通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;
		// 当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
		// 适用:一个任务一个任务执行的场景 
		ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
		// 一次创建10个请求
		for (int i = 0; i < 10; i++) {
			final int index = i;
			System.out.println("创建" + index + "次");
			// 向线程池中添加线程,只能依次执行
			singleThreadExecutor.execute(new Runnable() {

				@Override
				public void run() {
					System.out.println("线程名称为:" + Thread.currentThread().getName() + "执行" + index);
					try {
						Thread.sleep(2000);
						System.out.println("睡醒后" + index);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

				}
			});
		}
	}
}

5. 单例模式(解决懒汉模式线程安全问题)

        单例模式的懒汉模式存在线程不安全问题,它在第一次调用实例方法时才会去创建对象,这就导致了,如果有多个线程同时调用获取实例的方法,而此时对象还没有创建,那么就有可能多个线程同时判断对象为null,  从而同时去创建对象,破坏单例模式对象的唯一性。

         解决方式:

                1.对获取实例的方法加上synchronized关键字,使得每次只有一个线程能调用该方法,但是会降低性能, 因为每次都需要进行同步操作

class Singletons{
	
	private Singletons(){};
	
	private static Singletons instance;
	// 加方法锁
	public static synchronized Singletons getInstance() {
		if(instance != null) {
			return instance;
		}
		instance = new Singletons();
		return instance;
	}
}

                 2.双重检查锁定,即在获取实例的方法中先判断对象是否为null,如果是,再加锁判断是否为null,如果还是null,才创建对象,这样只有再第一次时才需要同步,减少了同步次数。对象声明需要加volatile关键字,防止指令重排导致的对象状态不一致。

class Singleton{
	// 构造方法私有化
	private Singleton() {};
	private static volatile Singleton instance;
	
	public static Singleton getInstance() {
		// 双重锁
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}	
		}		
		return instance;
	}

你可能感兴趣的:(java,jvm,开发语言)