@一贤不穿小鞋
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.重入锁和读写锁效率.