目录
一、线程创建的两种实现方式
(1)继承Thread类
public class ThreadTest001 {
public static void main(String[] args) {
Thread t1 = new MyThread01();
// 启动线程
t1.start();
}
}
class MyThread01 extends Thread{
// 重写run()方法
@Override
public void run() {
System.out.println("this is a thread");
}
}
(2)实现Runnable接口
public class ThreadTest001 {
public static void main(String[] args) {
// 创建Runnable类型的对象
Runnable r1 = new MyRunnable001();
// 将r1传入Thread中
Thread t2 = new Thread(r1);
t2.start();
}
}
class MyRunnable001 implements Runnable{
// 重写run()方法
@Override
public void run() {
System.out.println("this is a runnable interface");
}
}
与继承Thread类相比实现Runnable接口的好处:
1. Java支持单继承,如果继承了Thread类就不能再继承其他类;但Java可以实现多个接口,因此实现Runnable接口可以避免单继承的局限性
2. 适合多个相同代码的线程去处理同一个资源的情况
3. 增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
(3)实现Callable接口
public class ThreadTest001 {
public static void main(String[] args) {
Callable c1 = new MyCallable001();
FutureTask<Integer> task = new FutureTask<Integer>(c1);
Thread t3 = new Thread(task);
t3.start();
}
}
class MyCallable001 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 1;
}
}
Runnable接口和Callable接口的区别:
1. Callable接口有返回值,task.get()即可获取返回值
2. call()可以抛出异常,但run()不可以
3. Callable接口的重写方法是call(),而Runnable接口的重写方法是run()
(4)使用线程池
举个栗子:
ExecutorService threadPool = Executors.newFixedThreadPool(5);
使用线程池的好处:
线程池能够对线程进行统一分配、调优和监控
1. 降低资源消耗
2. 提高响应速度
3. 提高线程的可管理性
二、synchronized关键字
(1)原理:synchronized基于JVM保证线程同步,可以把任意非null的对象作为锁
在作用于方法时,锁住的对象是实例this
在作用于静态方法时,锁住的对象是类实例
在作用于一个对象实例时,锁住的对象是对应的代码块
(2)自旋锁
为什么需要自旋锁:
由于在多处理器的环境中某些资源的有限性,有时需要互斥访问,这时候就需要引入锁的概念,只有获取到锁的线程才能对临界资源进行访问,由于多线程的核心是CPU的时分片,所以同一时刻只能又一个线程获取到锁。但是那些没有获取到锁的线程该怎么办呢?
通常有两种做法:一种是没有获取到锁的线程就一直等待判断该资源是否已经释放了锁,这种锁叫做自旋锁,它不会引起线程阻塞。还有一种是,没有获取到锁的线程把自己阻塞起来,重新等待CPU的调度,这种锁称为互斥锁
如何实现自旋锁:
等待一段时间,执行一段时间的空方法不让出CPU,如果在一定时间内还没有获取到锁就进入阻塞队列。
自旋锁的问题:
1. 过多占据CPU的时间
2. 死锁
JVM自旋周期的选择:
1. 如果平均负载小于CPUs,则一直自旋
2. 如果超过CPUs/2个线程正在自旋,则后来的线程全部阻塞
3. 如果CPU处于节电模式,停止自旋
4. 自旋时间的最坏情况时CPU的存储延迟
5. 自旋时会适当放弃线程优先级之间的差异
三、Lock
(1)原理: Lock与底层的JVM无关。在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock(重入锁)、ReadWriteLock(实现类ReentrantReadWriteLock)
(2)CAS
CAS是基于CPU提供的原子操作指令实现的,是乐观锁的实现机制
CAS包含三个参数:内存位置v,预期值A,新值B
当一个线程想要去更改一个共享资源的数据时,首先会看他的预期值是否等于A,如果等于则将A改为B,如果预期值不等于A,则保持原有值不变。
示例如下:
public class CASTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2021) +"\t" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2022) + "\t" +atomicInteger.get());
}
}
// true 2021 第一次改变
// false 2021 第二次不变
优点:当多个线程操作时,它解决了悲观锁使用的独占锁一次只能有一个线程进入临界区的问题,提高并发性能。如果有很多个线程同时进入循环,那么每个线程都在占用资源执行,但是每次只有一个线程能够更新成功。
缺点:
1. ABA问题
什么是ABA问题:如果原来是值是A,中途变成了B,然后又变成了A,那么使用CAS进行检查时会发现它的值没有变化,但实际已经变了。
解决方案:在变量前,加一个版本号
2. 循环时间长开销大
解决方案:让JVM支持处理器提供的pause指令
pause指令有两个作用:
1. 可以延迟流水线执行指令,使CPU不会消耗过多执行资源
2. 可以避免在退出循环时因内存顺序冲突而引起CPU流水线被清空,从而提高CPU执行效率
3. 只能保证一个共享变量的原子操作
四、synchronized和Lock的区别
(1)synchronized时关键字,Lock时接口
(2)synchronized发生异常时会自动释放线程占有的锁,但Lock必须手动释放,否则就可能产生死锁
(3)Lock可以让等待的线程响应中断,synchronized不行
(4)Lock能够通过tryLock()方法知道是否成功获取锁
(5)Lock可以提高多个线程进行读写的效率,因为它有读锁和写锁两种
五、线程池
(1)ThreadPoolExecutor类
线程池最好使用ThreadPoolExecutor类进行创建。
原因:
1. 可以实时获取线程池内线程的各种状态
2. 可以动态调整线程池大小
ThreadPoolExecutor类有几个重要参数,如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:线程池中的常驻核心线程数
maximumPoolSize:线程池的最大线程数
keepAliveTime:多余的空闲线程的存活时间
TimeUnit:keepAliveTime的单位
BlockingQueue:阻塞队列
ThreadFactory:线程工厂(创建线程的)
Handler:拒绝处理任务时的策略
这几个参数的具体意义可以通过一个小例子来进行理解:
假设线程池是一个银行,线程就是银行里能够办理业务的窗口,假设这个银行有5个窗口,这五个窗口就是maximumPoolSize;
今天星期五,银行开设了3个窗口进行办理业务,这三个窗口就是corePoolSize;
当进来第4个人的时候,他就要进入等待区,直到前三个人中有一个人办理完业务,才能去办理业务,这个等待区就是BlockingQueue;
由于今天是7月2号,月初大家都想取钱,所以银行的人非常多,很快等待区就坐满了人。为了提高效率行长决定再临时抽调2个人,将银行剩余的两个窗口也开启,这时阻塞队列中的人就可以到这两个窗口办理业务了;
当5个窗口全部被占满,且等待区也全部占满后,银行此时已经达到饱和状态,这时就会有个保安大叔告诉你:“孩子,明天再来吧,今天人太多了”,这个大叔就是Handler;
终于,工作到下午了,来银行的人基本都办理完业务了,这个时候窗口就闲下来了。过了一段时间后,行长就跟临时抽调的人说:“你们回去吧,这里没有那么忙了,回去好好放个假”。这个过了一段时间就是keepAliveTime。
大概就是长这样,我只画了一个静态图……
(2)newFixedThreadPool
public class FixThreadTest {
public static void main(String[] args) {
ExecutorService e1 = Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
e1.execute(new MyRunnable());
}
e1.shutdown();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t this is a task!");
}
}
该线程池多用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
该线程池的工作流程:
1. 线程数小于核心线程数时,新建线程执行任务
2. 线程数等于核心线程数时,后面的任务会进入阻塞队列
3. 阻塞队列的容量非常大,可以一直加,默认的容量时Integer.MAX_VALUE
4. 执行完任务的线程将反复去队列中去任务执行
newFixedThreadPool的源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到newFixedThreadPool类的最大线程数就是核心线程数,他的存活时间数0,也就是会立即停掉多余线程。
(3)newSingleThreadExecutor
ExecutorService e2 = Executors.newSingleThreadExecutor();
该线程池只创建唯一线程,用这一个线程去执行阻塞队列中的全部任务。,常用于串行执行任务的场景,每个任务必须按照顺序执行。
该线程出的工作流程:
1. 线程池中没有线程时,创建一个线程执行任务
2. 有了一个线程后,如果线程正在工作,将后续任务加入阻塞队列中
3. 用这一个线程不断的取阻塞队列中的任务执行
newSingleThreadExecutor的源码如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看到该线程池的核心线程数和最大线程数都是1,存活时间为0,即立即失效
(3)newCachedThreadPool
ExecutorService e3 = Executors.newCachedThreadPool();
当提交任务的速度大于处理任务的速度时,每提交一个任务,就会创建一个线程。极端情况下会创建过多线程,耗尽CPU和内存资源。
该线程的工作流程:
1. 没有核心线程,直接向阻塞队列中提交任务
2. 如果有空闲线程,就去出任务执行,如果没有就新建一个线程
3. 执行完任务后的线程有60s的存活时间
newCachedThreadPool的源码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看出该线程池没有核心线程数,最大线程数为Integer.MAX_VALUE,相当于没有上限。