如何减少上下文切换
我们知道线程在做上下文切换的时候特别浪费资源,在一般情况下频繁的上下文切换不仅不能提高我们程序执行的效率反而会浪费时间因此我们在做多线程开发的时候应尽量避免线程频繁的上限文切换
1.无锁化编程
2.CAS技术
3.协程
如何避免锁死
锁死在程序中是非常致命的问题,只要已发生锁死就会造成程序无法执行下去:
一下四种情况同时发生就会造成我们的程序发生死锁,由此我们可以破坏其中一个条件,来破坏死锁
1.互斥条件
2.循环等待
3.请求保持
4.不可剥夺
避免死锁
1.避免一个线程同时获得多把锁
2.避免一个线程在锁内同时占有多个资源,尽量保证每个锁只占用一个资源
3.尝试使用定时多tryLock(time)
volatile关键字
作用 1.可见性
2.原子性
使用volatile关键字修饰的共享变量在多个执行期间JVM给CPU发送带Lock前缀的指令,带Lock前缀的质量有两个作用:
- 会引起处理的最新缓存数据写会内存
- 这个写回内存的操作会使其他CPU缓存了该内存地址的
数据无效
Synchronized原理
synchronized是基于monitor对象来实现,当时用synchronized同步代码块的时候Java代码在编译过程中会插入monitorenter和monitorexit,每个类都和monitor类有关联
synchronized锁的状态和升级
锁的状态会在代码执行过程中动态变化但是只能升级不能降级,这些状态存储在对象头里面
无锁状态-->偏向锁状态-->轻量级状态-->重量级状态
偏向锁
当一个线程在获得锁之后,会检查偏向锁是否指向自己;
若指向自己,在拿锁和释放锁的过程不在需要进行cas操作,只需进行简单的检查即可;
若又有其他的线程来竞争资源则会标记当前线程不在适合偏向锁而解除
对象锁
在Java中万物皆对象所以所得东西都可以当做是对象锁 一下是几种对象锁的例子
//第一种 是以自己的实例作为对象锁
private void objectLock() {
synchronized (this) {
//做其他事情
}
}
//第二种 等价于第一种 同样是以自己的实例作为对象锁
private synchronized void objectLock1() {
//做其他事情
}
// 第三种 实例化Java本身基类或者其他类作为对象锁
private String string = new String();
private Object obj = new Object();
///....
private void objectLock3() {
synchronized (string)//(obj)
{
//做其他事情
}
}
类锁
类锁之所以称之为类锁是因为在整个程序之中只存在为一个一份实例 例如 static synchronized
synchronized (XXX.class)
//第一种 lockDem的class在程序中也只会存在一份
private void classLock()
{
synchronized (LockDemo.class)
{
//做其他事情
}
}
//第二种 带static 修饰的方法 在类实例化之后整个程序只会有唯一的一份所以也称之为类锁
private static synchronized void classClock()
{
//做其他事情
}
显示锁与隐式锁
隐式锁 synchronized
显示锁Lock
ReentrantLock(可重入锁)
ReentrantReadWriteLock(可重入的读写锁)
WriteLock(写锁)
ReadLock(读锁)
等等都是显示锁
显示锁中的Condition组件中的await()
signal()
等方法代替了Object中的wait()
和notify()
方法,当我我们在调用线程的阻塞方法是,当前前线程会释放掉自己所持有的的锁
,同时把自己计入阻塞队列中,但是当我们调用sleep()
方法当前线程不会释放自己所持有的锁.
重入锁
Synchronize
ReentrantLock
ReentrantReadWriteLock
等皆为可重入锁,但是怎么证明我的锁为可重入锁呢,当我们使用递归调用的时候,当前线程不会因为自己调用自己而锁死,证明次锁为可重入锁
公平锁与非公平锁
在线程队列中什么是公平,什么不公平,这也和我们去排队买东西一样,先到的站在最前面,后到的在后面排队,这就是公平,若是有人在中间插队则为不公平,线程亦是如此.
若是由于某种机制导致我们的线程优先级比较高,则可以在队头插入,优先级低的则在队尾,完全没有按照先进先出的原则,则称之为非公平锁
Synchronize
天生就是非公平锁,当我们在定义Lock的时候在构造函数中出入true则为公平锁,传入false或默认值则为非公平锁.
CAS (CompareAndSwap)
现代CPU提供的对比和交换指令
原子指令
现在我们有四个线程共同计算count的值之后同时只能有一个线程可以进入对比交换器中,当1号线程进入之后并且修改值之后2号线程进入,但是2号开成发现自己所期望的旧值已经不是零了而是1与自己的期望不相符,3号和四号同样如次,他们重新计算count值然后对比交换知道所有的线程都执行完
如何避免ABA问题:
给我们需要修改的变量加入版本戳,修改一个版本戳加一;
效率问题:
当有非常多非常多线程时会造成大量现在自旋占用CPU资源,我可以换成锁的方式
同时只能修改一个共享变量问题:
把多个变量封装到AtomicReference中进行修改
AQS
AQS是利用CLH思想进行实现的只不过AQL利用的是双端就绪列队,当我们新创建一个线程使用`wait()`方法阻塞线程时,此线程加入就绪线程并且进行若干次自旋拿锁操作,若拿不到则进入阻塞队列,放我们调用`notify()/notifyAll()`时,唤醒我们的阻塞队列,再把线程放入就绪队列中.
在实现独占锁和可重用锁的时候,我们集成Lock抽象方法,然后再内部类中我们集成AbstractSynchronizeQueen 我们主要是修改AQS中的state字段,重写tryAcquire()
(tryAcquireShare主要用于可重入锁) tryRelease()
方法
线程的内存模型(JMM) 和 volatile 关键字
JVM中为我们的线程分为主内存和工作内存,主内存为共享内存,工作内存线程之间相互隔离互不干扰.有因为线程每次都是把共享数据读取到自己的工作内存中进行操作,在不加锁的情况下这就造成了数据混乱,造成线程之间不安全.
Volatile
关键字的作用是增加了共享变量的可见性
和原子性
;
可见性
:强制线程每次都从共享变量中读取
最新的数据,同时把工作内存的缓存的最新数据写回主内存
原子性
:volatile关键字有限制了CPU对指令进行重排序.
线程的实现方式
线程只有两种实现方式一种是Thread 另一种是Runnable,因为 Callable是交给Future执行而future是继承自Runable 所以只有两种方式实现线程.
我们为什么使用线程池
首先我们使用new Thread()
方法进行创建线程主要时间花销包括创建时间
,执行时间
,线程切换
,销毁时间
,当我们整个程序有大量的进程需要我们处理,没有个新的线程都需要这些重复的时间花销,但是我们为什么不在系统一起动就实现准备若干个线程,从而创建和销毁的时间就省去了. 由此我们引入我们的线程池
的优势
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
corePoolSize
整个线程池核心线程数 也可以理解为整个线程最小保持线程的个数,这个数值最好保持在与CPU
核心数保持一直可以通过 Runtime.getRuntime().availableProcessors()
方法进行获得
maximumPoolSize
线程池中最大线程数
当我们使用excetore(Runable)
提交新任务的时候,即使已有若干空线程,单线程数小于corePoollSize
也会创建新的线程来执行提交的任务.
当提交新任务线程数已经大于corePoolSize
小于maximumPoolSize
但阻塞队列未满不会创建新的线程,而是把线程放入阻塞队列,仅当队列已满才会创建新的线程
通过将corePoollSize
和maximumPoolSize
设置为相等会创建一个固定大小的线程池,后期可以通过使用setCorePoolSize
和setMaximumPoolSize
动态更改。
keepAliveTime
当空闲的线程数大于corePoolSize
且时间超过keepAliveTime
时会进行销毁多余的空闲线程
unit
keepAliveTime
的时间单位
workQueue
阻塞队列,当阻塞队列不为空的时候可以使用方法prestartCoreThread
或prestartAllCoreThreads
预先启动队列中的线程。
线程排队策略:
直接交接(SynchronousQueue)
阻塞队列中只有一个容量,相当于任务直接交付给工作线程,当一直提交任务时因为队列已满就会一直创建新的线程,当线程数等于maximumPoolSize
时线程池会启动拒绝策略
无界队列(LinkBlockingQueue,没有容量得链表阻塞队列)
,当提交任务数大于corePoolSize
但是由于是无界队列,队列长度会一直增加下去也会不创建新的线程,
有界队列(ArrayBlockingQueue)
可以防止一直创建线程做成内存泄漏,当线程数等于maximumPoolSize
时线程池会启动拒绝策略
threadFactory
通过提供不同的 ThreadFactory,您可以更改线程的名称、线程组、优先级、守护程序状态等。
例如
class MyThreadFactory extends AtomicLong implements ThreadFactory
{
@Override
public Thread newThread(Runnable r)
{
Thread t = new Thread(r, "我是线程的妈妈 "+incrementAndGet());
t.setDaemon(true);//设置成守护线程
return t;
}
}
什么是守护线程
线程分为用户线程
,守护线程
,守护线程会一直守着用户线程,当用户线程全部结束用户线程也会自动结束,因为没有需要守护的先程了,守护线程也就没必要存在了.
handler
拒绝策略:
AbortPolicy()直接抛出异常):当提交新任务时队列已满也没有工作线程能够接纳时直接抛出异常
CallerRunsPolicy(谁提交的谁运行):当提交新任务时队列已满也没有工作线程能够接纳时谁提交的谁运行新任务
DiscardPolicy(直接抛弃新的任务):当提交新任务时队列已满也没有工作线程能够接纳时直接抛弃新提交的任务
DiscardOldestPolicy(直接抛弃旧任务):当提交新任务时队列已满也没有工作线程能够接纳时直接抛弃队列中最早提交的任务(舍弃队头任务)
使用方法
ArrayBlockingQueue<Runnable> blockQueue = new ArrayBlockingQueue<Runnable>(10);
blockQueue.add(new WorkThread(1 + ""));
blockQueue.add(new WorkThread(2 + ""));
blockQueue.add(new WorkThread(3 + ""));
int coreSize = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(coreSize, coreSize + 10, 2, TimeUnit.MINUTES, blockQueue,new MyThreadFactory() );
threadPool.prestartAllCoreThreads();
}
class MyThreadFactory extends AtomicLong implements ThreadFactory
{
@Override
public Thread newThread(Runnable r)
{
Thread t = new Thread(r, "我是线程的妈妈 "+incrementAndGet());
t.setDaemon(true);
return t;
}
}