文章目录
- 线程的三种创建方式
- 对于FutureTask用在Thread中的理解
- 那么FutureTask的构造类接受参数?
- FutureTask是如何保存值的
- FutureTask获取值
- 更多
- wait方法得用在同步代码块中
- wait释放锁的问题
- notify的使用介绍
- join方法的中断异常
- sleep方法介绍
- sleep的中断异常
- sleep方法和yield方法的区别
- interrupt 和 interrupted 和 isInterrupted
- 什么是线程的上下文切换
- 死锁的产生条件
- 避免死锁
- 守护线程和用户线程
- ThreadLocal的介绍
- ThreadLocal类的简单分析
- ThreadLocal的继承性
- 弱引用和软引用的区别
- java的内存模型
- 简单描述一下内存不可见
- synchronized的内存语义
- volatile的介绍
- synchronized和volatile的比较
- 总线锁定和缓存一致性
- MESI协议提供的四个状态
- MESI协议约定的监听
- ++操作在底层的一个具体过程
- CAS介绍
- Unsafe类的认识
- 伪共享(缓存行)
线程的三种创建方式
- 继承Thread类,创建线程,但由于java类继承的单一性,所以该类就无法再继承其它类。
- 一个类实现Runnable 接口,将该类作为实例传入Thread
- 一个类实现Callable接口,有返回值。将该类作为实例传入FutureTask,再将FutureTask的实例传入Thread。
public class demo1 {
/**
* 线程的三种创建方式
*/
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread并创建线程");
}
}
public static class RunableTask implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口,创建线程,重写方法");
}
}
public static class CallableTask implements Callable{
@Override
public Object call() throws Exception {
System.out.println("实现Callable接口,重写call方法");
return "成功";
}
}
public static void main(String[] args) {
// --------继承Thread,重写run方法--------
MyThread myThread = new MyThread();
myThread.start();
// --------实现Runnable接口,重写run方法--------
// new Thread(new RunableTask()).start();
RunableTask task = new RunableTask();
new Thread(task).start();
// --------实现Callable方法,重写call方法--------
CallableTask callableTask = new CallableTask();
FutureTask<String> futureTask = new FutureTask<>(callableTask);
new Thread(futureTask).start();
try {
System.out.println("打印返回值:"+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
对于FutureTask用在Thread中的理解
Thread是并不支持Callable接口的,通过下图可以发现,并没有Callable接口,那么为什么FutureTask可以传入呢?
来看一下FutureTask的结构,可以发现Future实现了RunableFuture接口,而RunableFuture接口又继承实现了Runnable和Future接口。所以FutureTask可以传入到Thread当中去。
那么FutureTask的构造类接受参数?
进入FutureTask源码,可以发现其构造类,虽然可以接受Runnable和Callable,但是最终都会被转成Callable接口,可能跟其实现了Future接口,需要有返回值有关系吧。
FutureTask是如何保存值的
在run方法中,通过setException和set(result)来保存值。
FutureTask获取值
通过get方法获取值,在get方法中有一个重要的方法是awaitDone
这个awaitDone的方法,意思就如解释的那样,等待的过程,如果成功则返回,如果中断或者超时则终止返回。它是通过判断AQS状态state,来决定的。(研究多线程,一定要注意state,AQS,CAS)
更多
如果想具体了解,可以看下面这篇文章
Java并发编程Future超详细教程
wait方法得用在同步代码块中
wait()方法,它是一个Object的方法。
假设有一个公共资源,和两个线程,按照下面方式启动
public class demo{
private static List<String> list = new ArrayList<>();
public static void main(String []args){
Thread threadA = new Thread(()->{
//wait,只有在当前共享变量的同步代码块中,才能用
synchronized(list){
System.out.println("我正在等待中");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我已经被唤醒,准备运行");
}
});
Thread threadB = new Thread(()->{
synchronized(list){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("既然你偷懒了,释放锁了,那就让我来吧,等会我再叫你");
list.notifyAll();
System.out.println("我好了,到A你了");
}
});
threadA.start();
threadB.start();
}
}
效果如下
如果wait,或者notify,notifyAll不放在同步代码块中,是会抛出下面这个异常的
非法监视器状态异常
wait释放锁的问题
wait方法只会释放调用wait()方法的对象的身上的锁,并不是在哪个同步代码块中调用,就释放这个同步代码块的锁。比如下面:
synchronized(resourceA){
System.out.println("获取到资源A的锁");
synchronized(resourceB){
try{
System.out.println("获取到资源B的锁");
resourceA.wait();
System.out.println("释放资源A的锁");
}catch(Exception e){
e.printStackTrace();
}
}
}
对于上述代码,如果启动一个线程去执行,那么这个线程在获得两个锁之后,只会释放A锁,然后就被挂起,则B锁一直被它攥在手里。别的线程将无法获得B锁。
notify的使用介绍
notify()方法也是Object的一个方法
当有线程锁的资源,调用了wait方法之后,该线程就会释放该资源的锁,并进入阻塞状态。当该资源执行notify的时候,那么其余线程将再来争夺这个资源的锁,抢到锁的才会运行,所以并不是刚刚释放资源锁的线程立马开始运行,也是要抢夺的。
join方法的中断异常
当线程A还没结束时,主线程就被结束了,在抛出异常的地方,抛出了中断异常
public class demo6 {
public static void main(String[] args) {
Thread threadA = new Thread(()->{
System.out.println("线程A即将进入循环");
for(;;){
}
});
Thread mainThread = Thread.currentThread();
Thread threadB = new Thread(()->{
try {
System.out.println("让主线程先往下执行到threadA.join");
TimeUnit.SECONDS.sleep(2);
System.out.println();
System.out.println("现在已经2s后了,准备中断线程,即将抛出异常");
mainThread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadA.start();
System.out.println("线程A启动");
threadB.start();
System.out.println("线程B启动");
try {
System.out.println("准备让主线程阻塞,等待threadA执行完");
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行效果如下:
sleep方法介绍
Thread中有一个静态的 sleep方法,当一个执行中的线程调用了Thread.sleep方法后,调用线程会暂时让出CPU,但是该线程所拥有的锁,仍然被其抱着。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 资源后就可以继续运行了。如果在睡眠期间其它线程调用了该线程的 interrupt ()方法中断了该线程,则该线程会在调用 sleep 方法的地方抛出 IntermptedException 异常而返回
public class demo7 {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
Thread threadA = new Thread(()->{
try {
lock.lock();
System.out.println("threadA 获取锁,准备去睡觉");
TimeUnit.SECONDS.sleep(2);
System.out.println("threadA 准备醒了,.....执行完啦");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread threadB = new Thread(()->{
try {
lock.lock();
System.out.println("threadB 获取锁,准备去睡觉");
TimeUnit.SECONDS.sleep(2);
System.out.println("threadB 准备醒了,.....执行完啦");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
threadA.start();
threadB.start();
}
}
输出效果一直如下,不会交叉改变,说明线程抱着锁去睡觉了。
sleep的中断异常
在上面的代码中,ThreadB中添加上
threadA.interrupt();
会发现报下面这个错误,在threadA睡觉的地方抛出异常
sleep方法和yield方法的区别
sleep方法会让当前方法阻塞,但是这个cpu并不会进行重新调度,等待睡眠时间过了之后,还是会执行当前线程。
但yield方法会让出当前的cpu资源,让自己重新进入就绪状态,然后cpu就开始重新进行调度,包括让出资源的线程在内,每个线程都有机会获得cpu资源
interrupt 和 interrupted 和 isInterrupted
interrupt是Thread类中的一个普通方法,由Thread对象调用,调用后会中断调用线程。此时中断标志位为true。
interrupted是Thread类中的一个静态对象,直接由Thread类调用,调用后会返回当前线程的标志位,然后将当前中断标志位设置为false。
isInterrupted 是Thread类中的一个普通方法,用来返回当前线程的中断标志位。
可以看一个例子:
public class demo8 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("start thread isInterrupted:"+Thread.currentThread().isInterrupted());
while (!Thread.interrupted()){
}
System.out.println("end thread isInterrupted:"+Thread.currentThread().isInterrupted());
}
});
thread.start();
thread.interrupt();
System.out.println("中断thread线程");
thread.join();
System.out.println("over");
}
}
执行效果如下:
主线程中断thread线程后,进入,发现当前标志位是true,经过interrupted重置后改为false。
什么是线程的上下文切换
就是当前线程用完cpu分配的时间片后(当然也可能没用完就让出了cpu资源),需要让出cpu资源了,让出之后,cpu就要重新进行分配,将时间片分配给下一个线程。这一种时间片轮转法,让你感觉多个线程是同时执行的。这就是线程切换,从当前的线程的上下文,切换到了别的线程的上下文。
但是当线程重新获取到资源时,线程是怎么知道自己之前执行到了哪里呢?
所以在切换线程的上下文时,需要保存上下文环境,等到下一次cpu资源到来时,再恢复上下文环境,继续向下执行
死锁的产生条件
- 互斥条件:即一个资源只能由一个线程占用。
- 请求资源和持有条件:请求着别人的资源,又不释放自己的资源
- 不可剥夺条件:自己的资源在自己使用完前,不能被其他人抢走
- 环路等待条件:形成A(B)《==》B(A)
避免死锁
破坏上述死锁条件中的一种,或者提前避免发生死锁。
目前只有请求并持有和环路等待条件可以破坏。
预设置好执行的顺序,可以有效的避免产生死锁。
守护线程和用户线程
java中的线程分为两种,一种是daemon线程,一种是user线程。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程。只有所有用户线程执行完的时候,JVM才能退出。守护线程,就比如JVM启动时,内部同时还启动了很多的线程,比如垃圾回收线程。守护线程并不影响JVM的退出
在java中通过如下方法创建守护线程
public static void main(String[] args){
Thread deamon = new Threa(new Runnable(){
@Overried
public void run(){
}
});
deamon.setDaemon(true);
deamon.start();
}
ThreadLocal的介绍
在Thread类中,可以发现ThreadLocal作为ThreadLocalMap类型的变量存在着,ThreadLocalMap是一个定制化的HashMap,至于为什么用Map结构? 原因是一个线程可以关联多个ThreadLocal变量。
当一个线程访问ThreadLocal(执行get方法时)的时候,其实际执行的方法是,详细请看下面代码注释
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程持有的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//找到ThreadLocalMap中,key为当前线程所对应的的实例Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取Entry中保存着的value
T result = (T)e.value;
//返回该值
return result;
}
}
//如果没有ThreadLocalMap,则去初始化一个,并返回一个Value为Null的值。set方法内容跟下面这个方法几乎一样。
//但这个方法是private的,set方法不是。
return setInitialValue();
}
ThreadLocal类的简单分析
- 内部的ThreadLocalMap的key和value分别是什么?key指的是当前的线程,而value指的是当前线程对象的实例。
- 可以发现这个key用到的是WeakReference的一个方法,说明这个key是一个弱引用。之所以用弱引用的原因就是,当没有强引用指向该ThreadLocal变量时,它就可以被回收。
- 我们调用的set方法,最终执行的是ThreadLocalMap的set方法(即创建一个Entry对象)。
- 在ThreadLocalMap的set方法中,如果key为Null,存在一个清除key的方法 (replaceStaleEntry)。在清除key的方法中,存在一个删除key对应entry的方法 (expungeStaleEntry) 。
- 它是作为一个变量存在线程当中的,只对当前线程可见,所以不存在多线程安全和锁的问题。
ThreadLocal的继承性
ThreadLocal不支持继承性。即你在父线程中设置的的ThreadLocal变量,在子线程中是无法访问到的。
但是你可以用InheritableThreadLocal来创建ThreadLocal,如下,这样子线程也可以访问到父线程的了。如果子线程自己有,则使用自己的。
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
弱引用和软引用的区别
Java中4种引用的级别和强度由高到低依次为:
强引用 -> 软引用 -> 弱引用 -> 虚引用
- 强引用,gc宁愿抛出异常,也不会去回收这些具有强引用的对象。
- 软引用,当内存不够的时候,才会去回收这些具有软引用的对象。
- 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象
java的内存模型
在学习java内存可见性时,我们需要了解一下多线程下处理共享变量时java的内存模型。
java内存模型(JMM)规定,将所有的共享变量存放在主内存中,当线程需要使用这些变量时,再将这些变量拿到自己的工作内存中使用。java内存模型是一个抽象的概念。实际上应该是像下图这样子的。
而工作内存也就对应着图中的一二级缓存或者CPU寄存器。
简单描述一下内存不可见
针对上面这个图,进行一下描述。
最开始都是空的
- 如果此时A线程去访问一个共享变量X,那么先去一二级缓存,都不命中,然后再去主内存中读取变量X=0,然后A对其进行修改,将X改为1,并存入一二级缓存,并将X刷回主内存,这样主内存和两级缓存中都存在X=1。
- 此时B线程也去访问共享变量X,先去一级缓存,一级缓存不命中,那么就去二级缓存,二级缓存命中了,那么就将该共享变量改为了X=2,然后将一级缓存设置成X=2,主内存刷成X=2
- 然后A线程又去访问了,这时A线程发现自己的一级缓存中存在X=1,那么就返回了。但是线程B明明已经把共享变量X改成了2了。所以这就是内存的不可见性。
synchronized的内存语义
synchronized也是可以保证内存可见性的,通过其语义来理解:
进入synchronized块的内存语义,其实就是把synchronized中用到的变量从工作内存中清除,重新从主内存中读取。
退出synchronized块的内存语义,就是把synchronized块中的共享变量修改后的值,刷回到主内存。
只是加锁太笨重,而且上下文的切换影响性能。
所以java提供了一种弱形式的同步,也就是使用volatile
volatile的介绍
当一个变量被声明为volatile时,在对该变量进行写的操作的时候,不会将该变量写进缓存或者寄存器,而是直接写入主内存中。当其他线程读取该变量时,不会从工作内存中读取,而是直接去主内存中读取
synchronized和volatile的比较
- volatile不需要加锁,比synchronized轻量级,不会阻塞线程。
- synchronized关键字既能保证原子性,又能保证可见性;而volatile关键字只能保证可见性
总线锁定和缓存一致性
对于上面的JMM内存结构,只是简单描述了在单核情况下并发的内存不可见性,如果在多核情况下,多个线程并行的情况下,同样会产生问题。
- 所以先是提出了总线锁定的一个解决方案,意思就是锁定单个cpu,其它的cpu等待操作内存。但这个方案对于系统的开销损失较大。
- 于是后来提出了一个缓存一致性协议MESI。下面简单描述一下MESI保证内存可见性的一个过程。
MESI协议提供的四个状态
1.Modified,修改的:只在当前cpu内有修改后的缓存数据,其它cpu中没有,且该数据还并没有写回主内存
2.Exclusive,独占的:只在当前cpu内有缓存数据,且与主内存中一致
3.Share ,共享的:在所有的cpu中都有缓存数据,且与主内存中一致
4.Invalid,无效的:本cpu中的这份缓存已经无效了
MESI协议约定的监听
- 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
- 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
- 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S
当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下会性能开销是相对较大的。在写入完成后,修改其缓存状态为M。
所以如果一个变量在某段时间只被一个线程频繁地修改,则使用其内部缓存就完全可以办到,不涉及到总线事务,如果缓存一会被这个CPU独占、一会被那个CPU独占,这时才会不断产生RFO指令影响到并发性能(与线程多少不一样的)。
++操作在底层的一个具体过程
假设现在我们声明了一个变量
value
并对该变量做如下操作
value++
当多个线程操作value时,就会出现问题,因为value++并不是一个原子性操作。什么是原子性操作?一组指令要么全部执行,要么全不执行,不可能执行到一半。
value++在底层的过程应该是这样的
1.获取到value的值放入栈顶
2.将常量1放入栈顶
3.将当前栈顶两个值相加,并把结果放入栈顶
4.将栈顶的值赋给value
因此,这么多步,如果不进行同步,那么是很容易出现线程不安全问题的。
CAS介绍
- 为了保证内存可见性,可以对代码块加上synchronized。
但由于synchronized导致的线程阻塞,切换上下文引起的性能开销。 - 于是提供了关键字volatile
但是volatile并不保证原子性操作, - 所以JDK提供了非阻塞原子性操作CAS
compare and swap,通过硬件保证比较–更新,Unsafe类中提供了一系列的CAS操作
ABA问题的解决:
变量的状态发生了环形转换就会造成ABA,所以,JDK的AtomicStampedReference为每个变量的状态值都增加了一个时间戳,这样就避免了前后一致ABA问题
Unsafe类的认识
Unsafe类是JDK的rt.jar包下的类,里面的方法都是native方法,即都是本地方法,它们使用JNI的方式访问本地的C++库,实现对操作系统的操作。
unsafe类的介绍
伪共享(缓存行)
像我们上面说的工作内存那里,实际上是由多个缓存而组成的。我们这里以两层缓存为例,来讲一下伪共享的问题。
缓存内部其实是分为一行行的,称为缓存行,缓存行是有大小的,在向主内存取值的时候,并不是取一个变量进来,而是取了一个内存块过来,所以可能会有多个变量被取到了一个缓存行中。而缓存行只能由一个线程操作,所以当多个线程想要操作的变量在一个缓存行里,就会导致竞争缓存行,对比单线程操作,就会产生性能下降的问题。
但比如对一个线程操作一个数组来说。由于其存在内存中的变量是连续的,如果按顺序取值,那么对于一个缓存行来说,就可以取到多个数组的值,这比直接去内存中查询值来的更快。因为可以在一个缓存行中命中多个值。
如何避免伪共享。
JDK8 提供了 sun.misc Contended 注解,用来解决伪共享问题,但其只能用于Java核心类,比如rt包下的类。
对于用户路径下的类需要使用这个注解,需要添加JVM参数:
-XX:-RestrictContended,填充的宽度默认为 128 。
要自定义宽度则可以-XX:ContendedPaddingWidth 参数。