一. 基础概念
- 什么是线程?和进程的区别?
一个程序相当于一个进程,而这个程序中不同的执行路径就是线程。 - 启动线程的几种方式
1:Thread 2: Runnable 3:Executors.newCachedThrad - 创建线程的简单示例:
public class T02_HowToCreateThread {
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello MyThread!");
}
}
static class MyRun implements Runnable {
@Override
public void run() {
System.out.println("Hello MyRun!");
}
}
public static void main(String[] args) {
new MyThread().start();
new Thread(new MyRun()).start();
new Thread(() -> {
System.out.println("Hello Lambda!");
}).start();
}
}
二. 线程最基本的方法
- Thread.sleep(): 当前线程暂停一段时间,sleep完之后线程进入就绪状态;
使用场景:将CPU让给别的线程去运行 - Thread.yield(): 暂时退出进入等待队列,让别的线程执行,让出一下CPU,不能保证别的线程拿到资源;
使用场景:做性能测试时压测可能用到 - t.join(): 在当前线程A调用线程B的join方法,当前线程A等待线程B执行完才执行;
使用场景:保证多个线程按顺序执行 - t.interrupt(): 中断当前线程,抛出InterruptedException异常,程序决定如何处理InterruptedException异常,一般不会用interrupt控制业务逻辑; 使用场景:如果某个线程一直处于等待状态比如sleep2天,此时可以调用interrupt()中断当前线程
- t.stop(): 终止线程,一般很少用
三. 线程常用的状态
一个线程从创建到消亡可能经过的状态有哪些?
- New;
- Runnable(ready,running);
- TimedWaiting;
- Waiting;
- Blolcked;
- Terminated(结束)
状态之间如何流转?请看下面的状态流程图,是不是一目了然
四. synchronized与volatile的区别
4.1 区别
- volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,线程读取变量的值需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中(即释放锁前),从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢synchronized锁对象时,会出现阻塞。
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性,因为线程获得锁才能进入临界区,从而保证临界区中的所有语句全部得到执行。
- volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化。
volatile的作用
- 保证线程可见性(MESI CPU的缓存一致性协议,保证变量值在多个线程中一致)
禁止指令重排序(编译时分三步:1申请一块内存(并赋默认值比如int=0),2初始化修改默认值,3引用赋值最终初始化,多线程下存在指令重排情况,添加volatile后禁止指令重排序(CPU),等对象初始化完成后才赋值)
volatile实现线程安全的单例模式:
public class TestSingleton01 {
// volatile 作用:1 保证线程可见性(MESI,缓存一致性协议),2 禁止指令重排序
// volatile 不能替代synchronized,它不能保证原子性
// JIT 超高并发场景,这里需要添加volatile,因为多线程下存在指令重排情况,添加volatile后禁止指令重排序(CPU),等对象初始化完成后才赋值
private static volatile TestSingleton01 instance;
public TestSingleton01(){
}
public static TestSingleton01 getInstance(){
// 业务逻辑代码省略
if(instance == null){
synchronized (TestSingleton01.class){
// 双重检查
if(instance == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new TestSingleton01();// 指令分为三步:1申请一块内存(并赋默认值比如int=0),2初始化修改默认值,3引用赋值instance(最终初始化)
}
}
}
return instance;
}
}
volatile是否能代替synchronized?
不能,volatile不能保证原子性操作,运行以下程序查看其结果:
public class T04_VolatileNotSync {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++) count++;
}
public static void main(String[] args) {
T04_VolatileNotSync t = new T04_VolatileNotSync();
List<Thread> threads = new ArrayList<Thread>();
for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
以上程序运行结果永远不等于100000,为什么?因为volatile不能保证原子性,需要在m方法上加上synchronized。
总结:
synchronized 是给对象上锁,保证其在多线程中的原子性。
volatile 修饰变量,保证其在多线程中的可见性。
4.2 synchronized实现原理
先思考一个问题:
synchronized锁的对象是什么?
- 是一个Object对象(头两位),不能是String="object"常量,及Long,Integer(如果类库中锁了一个常量,多个地方用到这个常量时,就会存在死锁阻塞问题,而对象不会)
; 注意:在静态方法上加synchronized
相当于synchronized(T.class),在方法内部写synchronized(this)和在方法上加synchronized是等同的(T.class
理论上是单例的,如果在同一个classLoader中)
锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,
应该用final修饰避免将锁定对象的引用变成另外的对象。
通过以下代码验证:
public class T {
private static volatile int count = 10;
private final Object o = new Object();
public void m() {
synchronized(o) { //任何线程要执行下面的代码,必须先拿到o的锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
public static void main(String[] args) {
new Thread(()->{
T t= new T();
t.m();
}).start();
//创建第二个线程
Thread t2 = new Thread(t::m, "t2");
t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
t2.start();
}
}
synchronized 实现原理(锁升级)
最初的jdk版本中是重量级锁,每次都需要向操作系统申请锁,1.5后hotspot进行了改进:
synchronized(Object)
1)mark word记录线程id,并标记线程锁状态为:偏向锁(下次遇到同样的线程时直接返回当前锁)
2)如果存在锁争用的情况,其它线程争用这把锁时,升级为:自旋锁(CAS第二个线程等着第一个结束,等待中的线程占用CPU)
3)自旋10次之后还得不到锁时,升级为:重量级锁(OS-等待中的线程不占用CPU)
思考什么情况下用自旋锁,什么情况下用重量级锁(系统锁)?
1)加锁代码执行时间短,线程数少时用【自旋锁】
2)加锁代码执行时间长,线程数多时用【系统锁】
synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
底层实现:JVM没有特殊要求,拿出对象头部信息Mark Word(标记字)的2两位作为来描述不通类型的锁,另外Mark Word还可以用来配合GC、存放该对象的hashCode:
以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:
延申知识对象的结构:我们都知道,Java对象存储在堆(Heap)内存。那么一个Java对象到底包含什么呢?概括起来分为对象头、对象体和对齐字节。如下图所示
对象的几个部分的作用:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)。
五. Java原子类AtomicXXX
AtomicXXX类型解决原子性问题的更高效的方法,使用AtomXXX类,AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
实现原理:
通过Unsafe的compareAndSwap这类原子性的方法实现,分配内存和改变类的属性
采用CAS(Compare And Set 属于CPU原语指令,执行过程不会被打断)
cas(Value,Expected,NewValue)
- if Value == Expected
Value =NewValue
otherwise try again or fail
ABA问题
在修改对象属性之前,别的线程对属性值进行了修改,比如原来值为A ,别的线程将值改成了B,又改成了A;这时CAS操作就会误认为它是从来没被改变过;会造成对象的属性值引用不一样的问题,可能造成程序逻辑问题
如果解决?
JUC包提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以控制变量值的版本来保证CAS的正确性, 增加version :A1.0 B-2.0 A3.0; cas(version),如果是基础数据类型不要管,引用类型加版本。
Unsafe类
13版本的jdk可用其直接操作内存中的实例的属性
import sun.misc.Unsafe;
public class HelloUnsafe {
static class M {
private M() {}
int i =0;
}
public static void main(String[] args) throws InstantiationException {
Unsafe unsafe = Unsafe.getUnsafe();
M m = (M)unsafe.allocateInstance(M.class);
m.i = 9;
System.out.println(m.i);
}
}