1、线程、进程
进程是资源分配的基本单位,一个进程可以包含多个线程,每条线程执行不同的任务,不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。
线程是进程中执行运算的最小单位,虽然同一进程中的线程共享该进程的所有资源,但是每个线程也有自己独立的栈内存,用来存放本地数据。
2、创建线程的三种方式(实现多线程的四种方式)
(1)继承Thread类,重写run()方法
(2)实现Runnable接口,重写run()方法,【 实现Runnable接口的实现类的实例对象作为Thread的构造器的参数来创建线程,如:Thread thread = new Thread( new RunnableDemo() ) ; 】
(3)实现Callable接口,重写call方法,并使用FutureTask创建线程和获取返回值,而且callable还可以抛出异常。
FutureTask<Integer> ft = new FutureTask<>(new MyTask()); //使用FutureTask运行线程并获得返回值
Thread t = new Thread(ft);
t.start();
//自己获取返回值
System.out.println(ft.get());
(4)通过线程池来创建线程
3、线程的生命周期
1、线程的一生会经历5个状态
- 新建:使用new关键字创建了一个线程。
- 就绪:调用start()方法之后,线程处于就绪状态,不一定立即运行,要等待资源调度运行
- 运行:就绪状态的线程获得CPU资源后,执行run方法
- 阻塞:当前线程失去所占的资源,暂停运行,进入阻塞状态
- 死亡:线程执行完毕或被其他线程杀死,线程死亡。
2、状态之间的转换以及一些方法
(1)Thread类中的方法:
- sleep();:当线程调用sleep(time),线程从运行状态进入阻塞(休眠)状态,释放所占资源,使其他线程有执行机会;但是如果有锁的话,sleep方法并不会释放锁。
- yield();:使当前线程从运行状态转换为就绪状态,只能使同优先级线程或更高优先级线程有执行机会,所以线程yield之后可能又马上被执行;yield方法也不会释放锁资源。
- join();:先执行完调用该方法的线程,再执行其他线程。
(2)Object类中的方法:
- wait()、notify()和notifyAll(): 必须在synchronizeed代码块或synchronized方法中使用。
首先了解两个概念:锁池和等待池
(1)锁池:假设线程A已经拥有了某个对象的锁,而其他线程想要获得该对象的锁,但是该线程的锁现在正被A所拥有,所以这些线程就进入了该对象的锁池中。等线程A释放了该对象的锁,在锁池中的线程才能去竞争该对象的锁。
(2)等待池:假设线程A调用了某个对象的wait方法,则线程A就会释放该对象的锁,然后进入该对象的等待池中,并且不能竞争锁。
- wait(): wait()方法使当前线程进入阻塞状态并且释放锁,当前线程进入该对象的等待池,等待池中的线程不会去竞争锁资源。
- notify(): 线程调用对象的notify方法时,会随机唤醒一个wait的线程(即随机唤醒一个在等待池中的线程),被唤醒的线程便会进入该对象的锁池中,可以参与该对象的锁资源的竞争。
- notifyAll(): 与notify不同的时,该方法会唤醒对象的等待池中的所有对象,即等待池中所有的对象都将进入锁池中,参与锁资源的竞争。
注意:优先级别高的线程竞争到锁资源的概率大,没有竞争到锁资源的线程还会在锁池中。
(3)wait()与sleep()的比较
- wait方法是Object类中的方法,由final修饰,可以被所有类继承,但不能被重写;
而sleep是Thread类中的方法,由native修饰的本地方法。 - wait方法导致线程暂停,使用notify方法唤醒,会释放锁资源;
而sleep方法导致线程睡眠一段时间,自动醒来,不会释放锁资源。 - 使用wait时,当前线程一定要先获得该对象的锁,即wait方法的调用必须在synchronized方法或代码块中使用。
4、线程死锁问题:
什么是死锁: 指两个或两个以上的进程在执行时,因为争抢资源而造成的互相等待的现象,若无外力作用,它们都将无法推进下去。
死锁产生的条件:
- 互斥条件:一个资源每次只能被一个进程所使用
- 请求与保持条件:一个进程因请求资源而被阻塞时,对已获得的资源保持不放
- 不可抢占条件:进程已获得资源,没有被使用完之前,不能强行抢夺
- 循环等待条件:若干线程形成一种头尾相接的循环等待资源关系
避免死锁最好的办法就是破坏循环等待条件,将系统中所有的资源设置标志位,排序,规定所有的进程申请资源时必须按照一定的顺序。
5、synchronized和ReentrantLock
首先,了解锁的类型:
1、可重入锁:在执行对象中所有同步方法时不用再次获得锁
2、可中断锁:在等待获取锁的过程中可中断
3、公平锁:按等到获取锁的线程的等待时间进行获取,等待时间长的优先获取锁
4、读写锁:读的时候可以多线程一起读,写的时候只能同步的写。
还有一种说法:悲观锁与乐观锁
1、悲观锁:每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。
2、乐观锁:每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。
写操作频繁时用乐观锁,读频繁频繁时用悲观锁
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 悲观锁 | 可重入 可判断 可公平(两者皆可)乐观锁 |
性能 | 少量同步 | 大量同步 |
6、关于线程池
首先了解一下为什么需要用到线程池。
创建线程需要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程有限。为了避免这些问题,在程序启动的时候,我们就创建若干个线程来响应处理,它们被称为线程池,里面的线程称为工作线程。
假设一个服务器完成任务需要时间为:T1创建线程,T2线程中执行任务,T3销毁线程。
若,T1+T3远大于T2 ,则可采用线程池,以提高服务器性能。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或一些空闲的时间段。
线程池中最核心的类:java.util.concurrent.ThreadPoolExecutor
线程池中几个重要的参数:
-
corePoolSize:核心线程数
默认情况下,创建了线程池之后,线程池中的线程数为0,当有任务来的时候,就创建线程去执行它,当线程池中的线程数目达到核心线程数,则将到达的任务放入阻塞队列中。如果调用了prestartAllCoreThreads()或者prestartCoreThread()方法,会预创建所有核心线程数的线程或一个核心线程 -
maximumPoolSize:最大线程数
线程池中最大线程数,表示线程池中最多能创建多少线程;当线程池中核心线程+新建线程数=最大线程数时,这时再传进来任务,线程池就会拒绝新任务。 -
keepAliveTime:非核心线程的空闲线程的存活时间
默认情况下,当线程池中的线程数大于核心线程时,才会起作用;表示空闲线程能够存活的时间,直到线程数不超过核心线程数。如果调用allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0; -
unit:时间单位
是keepAliveTime 的时间单位 -
workQueue:阻塞队列,用来存储等待执行的任务的
一个阻塞队列,用来存储等待执行的任务的,一般使用LinkedBlockingQueue
public class ThreadPool{
public static void main(String[] args) {
/*线程池的几个重要的参数:
1、corePoolSize:核心线程数
2、maximumPoolSize:最大线程数
3、keepAliveTime:非核心线程的空闲线程存活时间
4、unit:时间单位
5、workQueue:阻塞队列,用来存储等待执行的任务
*
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3));
//执行第一个任务,使用已存在的核心线程来执行
pool.execute(new MyThread());
//又执行2、3、4个任务,这几个任务进入时,要被放在阻塞队列中,等待执行(最终还是被核心线程执行)
pool.execute(new MyThread());
pool.execute(new MyThread());
pool.execute(new MyThread());
//执行第5个任务,此时创建新线程来执行任务(注意:这时创建的新线程2和核心线程1会分摊执行任务,不一定核心线程1 会执行前4个任务)
pool.execute(new MyThread());
//上面传入5个任务,导致核心线程数+新创建线程数=2(最大线程数),所以此时要是再传入任务,线程池会拒绝新任务
// pool.execute(new MyThread());
pool.shutdown();
}
}
@SuppressWarnings("serial")
class MyThread implements Runnable,Serializable{
@Override
public void run() {
System.out.println("当前执行此任务的线程:"+Thread.currentThread().getName());
}
}
7、CAS
CAS是什么?
CAS是英文单词CompareAndSwap的缩写;是一种实现并发算法时常用到的技术,CAS有三个操作数:
当前内存值V,期望值A,新值B
CAS指令执行时,当且仅当要更新的值V与期望值A相等时,才能将内存值V修改为新值B,否则什么都不做。整个CAS操作就是一个原子操作。
CAS的缺点:
1、循环时间长,开销大:
若CAS失败,会一直尝试,长时间不成功,会浪费资源和时间。
2、只能保证一个共享变量的原子操作
当有多个共享原子变量需要保证原子性时,需要用锁。
3、ABA问题(重):
若读到的内存值V为A,最后准备赋值时读取还是A,不一定就是没有被其他线程更改过,可能这段时间内,它的值被改为B,又被改回A,CAS操作就会误以为它从来没有改变过,这就是ABA问题。