1.实现线程的几种方式
- 继承Thread类
public class SubClassTest {
public static void main(String[] args) {
new Thread(new MyThread2()).start();
}
}
class MyThread2 extends Thread{
@Override
public void run() {
System.out.println("Here is in subClass!");
}
}
//输出
Here is in subClass!
- 继承Runnable接口
public class RunnableTest {
public static void main(String[] args) {
new Thread(new MyThread3()).start();
}
}
class MyThread3 implements Runnable{
@Override
public void run() {
System.out.println("Here is Runnable!");
}
}
//输出
Here is Runnable!
- 继承Callable接口,要配合FutureTask接受结果
这里通过开启一个子线程求平方
public class CallableTest{
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread(5);
FutureTask<Integer> f = new FutureTask<>(myThread);
new Thread(f).start();
System.out.println(f.get());
Thread t = new Thread();
t.start();
}
}
class MyThread implements Callable<Integer> {
private int x;
public MyThread(int x){
this.x = x;
}
@Override
public Integer call() throws Exception {
System.out.println("Here is call !");
x = x*x;
return x;
}
}
//输出
Here is call !
25
4.2种类线程池
- newCachedThreadPool
可以添加任意多的线程;超时1分钟后可以回收;一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- newFixedThreadPool
典型且优秀的线程池;但线程池没有任务时不会被销毁
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- newSingleThreadExecutor
相比于手动开启一个线程,它多了任务提交失败的机制。如果worker异常,会有另外一个worker代替。保证所有的任务按照指定顺序执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- newScheduleThreadPool
可以再延时一段时间后执行任务,或定期执行任务。
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
3.Synchronized原理以及优化
1.重量级锁
每个对象锁对应一个monitor对象,该monitor对象是os级别的,维护一个owner,一个EntryList,一个waitset。
初始时owner为null,当线程A进入同步代码快,则成为owner,markword的后两位改成10,前面的位改成指向monitor的指针。如果其他线程也进入,则会进入EntryList等待
2.轻量级锁
应用场景:
一个对象锁中多个线程的访问是错开的,这样就没必要申请os级别的重量级锁。轻量级锁的使用是透明的,也就是默认使用。
方法:
当一个线程进入同步代码块,栈帧中生成Lock Record对象,通过cas将对象的mark word和Lock Record中的字段交换,Lock Record对象还存放了指向对象的指针,交换成功则加了轻量级锁,标志位00
cas失败:
原因一:发生了重入,那么继续生成一个Lock Record对象,但该对象的Lock Record为null。原因二:其他线程拥有了该对象的轻量级锁,进入锁膨胀阶段。
3.锁膨胀
为对象申请重量级锁,并将对象头中的mark word 改成指向重量级锁的指针和10,申请的monitor对象的owner为原来的线程,当前线程进入EntryList等待。
4.偏向锁
使用原因:
轻量级锁每次在发生重入时仍需要通过cas交换mark word,而偏向锁只有在第一次进入同步代码块时通过cas将线程id设置到对象头,以后如果发现该对象mark word包含有当前线程id,就不需要判断。
特点:
- 默认延时开启
- 如果使用了偏向锁,在离开synchronized代码块之后后三位仍然是101
- 当线程1使用完对象锁A之后,线程2也使用了该锁,那么在synchronized代码块内部加的是轻量级锁。
- 批量重定向和批量撤销
当有三个线程t1,t2,t3,t1先执行,然后唤醒t2,t2唤醒t3,有40次循环创建40个对象
t1中,所有对象都是偏向t1的
加锁前:000...101
加锁中:threadID_t1+101
加锁后:threadID_t1+101
t2中,前20个对象(其他线程调用,取消偏向状态,阈值超过20重偏向)
加锁前:threadID_t1+101
加锁中:lockrecord+00
加锁后:000...001
后19个对象
加锁前:threadID_t1+101
加锁中:threadID_t2+101
加锁后:threadID_t2+101
t3中,前20个对象
加锁前:000...001
加锁中:lockrecord+00
加锁后:000...001
后19个对象
加锁前:threadID_t2+101
加锁中:lockrecord+00
加锁后:000...001
当new第41个对象时,默认为不可偏向
锁消除
自旋优化:
当需要加重量级锁时,他重新多次尝试能否获得锁,如果获得了就不需要加重量级锁。重试的次数和CPU有关,比如如果刚才重试成功了就会多试几次。
4.Synchronized和Lock
- Synchronized基于JVM来实现的,ReentrantLock基于JDK来实现的
- 由于对Synchronized做了优化,因此性能上差不多
- ReentrantLock可以被打断,即正在等待的线程取消了等待,对应于
lockInterruptibly()
- Synchronized只有非公平,而ReentrantLock默认非公平,也可以支持公平。
- wait/notify上,一个RenntrantLock对象有newCondition(),可以使用多个ConditionObject
5.CAS
简介:CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B,是乐观锁的实现方式。
应用:AtomicInteger的add方法就使用了unsafe类的compareAndSwapInt方法来实现
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
6.volatile
可见性:
Java内存模型中,每个线程有自己的工作内存,还有主内存。volatile类型的变量,如果发生改变,那么该线程会给总线发信号,其他线程都在监听总线,如果监听到该信号,会将自己的缓存中对应的volatile变量置为无效,需要重新从主内存中读取新值。
有序性:
加内存屏障,即特定的指令,保证排序的一些规则,有4种,对于volatile类型变量的读会在后面添加LoadStore和LoadLoad内存屏障,保证了后面对普通变量的读和写不会和该volatile变量的读重排序。
7.final
1.写final域重排序规则
在对象引用被其他线程可见之前,对象的final域已经初始化,实现方式是在写操作后加StoreStore屏障。
在下图中,非final域就没有被正确初始化,而final域正常
public class FinalExample {
int i; //普通变量
final int j; //final变量
static FinalExample obj;
public FinalExample () { //构造函数
i = 1; //写普通域
j = 2; //写final域
}
public static void writer () { //写线程A执行
obj = new FinalExample ();
}
public static void reader () { //读线程B执行
FinalExample object = obj; //读对象引用
int a = object.i; //读普通域
int b = object.j; //读final域
}
}
2.读final域的读规则
保证先读包含final域对象的引用,再读final域。而由于发生了重排序普通域可能会先被读,然后对象引用再被读。
8.happens-before
不是指代码的位置,而是指代码的执行顺序。
- 一个线程内部前面的和后面的
- 线程的start操作和start后的
- volatile读和volatile写
- join中的操作和后面的
- 传递性
- ........
9.单例
- 为什么用volatile
singleton = new Singleton()
分为三步走
- 分配内存
- 构造函数初始化
- 赋值给引用
如果没有volatile,后两步发生指令重排序,进入同步代码块的线程还没有初始化完成,另外一个线程判断不为null,则拿到的就是一个没有被正确初始化的对象。
- 为什么两次判断
开始后可能有两个线程同时执行,一个进入EntryList,另一个进入同步代码块
public class Singleton {
private Singleton(){}
private static volatile Singleton singleton;
public Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
}
10. AQS和ReentarantLock
ReentrantLock上锁流程:
- 先调用lock()
- 先cas将state由0改成1一些看能不能成功,不能就acquire
- acquire流程
public final void acquire(int arg) {
// ㈡ tryAcquire
if (!tryAcquire(arg) &&
// 当 tryAcquire 返回为 false 时, 先调用 addWaiter, 接着acquireQueued
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
- tryAcquire在ReentrantLock中有公平锁和非公平锁两种实现。
对于公平锁:
先获取state,如果是0,那么尝试通过cas将0改为1,否则判断是否重入,如果这两种情况满足,则返回true,否则返回false
对于非公平锁:
先获取state,如果是0,那么队列中没有其他线程等待且尝试通过cas将0改为1,否则判断是否重入,如果这两种情况满足,则返回true,否则返回false - addWaiter和enq逻辑
先将当前线程封装到Node里面
addWaiter是当tail != null时加入队列尾部。enq是开始时head = tail =null的设置 - acquiredQueue
获取尾部先加入节点的前驱节点,如果前驱是head则可以调用tryAcquire再尝试获取锁。(如果成功获取到了,设置当前节点为head并返回)
没有获取到就会走下一步逻辑 - shouldParkAfterFailedAcquire
这里判断前驱的waitStatus是否为-1,如果不是判断是否<=0,如果是则设置为-1。之后重新进入死循环再执行一遍 - parkAndCheckInterrupt
LockSupport.park阻塞当前线程
解锁流程: - 调用unlock(),再调用release
- 如下图
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
3.调用tryRelease判断state - 1后是否为0,如果是设置独占线程为null,并返回true,否则返回只将state减一,返回false。
4.如果h != null && h.waitStatus != 0 就unpark
5.unpark
由于再队列中的线程可能取消了等待,因此从队列最后向前找,如果某个Node的waitStatus = -1,那么调用LockSupport.unpark唤醒它。
11.countdownlatch semaphore cyclicbarrier
- CountDownLatch
应用场景:
一个线程等待多个线程执行结束
- 参数中传递的值就是state
- countDown方法中对应释放共享锁,即将state - 1,Node节点出队。
- 而await方法对应获取锁,如果state = 0,则执行结束,否则进行入队操作。
- Semaphore
允许多个线程同时访问。 - CyclicBarrier
- CountDownLatch 和CyclicBarrier的区别
- CyclicBarrier可以使用多次
- 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
12.await和object.wait的区别
Condition能够支持不响应中断,而通过使用Object方式不支持
Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个
Condition能够支持超时时间的设置,而Object不支持
针对Object的wait方法
void await() throws InterruptedException//当前线程进入等待状态,如果在等待状态中被中断会抛出被中断异常long awaitNanos(long nanosTimeout)//当前线程进入等待状态直到被通知,中断或者超时boolean await(long time, TimeUnit unit)throws InterruptedException//同第二种,支持自定义时间单位boolean awaitUntil(Date deadline) throws InterruptedException//当前线程进入等待状态直到被通知,中断或者到了某个时间
针对Object的notify/notifyAll方法
void signal()//唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。void signalAll()//与1的区别在于能够唤醒所有等待在condition上的线程