线程
什么是进程和线程
进程是程序运行资源分配的最小单位
a, 进程和进程 之间是相互独立的
b, 同一进程中的多条线程共享该进程中的全部系统资源
c, 进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等。
线程是 CPU 调度的最小单位,必须依赖于进程而存在
a, 线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈), 共享同一进程所拥有的全部资源。----多线程并发
b,线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。
CPU 时间片轮转机制
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
归结如下:时间片设得太短会导致过多的进程切换,降低了 CPU 效率:
而设得太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常是一个比较合理的折衷。
并行和并发
并行:指应用能够同时执行不同的任务,例:A线程下载图片,B线程下载歌曲
这两件事情可以同时执行。
并发:指应用能够交替执行不同的任务,比如两个线程同时卖100张火车票,A线程卖了一张,B线程卖了一张,.......,交替执行。(获得CPU时间片后才区执行)比如,1分钟A和B线程卖了30张,所以并发量是30张/每分钟.
两者区别: 并发一个是交替执行, 并行一个是同时执行
OS限制:一个进程,Linux 最多是1000个线程,Windows 最大是2000个。
打开一个线程会消耗1024字节。
新启线程的方式?-----官方文档:两种
a, 接口Runnable
b, 继承Thread类
举例:
/*一:扩展自Thread类*/
private static class UserThread extends Thread{
@Override
public void run() {
super.run();
// do my work;
System.out.println("I am extendec Thread");
}
}
/*二:实现Runnable接口*/
private static class WorkRunnable implements Runnable{
@Override
public void run() {
// do something
System.out.println("I am implements Runnable");
}
}
其他地方启动线程:
UserThread userThread = new UserThread();
userThread.start(); // 启动线程,等待CPU调度
userThread.start(); // 再调用一次start方法时会抛出异常
WorkRunnable workRunnable = new WorkRunnable();
new Thread(workRunnable ).start(); // 因为Thread的构造方法接收一个Runnable对象,所以这里可以传workRunnable,因为workRunnable已经继承了Runnable
有开启就有停止,怎么样才能让Java里的线程安全停止工作呢?
停止线程的方式:
stop():Thread类的方法,目前已经废弃,不建议使用。暴力停止线程。导致线程占用的资源不会被释放
interrupt(): 是发起对线程的中断,设置中断标志位
isInterrupted():判定当前线程 是否被中断
interrupted():判定当前线程 是否被中断,并把中断标志位置为false
暂停、恢复和停止操作对应在线程 Thread 的 API 是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主 要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方 法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
高并发编程的意义、好处和注意事项
并发编程的好处:
(1)充分利用 CPU 的资源
(2)加快响应用户的时间
(3)可以使你的代码模块化,异步化,简单化
多线程需要注意的事项:
(1)线程之间的安全性
(2)线程之间的死锁
(3)线程太多了会将服务器资源耗尽形成死机当机
InterruptedException 中断异常对中断标志位的影响:
输出打印如下:
从上面可以得出:
1, userThread.interrupt();// 中断线程,设置线程的标识位为true
2, catch里通过捕获的InterruptedException,会将标志为置为false
3, 如果想中断打印/线程,在catch里调用interrupt方法
Catch 里执行intterupt方法后的输出结果如下:
线程的运行状态和相关方法:
新建:创建一个线程的实例,即new Thread()
Start()方法:线程启动,等待CPU调度,
yield()方法:使当前线程让出 CPU 占有权,但让出的时间是不可设定的。
不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行 yield( )的线 程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。
所有执行 yield()的线程有可能在进入到就绪状态后,再次会被操作系统调度
马上又被执行。
即执行yield() 方法,不释放锁,进入就绪状态,会再次被CPU调度,马上又被执行。
Join()方法:把指定的线程加入到当前线程中,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续 执行线程 B。
synchronized 内置锁
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字
synchronized 主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量 访问的可见性和排他性,又称为内置锁机制。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,
类锁是用于类的静态方法或者一个类的 class 对象上的。
我们知道,类的对象实例可以有很多个,但 是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存 在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁之间也是互不干扰的。
锁的对象不唯一:(这里主要学习:i++具体做了啥,Integer.valueOf()干了啥)
即反编译看到i++执行了Integer.valueOf()方法,查看Integer.valueOf()的源码,看到的是新建一个Integer对象
锁的对象唯一:
volatile,最轻量的同步机制
volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某
个变量的值,这新值对其他线程来说是立即可见的
举例:
加上volatile关键字
volatile 不能保证数据在多个线程下同时写时的线程安全
volatile 最适用的场景:一个线程写,多个线程读。
ThreadLocal 辨析
ThreadLocal 和 Synchonized 的区别?
ThreadLocal 和 Synchonized 都用于解决多线程并发訪问。可是 ThreadLocal
与 synchronized 有本质的差别。
a, synchronized 是利用锁的机制,使变量或代码块 在某一时该仅仅能被一个线程訪问。
b, ThreadLocal 为每个线程都提供了变量的 副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
总结起来就是一句化:每个线程拥有自己的ThreaLocalMap成员变量, ThreadLocalMap里通过entry数组保存自己线程的数据。
ThreadLocal 的理解:
每个Thread线程有自己的ThreadLocalMap 成员变量,通过threadLocalMap来set和get,保证了每个线程都是访问和获取自己线程的数据,实现了线程隔离。
上代码说明
thraedLocalMap保存的是,key=ThreadLocal, value=线程:0;(value=线程:1,value=线程:2)
=====================
说明:
1, ThreadLocal的hashcode 位与 数组长度值减1,得到threadLocal在数组中的下标值
2, 根据下标值i 取出对应的值entry
3, 如果entry不为空,并且entry.get() == 传进来的threadLocal, 则返回取到的entry
ThreadLocal 的第二种辨析
上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap,
ThreadLocalMap 是 ThreadLocal 的静态内部类,然后 Thread 类中有一个这样类型成员,所以 getMap 是直接返回 Thread 的成员。
看下 ThreadLocal 的内部类 ThreadLocalMap 源码:
可以看到有个 Entry 内部静态类,它继承了 WeakReference,总之它记录了
两个信息,一个是 ThreadLocal<?>类型,一个是 Object 类型的值。getEntry 方法 则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal 对应的值。
回顾我们的 get 方法,其实就是拿到每个线程独有的 ThreadLocalMap
ThreadLocal引发的内存泄漏分析
引用
Object o = new Object();这个 o,我们可以称之为对象的引用,
而 new Object()我们可以称之为在内存 中产生了一个对象实例。
当写下 o=null 时,只是表示 o 不再指向堆中 object 的对象实例,不代表这
个对象实例不存在了。
四种引用
强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,
在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行
第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK
1.2 之后,提供了 SoftReference 类来实现软引用。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱
引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之 后,提供了 WeakReference 类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象
实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用
来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象
实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了
PhantomReference 类来实现虚引用。
内存泄漏的具体过程:
首先只简单的在每个任务中 new 出一个数组
可以看到内存的实际使用控制在 25M 左右:因为每个任务中会不断 new 出
一个 5M 的数组,5*5=25M,这是很合理的。如下:
启用了 ThreadLocal 以后:
内存占用最高升至 150M,一般情况下稳定在 90M 左右,那么加入一个
ThreadLocal 后,内存的占用真的会这么多?
于是,我们加入一行代码:
再执行,看看内存情况:
可以看见最高峰的内存占用也在 25M 左右,完全和我们不加 ThreadLocal 表现一样。这就充分说明,确实发生了内存泄漏。
分析
根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个
ThreadLocalMap,这个映射表的 key是 ThreadLocal 实例本身,value 是真正需
要存储的 Object,也就是说 ThreadLocal本身并不存储值,它只是作为一个 key
来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map
是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
因此使用了 ThreadLocal 后,引用链如下:
这样,当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal
实例,所以 threadlocal 将会被 gc 回收。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永 远不会被访问到了,所以存在着内存泄露。
只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开,
Current Thread、Map value 将全部被 GC 回收。最好的做法是在不需要使用
ThreadLocal 变量地方,都调用它的 remove()方法,清除数据。
其实考察 ThreadLocal 的实现,我们可以看见,无论是 get()、set()在某些时
候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法。
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得
思考:为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
key 使用强引用:引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap
还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 的对象实例不会 被回收,导致 Entry 内存泄漏。
key使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap
持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的对象实例也会被 回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都有机会被回收。比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可 以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟
Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。
总结
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
JVM 利用调用 remove、get、set 方法的时候,回收弱引用。
当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的
重复运行的,从而也就造成了 value 可能造成累积的情况
ThreadLocal回顾:
总结起来就是一句化:每个线程拥有自己的ThreaLocalMap成员变量, ThreadLocalMap里通过entry数组保存自己线程的数据。
先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMap,ThreadLocalMap是ThreadLocal的静态内部类,然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员。
从ThreadLocal内部类ThreadLocalMap的源码,可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。
回顾我们的get方法,其实就是拿到每个线程独有的ThreadLocalMap
然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行map的创建,初始化等工作。
线程间的协作
线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,
而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,
而最终执行又是另一个线程。
等待/通知机制
是指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B
调用了对象 O 的 notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait() 方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象 上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通 知方之间的交互工作。
举例说明:
线程1、线程2、线程3都调用了 wait方法,锁对象为Express
当如下方法得到执行时,会发出通知
这里时notify(),有可能是线程0、线程1、线程2、线程3、线程4、线程5其中的一个线程得到通知。从打印的日志看是线程0得到了通知,进而执行后续的操作。
notifyAll
notify():
通知一个在对象上等待的线程,使其从 wait 方法返回,而返回的前提是该线程
获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。
notifyAll():
通知所有等待在该对象上的线程
wait()
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断
才会返回.需要注意,调用wait()方法后,会释放对象的锁
wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n 毫秒,如果没有
通知就超时返回
wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒
等待和通知的标准范式
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级
别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法。
进 入 wait()方法后,当前线程释放锁,
下面这段没看明白????TODO
在从 wait()返回前,线程与其他线程竞争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized 代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会 继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的 线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新
去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。
调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才
会自然而然的释放锁,所以 notify()系列方法一般都是 syn
CountDownLatch
概念:
闭锁,CountDownLatch 这个类能够使一个线程等待其他线程完成各自的工
作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动
所有的框架服务之后再执行。
作用:
使一个线程等待其他线程都完成了任务后,这个线程才去执行自己的任务。
具体实现:
CountDownLatch 是通过一个计数器来实现的,计数器的初始值为初始任务
的数量。每当完成了一个任务后,计数器的值就会减 1
(CountDownLatch.countDown()方法)。当计数器值到达 0 时,它表示所有的已
经完成了任务,然后在闭锁上等待 CountDownLatch.await()方法的线程就可以恢
复执行任务。
举例:
static CountDownLatch latch = new CountDownLatch(6);
/*初始化线程*/
private static class InitThread implements Runnable {
public void run() {
System.out.println(TAG + Thread.currentThread().getName() + " 初始化线程run开始执行");
latch.countDown();
for (int i = 0; i < 2; i++) {
System.out.println(TAG + Thread.currentThread().getName() + " 初始化线程do its work");
}
}
}
/*业务线程等待latch的计数器为0完成*/
private static class BusiThread implements Runnable {
public void run() {
try { latch.await(); // 等待latch的计数器为0
} catch (InterruptedException e) { e.printStackTrace(); }
for (int i = 0; i < 4; i++) {
System.out.println(TAG + "业务线程," + Thread.currentThread().getName() + " do task");
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
SleepUtils.ms(1);
System.out.println(TAG + "1 当前线程:" + Thread.currentThread().getName());
latch.countDown();
System.out.println(TAG + "1 latch.getCount():" + latch.getCount());
SleepUtils.ms(1);
System.out.println(TAG + "2 当前线程:" + Thread.currentThread().getName());
latch.countDown();
System.out.println(TAG + "2 latch.getCount():" + latch.getCount());
}
}).start();
System.out.println(TAG + "new Thread() start end");
new Thread(new BusiThread()).start();
System.out.println(TAG + "second new Thread() start end");
for (int i = 0; i <= 3; i++) {
System.out.println(TAG + "for recycle start");
Thread thread = new Thread(new InitThread());
thread.start();
}
latch.await();
System.out.println(TAG + "Main do work end");
}
死锁
概念
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
举个例子:A和B去按摩洗脚,都想在洗脚的时候,同时顺便做个头部按摩,13技师擅长足底按摩,14擅长头部按摩。
这个时候A先抢到14,B先抢到13,两个人都想同时洗脚和头部按摩,于是就互不相让,扬言我死也不让你这样的话,A抢到14,想要13,B抢到13,想要14,在这个想同时洗脚和头部按摩的事情上A和B就产生了死锁。怎么解决这个问题呢?
第一种,假如这个时候,来了个15,刚好也是擅长头部按摩的,A又没有两个脑袋,自然就归了B,于是B就美滋滋的洗脚和做头部按摩,剩下A在旁边气鼓鼓的,这个时候死锁这种情况就被打破了,不存在了。
第二种,C出场了,用武力强迫A和B,必须先做洗脚,再头部按摩,这种情况下,A和B谁先抢到13,谁就可以进行下去,另外一个没抢到的,就等着,这种情况下,也不会产生死锁。
死锁还有几个要求,
1、争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁;
2、争夺者拿到资源不放手。
死锁学术化的定义
死锁的发生必须具备以下四个必要条件。
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直到占有资源的进程释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。
只要打破四个必要条件之一就能有效预防死锁的发生。
打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。(先抢到A资源,才能抢B资源)
打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
避免死锁常见的算法有有序资源分配法、银行家算法。
危害
- 线程不工作了,但是整个程序还是活着的
- 没有任何的异常信息可以供我们检查。
- 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。
解决
关键是保证拿锁的顺序一致
两种解决方式
-
- 内部通过顺序比较,确定拿锁的顺序;
- 采用尝试拿锁的机制。
活锁
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
举例:
线程A、B,锁1 和锁2
线程A拿到锁1,尝试拿锁2, 即A(1)<2> ((1)表示拿到锁1,<2>表示尝试拿锁2)
线程B拿到锁2,尝试拿锁1, 即B(2)<1>
很明显,A和B 都拿不到剩余锁的,接下来A释放锁1,即A()(),B释放锁2,即B()()。
下一次循环时,可能A又拿到了锁1,尝试拿锁2,B拿到锁2,尝试拿锁1
这样就是:
A不断的拿到锁1,尝试拿锁2,没有拿到,释放
B不断的拿到锁2,尝试拿锁1,没有拿到,释放
简介表达就是
A(1)<2> -- A()() -- A(1)<2> -- A()() -- A(1)<2> -- A()() .........
B(2)<1> -- B()() -- B(2)<1> -- B()() -- B(2)<1> -- B()() .........
CAS基本原理(Compare And Swap)
什么是原子操作?如何实现原子操作?
原子操作指的是:某个任务,要么全部执行完里面的指令。要么一条指令都不执行。例如像synchronized锁住的代码块。
实现原子操作:
一:可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,
这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。还有一种情况,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争,同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。
二:还可以使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。
CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
举例说明:
有四个线程,都要执行count++,count初始值为0
1,四个线程首先从内存中取得原始值0
2,四个线程内部都把count加1,内存中仍是0
执行到count++,四个线程内部都把count加1,即由0变为1
3,只有一个线程,由0 改为 1
四个线程,都想把新值写入内存,CPU处理器的CAS机制,保证了只有一个线程可以去执行比较和交换的任务。
有一个线程,去比较现在内存中count的值是否等于0,内存中是等于0,则替换,即把内存中的值由0替换为1,内存值改写为1,即count变量改为新值1
4, 内存中的值,已经被一个线程改为 1了,这个线程退出,其余线程继续执行
5,另外一个线程进来,比较内存中的值 是否 为0 (因为自己拿到的是0),发现不是0 了(前一个线程已经修改了内存的值为1,不是0),则重新拿到内存的值1,继续执行,同理其他线程
6。其余三个线程,重新拿到内存中为1的值,继续重新以count = 1 执行。
三个线程中只有一个线程 ,可以进入,如下
7, 由 1 改为 2
其中一个线程,进入后,判断对比内存中的 count值是否 为1,是的话,将内存中的值改为2,退出CAS,其他的两个线程,继续,其中一个线程进入CAS时,发现内存中的count =2,不是1,又得重新获取内存中的值,继续以count = 2 重新来一遍。
CAS实现原子操作的三大问题
-
ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。
2,循环时间长开销大。
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3,只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。(举例,要把A改为B,把甲改为乙,A和甲是多个共享变量,CAS无法保证多个共享变量操作的原子性)
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
阻塞队列和线程池原理
队列(队列称为先进先出(FIFO—first in first out)线性表)
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
什么是阻塞队列
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
常用阻塞队列
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
以上的阻塞队列都实现了BlockingQueue接口,也都是线程安全的。
有界无界?
有限队列就是长度有限,满了以后生产者会阻塞,无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞,当然空间限制来源于系统资源的限制,如果处理不及时,导致队列越来越大越来越大,超出一定的限制致使内存超限,操作系统或者JVM帮你解决烦恼,直接把你 OOM kill 省事了。
无界也会阻塞,为何?因为阻塞不仅仅体现在生产者放入元素时会阻塞,消费者拿取元素时,如果没有元素,同样也会阻塞。
Array实现和Linked实现的区别
1. 队列中锁的实现不同
ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;
LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock
2. 在生产或消费时操作不同
ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;
LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node<E>进行插入或移除,会影响性能
3. 队列大小初始化方式不同
ArrayBlockingQueue实现的队列中必须指定队列的大小;
LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE
线程池
为什么要用线程池?
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池的3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。 如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
ThreadPoolExecutor 的类关系
Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。
ExecutorService接口继承了Executor,在其上做了一些shutdown()、submit()的扩展,可以说是真正的线程池接口;
AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;
ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;
ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
线程池的创建各个参数含义
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;
如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用
TimeUnit
keepAliveTime的时间单位
workQueue
workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。
一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)更重要的,使用无界queue可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。
threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。
Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。
RejectedExecutionHandler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
线程池的工作机制
corePoolSize—>BlockingQueue—>maximumPoolSize—>拒绝策略
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
提交任务
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
AbstractQueuedSynchronizer
学习AQS的必要性
队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),
作用:
用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。
AQS使用方式和其中的设计模式
使用:
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态。
在AQS里由一个int型的state来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用。
同步器既支持独占式地获取同步状态,也支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
同步器与锁的关系:
同步器是实现锁/任意同步组件的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系:
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
AQS中的方法
模板方法
实现自定义同步组件时,将会调用同步器提供的模板方法,
这些模板方法同步器提供的模板方法基本上分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况。
可重写的方法
访问或修改同步状态的方法
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
•getState():获取当前同步状态。
•setState(int newState):设置当前同步状态。
•compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
CLH队列锁:所有排队的线程,放到一个链表队列里
整体思想:线程的前驱节点myPred 会不断的自旋。检测队列前面一个线程的locked 字段是否为false, 如果为false,则表明前面一个线程释放了锁,发现前面的线程释放了锁,结束自旋。
CLH队列锁即Craig, Landin, and Hagersten (CLH) locks。
CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
当一个线程需要获取锁时:
- 创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用
- 线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred
线程B需要获得锁,同样的流程再来一遍
3.线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)
4.当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点
如上图所示,前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。
CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。CLH队列锁常用在SMP体系结构下。
Java中的AQS是CLH队列锁的一种变体实现。
ReentrantLock的实现
锁的可重入
概念:重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。
举例:
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:
逻辑:
判断是否是当前线程去获取锁的线程,来决定获取操作是否成功
如果是得到锁的线程,再去请求获取锁,则将同步状态值增加,并返回true,表示获取同步状态成功。
同步状态表示锁被一个线程重复获取的次数(即同步状态的概念是指锁被一个线程重复获取的次数,指的是次数,同一个线程获取锁的同一个锁的次数)。
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
公平和非公平锁:
公平:就是去判断队列有没有线程,有,就排到队列尾部
非公平:就是不管里队列里有没有线程,只要有线程抢到锁,就可以获得到锁,不用排队
从代码去分析:
ReentrantLock的构造函数中,
默认的无参构造函数将会把Sync对象创建为NonfairSync对象,这是一个“非公平锁”;
带参构造函数ReentrantLock(boolean fair)传入参数为true时将会把Sync对象创建为“公平锁”FairSync。
对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,
而公平锁则不同。
公平锁的tryAcquire方法与非公平锁的nonfairTryAcquire(int acquires)比较,唯一不同的位置是:多了hasQueuedPredecessors()方法的判断条件,即加入了同步队列中当前节点是否有前驱节点的判断,
hasQueuedPredecessors() = true,则表示有线程比当前线程更早地请求获取锁,因此当前线程需要等待前驱线程获取到锁并释放锁之后才能继续去请求获取锁。
JMM基础-计算机原理
Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。Java1.5版本对其进行了重构,现在的Java仍沿用了Java1.5的版本。Jmm遇到的问题与现代计算机中遇到的问题是差不多的。
物理计算机中的并发问题,物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
根据《Jeff Dean在Google全体工程大会的报告》我们可以看到
计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的。
(以下案例仅做说明,并不代表真实情况。)
如果从内存中读取1M的int型数据由CPU进行累加,耗时要多久?
做个简单的计算,1M的数据,Java里int型为32位,4个字节,共有1024*1024/4 = 262144个整数 ,则CPU 计算耗时:262144 *0.6 = 157 286 纳秒,而我们知道从内存读取1M数据需要250000纳秒,两者虽然有差距(当然这个差距并不小,十万纳秒的时间足够CPU执行将近二十万条指令了),但是还在一个数量级上。但是,没有任何缓存机制的情况下,意味着每个数都需要从内存中读取,这样加上CPU读取一次内存需要100纳秒,262144个整数从内存读取到CPU加上计算时间一共需要262144*100+250000 = 26 464 400 纳秒,这就存在着数量级上的差异了。
而且现实情况中绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是基本上是无法消除的(无法仅靠寄存器来完成所有运算任务)。早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
在计算机系统中,寄存器划是L0级缓存,接着依次是L1,L2,L3(接下来是内存,本地磁盘,远程存储)。越往上的缓存存储空间越小,速度越快,成本也更高;越往下的存储空间越大,速度更慢,成本也更低。从上至下,每一层都可以看做是更下一层的缓存,即:L0寄存器是L1一级缓存的缓存,L1是L2的缓存,依次类推;每一层的数据都是来至它的下一层,所以每一层的数据是下一层的数据的子集。
在现代CPU上,一般来说L0, L1,L2,L3都集成在CPU内部,而L1还分为一级数据缓存(Data Cache,D-Cache,L1d)和一级指令缓存(Instruction Cache,I-Cache,L1i),分别用于存放数据和执行数据的指令解码。每个核心拥有独立的运算处理单元、控制器、寄存器、L1、L2缓存,然后一个CPU的多个核心共享最后一层CPU缓存L3
Java内存模型(JMM)
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
看看一线大厂面试题
sychronied修饰普通方法和静态方法的区别?什么是可见性?
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。
要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。
锁分哪几类?
CAS无锁编程的原理。
使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。
CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
还可以说说CAS的三大问题。
ReentrantLock的实现原理。
线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每 释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。
底层则是利用了JUC中的AQS来实现的。
AQS原理 (小米 京东)
是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。
这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。
在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。
Synchronized的原理以及与ReentrantLock的区别。(360)
synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
Synchronized做了哪些优化 (京东)
引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析
等技术来减少锁操作的开销。
逃逸分析
如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化:
同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。
锁消除和粗化
锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。
锁粗化:将临近的代码块用同一个锁合并起来。
消除无意义的锁获取和释放,可以提高程序运行性能。
Synchronized static与非static锁的区别和范围(小米)
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
volatile 能否保证线程安全?在DCL上的作用是什么?
不能保证,在DCL的作用是:volatile是会保证被修饰的变量的可见性和 有序性,保证了单例模式下,保证在创建对象的时候的执行顺序一定是
1.分配内存空间
2.实例化对象instance
3.把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为null了
的步骤, 从而保证了instance要么为null 要么是已经完全初始化好的对象。
volatile和synchronize有什么区别?(B站 小米 京东)
volatile是最轻量的同步机制。
volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证操作的原子性,因此多线程下的写复合操作会导致线程安全问题。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
什么是守护线程?你是如何退出一个线程的?
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
线程的中止:
要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。因为会导致程序可能工作在不确定状态下。
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,被中断的线程则是通过线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
sleep 、wait、yield 的区别,wait 的线程如何唤醒它?(东方头条)
yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
Wait通常被用于线程间交互,sleep通常被用于暂停执行,yield()方法使当前线程让出CPU占有权。
wait 的线程使用notify/notifyAll()进行唤醒。
sleep是可中断的么?(小米)
sleep本身就支持中断,如果线程在sleep期间被中断,则会抛出一个中断异常。
线程生命周期。
Java中线程的状态分为6种:
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。
ThreadLocal是什么?
ThreadLocal是Java里一种特殊的变量。ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
在内部实现上,每个线程内部都有一个ThreadLocalMap,用来保存每个线程所拥有的变量副本。
线程池基本原理。
在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。第二:提高响应速度。第三:提高线程的可管理性。
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
有三个线程T1,T2,T3,怎么确保它们按顺序执行?
可以用join方法实现。