Java—并发与多线程
线程是cpu调度的基本单位,一个进程的默认开启的线程有主线程main()、垃圾回收线程GC等
1、线程的创建
继承Thread类,重写run()方法,然后创建线程对象,调用start()方法启动线程。好处是简单,坏处是Java是单继承,一个类继承Thread类后就不能继承其他类了。
实现Runnable接口,实现run()方法,创建Runnable接口实现类对象,并把该对象作为参数创建一个Thread对象,然后调用start()方法启动线程。(推荐使用)
实现callable接口,重写call()方法
2、静态代理(Thread底层的设计模式)
真实对象和代理对象都要实现共同接口,代理对象要代理真实角色完成共同接口中的方法。好处是代理对象除了可以完成真实对象能完成的事情,还可以完成很多真实对象完成不了的事情。真实对象可以专注于自己的事情。
3、Lambda表达式
Java 1.8的新特性,允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。Lambda 表达式主要用来定义行内执行的函数式接口(只包含一个抽象方法的接口叫函数式接口,比如Runnable接口)
4、线程的五大状态
链接: link.
创建状态:执行new之后,线程就被创建了
就绪状态:调用start()方法后,线程进入就绪状态
运行状态:cpu调度执行,线程进入运行状态
阻塞状态:调用sleep()、wait()或同步锁定时,线程进入阻塞状态。每个对象都有一个锁,当一个线程对象调用sleep()方法时,在sleep()结束前不会释放锁。
死亡状态:线程中断或结束就进入死亡状态
5、yield、join、线程的优先级
线程礼让:当一个线程在执行的过程中调用yield()方法,该线程就会让出cpu给其他线程使用,进入就绪状态(注意不是进入阻塞态)。礼让不一定成功,要看cpu心情
join:在一个线程执行期间,调用另一个线程的join方法,那么该正在执行的线程就会让出cpu,被另一个线程强行插队
线程的优先级:最小为1,最大为10;默认优先级为5.
6、多线程同步
链接: link.
Synchronized关键字:可以用于修饰方法,也可以用于修饰代码块。被修饰的方法和代码块就是同步的方法和代码块。
Synchronized方法:方法在声明时加上Synchronized关键字就可以。
Synchronized代码块:可以把任意的代码段声明为synchronized,可以指定上锁的对象
//同步代码块,syncObject为要上锁的对象
synchronized(syncObject){
//访问synchObject的代码
}
Lock锁:ReentrantLock(可重入锁):
//ReenreantLock类的常用方法有:
// ReentrantLock() : 创建一个ReentrantLock实例
// lock() : 获得锁
// unlock() : 释放锁
class Bank {
private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();//上锁
try{
account += money;
}finally{
lock.unlock();//执行完操作解锁
}
}
Synchronized 和Lock的区别:
①、Synchronized 时Java内置的关键字,Lock是一个Java类
②、Synchronized 无法判断锁的状态,Lock可以判断是否获取到了锁(lock.tryLock())
③、Synchronized 会自动释放锁,Lock必须手动释放锁。
④、Synchronized 可重入锁,不可以中断,非公平;Lock 可重入锁,可以判断锁,可自己设置是否非公平
7、死锁
链接: link.
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。
死锁产生的4个必要条件
互斥条件、请求和保持条件、不剥夺条件、环路等待条件
避免死锁:银行家算法
8、生产者与消费者问题
Synchronized+线程通信(wait()和notify()/notifyAll())
管程法:生产者负责生产数据,消费者负责消费数据,缓冲区用于存放数据。生产者把生产的数据放入缓冲区,消费者从缓冲区取出数据进行消费
信号灯法:生产者负责生产数据,消费者负责消费数据。标志位(信号灯)用于判断当前状态,如果当前状态是生产者正在生产,那么消费者就不能消费,进入等待;如果当前状态是消费者正在消费,那么生产者就不能生产,进入等待。
虚假唤醒问题:
链接: link.
9、线程池
链接: link.
链接: link.
链接: link.
链接: link.
链接: link.
链接: link.
使用线程池的好处:
(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
线程创建的开销
对操作系统来说,创建一个线程的代价是十分昂贵的, 需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。【分配内存、列入调度、内存换页、清空缓存和重新读取】
线程池的返回值ExecutorService:ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程
java提供了4种线程池的实现如下(返回值都是ExecutorService):
(1)Executors.newCacheThreadPool():可缓存线程池如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
(2)Executors.newFixedThreadPool ():创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
(3)Executors.newScheduledThreadPool ():创建一个定长线程池,支持定时及周期性任务执行。
(4)Executors.newSingleThreadExecutor ():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所 所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
向线程池提交任务以及关闭线程池:
可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功
也可以使用submit 方法来提交任务,它会返回一个Future对象,那么我们可以通过这个Future对象来判断任务是否执行成功,通过Future对象的get方法来获取返回值
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池
以上4个线程池类底层都是用ThreadPoolExecutor类进行初始化的
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler ) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数解释:
corePoolSize:核心池大小,意思是当超过这个范围的时候,就需要将新的线程放到等待队列中了即workQueue;
maximumPoolSize:线程池最大线程数量,表明线程池能创建的最大线程数
keepAlivertime:当活跃线程数大于核心线程数,空闲的多余线程最大存活时间。
unit:存活时间的单位
workQueue:存放任务的队列---阻塞队列
ThreadFactory:线程创建工厂
handler:超出线程范围(maximumPoolSize)和队列容量的任务的处理程序
workQueue 工作队列(Blocking Quene 阻塞队列),有4种类型
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
偷~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~的!