昨晚看到Guide哥推送的《深入浅出Java多线程》,今天一鼓作气看了9节,虽然看得很疲倦,但是对多线程的基本操作和一些原理还是有不少了解;
从进程与线程的区别开始,进程是由操作系统分配内存与其他资源(如io),线程是操作系统能够进行运算调度的最小单位。
线程创建的2种方式,继承Thread和实现Runnable,其中Thread使用到了装饰者模式,装饰Runnable对象,扩展Runnable的功能,又使用到了策略模式,Thread中的Runnable的各种实现就是一种策略;
具体使用哪个可以从2个方面考虑:
1、继承方式还是实现方式;
2、是否需要使用Thread类的诸多方法;
这两种线程的执行都是没有返回值的,如果需要返回值可以使用JDK提供的Callable接口和Future接口,需要配合ExecutorService#submit(Callable c) : Future 使用,返回值通过Future#get() : T 获取,get()方法会阻塞当前线程,直到得到返回结果;Future接口中定义了取消线程的cancel()方法,所以如果想取消线程的话可以使用Future的实现类FutureTask,cancel()操作内部实际上是执行Thread#interrupt()方法,所以调用此方法取消并不一定能取消成功。
昨天总结写的比较晚,写到12点多硬是撑不住了,今天将昨天未总结完的补上。
线程的优先级可以调用Thread#setPriority(int i)来设置优先级,取值范围1~10,然而并不是优先级高的线程一定比优先级低得先执行,具体还是由操作系统决定。
线程必须存在于线程组ThreadGroup当中,线程的优先级不能大于所在线程组的最大优先级,如果超过将会被线程组的最大优先级取而代之;
线程中还有一个守护线程(Deamon),可以通过Thread#setDaemon(boolean on)来设置,只有当所有非守护进程结束后,守护线程才会自动结束。
接下来是线程的6种状态以及它们之间的转换:NEW、RUNNABLE、BLOCKED、WATTING、TIMED_WATTING、TERMINATED
NEW
→ RUNNABLE
调用start(),如果有其他线程拿到锁了,则进入BLOCKED状态;
RUNNABLE
→ BLOCKED
等待锁
RUNNABLE
→ WAITING
调用当前锁调用wait()、其他线程调用join()
RUNNABLE
→ TIMED_WAITING
调用wait(time)、join(time)、sleep(time)
WAITING/TIMED_WAITING
→ RUNNABLE
调用Object.notify()、notifyAll()、LockSupport.unpark;
需要注意的是线程调用start()方法必定会进入RUNNABLE
状态,遇到锁后才有可能进入BLOCKED
状态;
接下来是线程间的通信,java线程机制主要采用锁+同步的方式来进行线程间的通信
Synchronized:同步标识,需要一个对象做为锁,且只能是Class对象或者Object对象;如果是静态方法中使用这个默认为该类.class,如果是普通方法标记则默认为this,同步代码块需要通过参数传入锁,如果两段同步的锁相同,那么线程可以从第一段同步执行完后,直接进入第二段同步代码,不需要竞争锁,因为锁只能被一个对象持有;
join():当一个线程调用join后,那么其他线程必须进入WAITING等待这个线程执行完毕后才会执行,主线程也不例外;
Object#wait()与Thread#Sleep(long t)区别:
1、wait()会释放锁,而sleep()不会;
2、wait()只有在拿到锁的情况才能执行,而sleep()在线程中随时可以执行;
接着作者提到了ThreadLocal类主要是为每个线程创建一个副本,用来存储线程自己的私有变量;
呼啦啦,这才总结了一半,不总结还真不知道昨天学了多少知识!总结写的好心累啊,一方面想着学习进度,一方面手头还有些工作要弄,不管了,加油吧!少年!
下面是原理篇,主要是java内存模型、volatile关键字、锁的几种类别,先从java内存模型讲起吧,
java运行时内存主要由方法区、堆、虚拟机栈、本地方法栈、程序计数器
这无大部分组成,对于每个线程来说,堆是共有的,而栈是私有的,每个线程都含有一个本地内存,保存着该线程使用的堆空间中共享变量的副本,线程只与本地内存交互,而本地内存什么时候更新到共享内存由虚拟机控制。线程间的通信必须通过共享内存来进行通信。
讲到共享内存通信,那不得不提volatile关键字,其作用主要有2个:
1、内存共享:当一个线程对volatile修饰的变量进行写操作时,java运行模型会立即把该线程对应的本地内存中的值刷新到主内存中,当一个线程对volatile修饰的变量进行读操作时,java运行模型会将本地内存中的值设置成无效,并从主内存中读取;
2、防止指令重排;
讲到volatile关键字不得不提单例模式双重检查+锁模式,其中单例变量就使用了volatile关键字标记,而它的作用主要就是防止指令重排,因为new Object()操作在编译器内有3条指令:分配内存、初始化、赋值给变量
,而如果还没有实例化就复制给了变量,这是另一个线程刚好进入到第一层检查,那么就会直接返回一个未初始化完成的对象;
最后,了解到了同步锁还存在升级机制,而锁的状态也分为4种:无锁状态、偏向锁、轻量级锁、重量级锁
既然讲到锁,那得弄清楚锁存在哪个地方,锁都是基于对象的,锁信息存放在java对象头中,
java对象头占用2个字宽,如果是数组则占用3个字宽,多一个字宽用来存储数组的长度,一个字宽对应操作系统的位数,如果是64位操作系统中,那java对象头就占了128位,这128位,主要存储2种数据,hashCode+锁信息、类型数据指针;
对象头中就保存着锁的状态,线程在第一次进入同步代码块时,会将线程ID保存到锁的对象头中,并且把锁设置为偏向锁的信息也保存进去;
线程再次进入同步代码块中时,会去检查锁对象头里的Mark Word信息,如果锁的对象头中Mark Word里已经有线程ID了,且线程ID等于自己时,那说明该线程已经获得了锁,并且不需要花费CAS(compare and swap)操作来进行加锁和解锁,那么这个锁还是偏向锁;
如果线程ID不等于自己的线程ID,那说明有另一个线程来竞争这把锁了,这个时候会尝试CAS操作来替换Mark Word里的线程ID和锁类型,CAS操作返回成功和失败,如果成功,则表示之前线程已经不存在了,那么替换ID,不升级锁;如果失败,说明之前的线程还存在,这时需要暂停之前的线程,将锁升级为轻量级锁(这个过程有一定开销);
还是刚刚那个线程,在讲偏向锁升级为轻量级锁后,会自旋竞争锁,如果超过一定时间获取不到,就会进入阻塞状态,CAS会将锁升级为重量级锁,未竞争到锁的线程将会进入阻塞状态,阻塞状态不消耗CPU资源,但是线程间状态的转换需要相对较长的较长的时间,所以重量级锁效率较低
呼!终于完了,最后再来总结下这三种锁的区别和使用场景吧。
1、偏向锁:加锁和解锁操作不需要额外的消耗,速度接近非同步方法,但是线程竞争锁导致锁升级会造成额外的消耗,适用于多线程中同一时间段内只有一个线程访问同步代码块的情况;
2、轻量级锁:竞争锁不会阻塞,提高了程序的响应速度,但是始终得不到锁的线程会自旋消耗CPU资源,适用于追求响应速度的情况;
3、重量级锁:线程竞争不使用自旋,不会消耗CPU,但是线程阻塞,响应时间缓慢,同步块执行速度较长,适用于追求吞吐量的情况。