19.线程安全及线程池

@一贤不穿小鞋

1.线程池的作用

  • 节省频繁的创建线程和销毁线程的消耗系统资源.

2.线程池

  • 存放多个线程对象的容器叫线程池.

3.线程池常用接口和类

3.1:ExecutorService:线程池接口.

  • 常用方法:
  • submit(Runnable task);从线程池中取出一个线程对象执行任务.
  • shutdown(); 关闭线程池.
  • isTerminated(); 如果所有任务在关闭后完成,则返回 true 。

3.2:Executors:线程工具类

  • 常用方法:
  • newSingleThreadExecutor(); 创建拥有单个线程对象的线程池
  • newFixedThreadPool(int nThreads) 创建拥有指定线程对象数量的线程池
  • newCachedThreadPool() 创建可缓存的线程池.

3.3:线程池的使用:

		eg:public static void main(String[] args) {
		//获得单个线程对象的线程池,用父接口作为数据类型,得到子类对象
		//ExecutorService pool1=Executors.newSingleThreadExecutor();
		
		//获得固定线程对象数量的线程池,用父接口作为数据类型,得到子类对象
		//ExecutorService pool1=Executors.newFixedThreadPool(2);
		
		//获得可缓存的线程池,用父接口作为数据类型,得到子类对象
		ExecutorService pool1=Executors.newCachedThreadPool();
		
		
		//用线程对象调用方法从池中取出一个线程对象执行任务
		pool1.submit(new Runnable() {
			/**
			 * 重写父接口中任务方法
			 */
			@Override
			public void run() {
				for (int i = 1; i <=100; i++) {
					System.out.println(Thread.currentThread().getName()+":"+i);
				}
			}
		});
		
		pool1.submit(new Runnable() {
			/**
			 * 重写父接口中任务方法
			 */
			@Override
			public void run() {
				for (int i = 1; i <=100; i++) {
					System.out.println(Thread.currentThread().getName()+":"+i);
				}
			}
		});
		
		pool1.submit(new Runnable() {
			/**
			 * 重写父接口中任务方法
			 */
			@Override
			public void run() {
				for (int i = 1; i <=100; i++) {
					System.out.println(Thread.currentThread().getName()+":"+i);
				}
			}
		});
		
		//关闭线程池
		//pool1.shutdown();
	}

4.临界资源问题

  • 当同一个进程中多个线程共同执行同一个任务时,其中一个线程抢到cpu时间片,操作任务中共享资源,还没来得及对资源进行修改,另一个线程又将cpu时间片捡到了,又去操作任务中共享资源,这时就会再现临界资源问题.

5.解决临界资源问题,要用到线程同步.

6.线程同步

  • 将需要一起执行完的代码绑定在一起形成一个代码块,一个线程进入代码块中,其他的线程必须在外面等待,等待这个线程将代码块执行完,其他线程才能进入这个代码块执行.

7.线程同步的方式:

7.1:同步代码块

  • 将要一起执行的代码绑定在一起,如果一个线程进入这个代码块,它会自动上锁,不让其他的线程进入这个代码块中去执行,直到这个线程执行完这个代码块,自动解锁了,其他的线程才能进入执行.

7.1.1:语法

  • synchronized (锁对象) {
    代码块;(也叫锁范围指的是要一起执行代码)
    }

7.1.2:锁对象可以是java中任何对象,但是这个对象必须是多个线程共享的一个对象,且这个对象的值最好固定不变.

7.1.3:锁范围

  • 越小越好.
eg:public class MyRunnable implements Runnable{
	public int ticket=1000;
	
	//创建锁对象
	public Object ob=new Object();

	/**
	 * 重写任务方法
	 */
	@Override
	public void run() {
		while (true) {
			//同步代码块,线程要排队执行同步代码块,原因是同步代码块每次只允许一个线程进去执行
			synchronized (ob) {
				if (ticket>=1) {
					System.out.println(Thread.currentThread().getName()+"正在销售第"+ticket+"张票");
					ticket--;
				} else {
					break;
				}
			}
		}
	}
}

7.2:同步方法

  • 将要一起执行的代码放在同步方法中,这个方法每次只允许一个线程进去执行, 其他的线程在方法外面等待,等方法中线程将方法执行完,其他的线程才能进入这个方法.

7.2.1:语法

  • 访问修饰符 synchronized 返回值类型 方法名(形参列表) {
    代码块;(锁范围,指要一起执行的代码)
    }

7.2.2:锁范围越小越好.

eg:/**
 * 任务类
 * @author sx
 * @version 1.0 2020年10月30日
 */
public class MyRunnable implements Runnable{
	public int ticket=1000;
	
	//创建锁对象
	public Object ob=new Object();

	/**
	 * 重写任务方法
	 */
	@Override
	public void run() {
		while (true) {
			if(saleTicket()==true) {
				break;
			}
		}
	}
	
	/**
	 * 卖票的同步方法
	 * @return boolean
	 */
	public synchronized boolean saleTicket() {
		if (ticket>=1) {
			System.out.println(Thread.currentThread().getName()+"正在销售第"+ticket+"张票");
			ticket--;
			return false;
		} else {
			return true;
		}
	}
}

7.3:重入锁(对象互斥锁)

  • 实现lock接口,将要一起执行的代码块上锁,每次只允许一个线程进去执行,其他线 程被锁在外面,等这个线程执行完代码块,解锁后,其他的线程才能进去执行.

7.3.1:语法:

				//创建锁对象
				Lock l=new ReentrantLock();
				//上锁
				l.lock()
					代码块;
				//解锁
				l.unlock();

7.3.2:重入锁一定要注意在任意情况下解锁,否则会出现死锁.

eg:public class MyRunnable implements Runnable{
	public int ticket=1000;
	
	//创建重入锁
	Lock l=new ReentrantLock();
	
	/**
	 * 重写任务方法
	 */
	@Override
	public void run() {
		while (true) {
			//上锁
			l.lock();
				try {
					if (ticket>=1) {
					System.out.println(Thread.currentThread().getName()+"正在销售第"+ticket+"张票");
						ticket--;
					} else {
						System.out.println(Thread.currentThread().getName()+"票已售完");
						break;
						
					}
				} finally {
					//解锁
					l.unlock();
				}
				
		}
	}
}

7.4:ReentrantReadWriteLock

  • 读写锁,实现lock接口.支持一写多读.
  • 语法:
	//创建读写锁
	ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
	//通过读写锁,获得读取锁
	ReadLock r=rwl.readLock();
	
	//通过读写锁,获得写入锁
	WriteLock w=rwl.writeLock();
			//上锁
			锁对象.lock()
					代码块;
			//解锁
			锁对象.unlock();
  • 互斥规则:
    写-写:互斥阻塞.
    读-写:互斥,读阻塞写,写阻塞读.
    读-读:不阻塞.
	eg:public class WriteAndReadValue {
	//创建重入锁对象
	//Lock l=new ReentrantLock();
	
	//创建读写锁
	ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
	//通过读写锁,获得读取锁
	ReadLock r=rwl.readLock();
	
	//通过读写锁,获得写入锁
	WriteLock w=rwl.writeLock();
	
	//声明一个变量
	public Integer num;

	/**
	 * 获得属性的值
	 * @return Integer
	 */
	public Integer getNum() {
		//上锁
		//l.lock();
		r.lock();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("读取成功:"+num);
		//解锁
		//l.unlock();
		r.unlock();
		return num;
	}

	/**
	 * 设置属性的值
	 * @param num
	 */
	public void setNum(Integer num) {
		//上锁
		//l.lock();
		w.lock();
		try {
			Thread.sleep(1000);
			this.num = num;
			System.out.println("写入成功:"+num);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//解锁
		//l.unlock();
		w.unlock();
	}
}

		public static void main(String[] args) {
		//创建一个线程池
		ExecutorService pool1=Executors.newFixedThreadPool(20);
		
		//创建对象
		WriteAndReadValue warv=new WriteAndReadValue();
		
		//获得当前系统时间
		long startTime=System.currentTimeMillis();
		
		/*从线程池中取出两个线程执行写值任务*/
		for (int i = 1; i <=2; i++) {
			//从线程池中拿出一个线程执行写值的任务
			pool1.submit(new Runnable() {
				/**
				 * 重写任务方法
				 */
				@Override
				public void run() {
					//给变量写入值
					warv.setNum(11);
				}
			});
		}
		
		/*从线程池中取出18个线程执行读值任务*/
		for (int i = 1; i <=18; i++) {
			//从线程池中拿出一个线程执行写值的任务
			pool1.submit(new Runnable() {
				/**
				 * 重写任务方法
				 */
				@Override
				public void run() {
					//给变量读值
					warv.getNum();
				}
			});
		}
		
		pool1.shutdown();
		//循环判断线程池是否全部关闭
		while(true) {
			//判断所有任务在关闭后完成,则返回 true 。
			if (pool1.isTerminated()) {
				//获得当前系统时间
				long endTime=System.currentTimeMillis();
				//计算20个线程执行的时间
				long time=endTime-startTime;
				System.out.println("执行时间为:"+time);
				break;
			}
		}
	}

8.线程同步:线程越安全,效率越低.

  • 优点:安全,可以解决临界资源问题.
  • 缺点:效率低(既要抢cpu时间片,还要排队等待).

9.(扩展)线程通信:前提要用线程同步.

9.1:使用线程通信的原因

  • 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

9.2:线程通信常用的方法:

  • 注意:下面的三个方法只能在同步方法或同步代码块中用.
  • 对于同一个进程中多个线程,共用同一个锁对象才能唤醒等待的线程.

9.2.1:锁对象.wait();

  • 让当前的线程处于等待(阻塞状态),释放当前线程所占用的锁和cpu时间片,等待其他线程调用notify()或notifyAll()来唤醒当前线程,从当前等待位置接着执行.

9.2.2:锁对象.notify();

  • 唤醒当前锁对象中任一个wait()线程对象

9.2.3:锁对象.notifyAll();

  • 唤醒当前锁对象中所有wait()线程对象
eg:/**
 * 包子类
 * @author sx
 * @version 1.0 2020年11月2日
 */
public class BaoZi {
	/**
	 * 包子编号
	 */
	public Integer id=0;
	/**
	 * 声明一个变量作标记,标记包子编号
	 */
	public static Integer count=0;
	

	public BaoZi() {
		this.id = ++count;
	}
}

/**
 * 包子工厂类
 * @author sx
 * @version 1.0 2020年11月2日
 */
public class BaoZiFactory {
	/**
	 * 声明一个变量作标记,标记包子是否有,true表示有,false表示没有
	 */
	Boolean flag=false;
	
	/**
	 * 声明一个变量存包子总数量
	 */
	public Integer num=100;
	
	/**
	 * 生产包子
	 * @param num 
	 * @return 
	 */
	public synchronized Boolean makeBaoZi() {//1,2,3,4
		while (flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		if (num>0) {
			BaoZi b1=new BaoZi();
			System.out.println(Thread.currentThread().getName()+"生产"+b1.id+"号包子");
			//改变包子数量
			num--;
			//改变标记
			flag=true;
			//唤醒所有线程,只是生产者会重新.wait(),其他消费者才会去消费
			this.notifyAll();
			return false;
		}else {
			return true;
		}
	}
	
	/**
	 * 消费包子
	 * @param num 
	 * @return boolean
	 */
	public synchronized boolean eatBaoZi() {
		while (flag==false) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		if (num>=0) {
			System.out.println(Thread.currentThread().getName()+"消费"+BaoZi.count+"号包子");
			//改变标记
			flag=false;
			//唤醒所有线程,只是消费者会重新.wait(),其他生产者去生产
			this.notifyAll();
			return false;
		}else {
			return true;
		}
	}
}

		public class Producter implements Runnable{
	/**
	 * 声明一个包子工厂对象
	 */
	public BaoZiFactory factory;
	
	/**
	 * 通过构造方法传一个包子工厂对象
	 * @param factory
	 */
	public Producter(BaoZiFactory factory) {
		this.factory = factory;
	}

	/**
	 * 重写任务方法,生产包子
	 */
	@Override
	public void run() {
		while (true) {
			if(factory.makeBaoZi()) {
				break;
			}
		}
	}
}
	
		/**
 * 生产者
 * @author sx
 * @version 1.0 2020年11月2日
 */
public class Customer implements Runnable{
	
	/**
	 * 声明一个包子工厂对象
	 */
	public BaoZiFactory factory;
	
	/**
	 * 通过构造方法传一个包子工厂对象
	 * @param factory
	 */
	public Customer(BaoZiFactory factory) {
		this.factory = factory;
	}

	/**
	 * 重写任务方法,消费包子
	 */
	@Override
	public void run() {
		while(true) {
			if(factory.eatBaoZi()) {
				break;
			}	
		}
	}
}
	
		public static void main(String[] args) {
		//创建一个包子工厂对象 
		BaoZiFactory factory=new BaoZiFactory();
		
		/*创建四个生产者,共享一个任务,生产100个包子*/
		//创建一个生产任务对象
		Producter p1=new Producter(factory);
		//创建四个生产者
		Thread t1=new Thread(p1, "生产者1");
		Thread t2=new Thread(p1, "生产者2");
		Thread t3=new Thread(p1, "生产者3");
		Thread t4=new Thread(p1, "生产者4");
		
		/*创建四个消费者,共享一个任务,消费100个包子*/
		//创建一个消费任务
		Customer c2=new Customer(factory);
		//创建四个消费者
		Thread t5=new Thread(c2, "消费者5");
		Thread t6=new Thread(c2, "消费者6");
		Thread t7=new Thread(c2, "消费者7");
		Thread t8=new Thread(c2, "消费者8");
		//启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		
		t5.start();
		t6.start();
		t7.start();
		t8.start();
	}

9.(了解)线程安全的集合

  • 有下划线的表示线程安全的集合.
    在这里插入图片描述

9.1:CopyOnWriteArrayList(使用与ArrayList一样)

  • 读不锁,写有锁,读写之间不阻塞,优于读写锁.
  • 写入时,先copy一个容器副本,再添加新元素,最后替换引用.

9.2:CopyOnWriteArraySet

  • 底层采用CopyOnWriteArrayList实现,不同在于,使用add添加元素时会调用addIfAbsent()方法,会遍历数组,如元素存在,则不添加.

9.3:ConcurrentHashMap(使用与hashMap一样)

  • 采用分段锁.

9.4:Queue:表示队列先进先出.

  • offer(E e);添加元素.
  • poll() ;获取第一个元素并移除.
  • peek();获取第一个元素但不删除.

9.5:ConcurrentLinkedQueue

  • 线程安全,可高效读写的队列,高并发下性能最好的队列.
  • 无锁,CAS比较交换算法

9.6:BlockingDeque

  • 是Queue的了接口,阻塞的队列,增加了两个线程状态为无限期等待的方法.
  • 适用场景:可用于解决生产者和消费者问题.

9.7:ArrayBlockingQueue

  • 数组结构实现,有界队列.(手工固定上限)

9.8:LinkedBlockingDeque

  • 链表结构实现,无界队列.(默认上限是Integer.MAX_VALUE)

总结:
1.临界资源问题
2.线程同步
3.线程同步的三种方式
4.重入锁和读写锁效率.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值