线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
当多个线程同时执行相同的控制流时,我们就称之为并发;用代码语言来说,就是多个线程同时使用某个类,调用某个方法,修改某个变量。
public class Test{
public static void main(String[] args) throws Exception
{
// 第一种,实例化一个继承自Thread的类,调用start方法
new MyThread().start();
// 第二种,实例化一个Thread并传递一个Runnable对象,调用start方法
new Thread(new MyRunnable()).start();
// 第三种,实例化一个Thread,传递一个FutureTask对象,FutureTask对象中传递Callable,调用start方法
// Callable的结果会在未来某个时间返回给FutureTask
// 可以通过FutureTask的get方法获取到
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
Thread.sleep();
System.out.println(futureTask.get());
// 第四种,基于lambda的匿名内部类
// lambda表达式,java8才出现
new Thread(() ->
{
String va = "lambda...";
System.out.println(va);
}).start();
// 第五种,基于线程池的方式,但是其线程的本质与上面的几种没有实质性的区别
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() ->
{
System.out.println("thread pool...");
});
executorService.shutdownNow();
}
}
//以下是上面测试方法相关的测试对象
public class MyThread extends Thread{
@Override
public void run()
{
System.out.println("my thread...");
}
}
public class MyRunnable implements Runnable{
@Override
public void run()
{
System.out.println("my runnable...");
}
}
public class MyCallable implements Callable<String>
{
@Override
public String call() throws Exception
{
System.out.println("my callable...");
return "callable return...";
}
}
当系统出现高并发,多个线程执行某个方法,或者修改某个变量的时候,如果不考虑并发问题,可能因为执行的时序从而导致各个线程间的值交叉错乱;如下单例的示例
public class SingleObj
{
private static SingleObj singleObj = null;
private SingleObj(){}
public static SingleObj getInstance(){
// 假如有10个线程同时执行到这个位置
// 那么这10个线程的 if(null == singleObj) 判断的都会是ture
// 因此这10个线程就都会走if内的实例化
// 从而导致,系统中singleObj就不是单例对象了
if(null == singleObj){
singleObj = new SingleObj();
}
return singleObj;
}
}
线程安全产生的原因
解决线程安全,线程同步的手段
避免不必要的共享数据,保证堆上的所有数据不会逃逸出去从而被其他的线程访问
将一个对象锁住,保证同一时间只有拿到锁的线程在执行
保证数据的可见性,防止指令重排
数据只读,不能写
在整理线程安全相关东西之前,我们来了解一下,一个对象在HotSpot虚拟机中的内存布局;
HotSpot对象头主要用于存储运行时数据
(HashCode [ identity ],GC分代年龄,锁状态标记、偏向线程ID,偏向时间戳等),这部分数据随着当前对象的锁状态,不断的在发生变化,如下图所示;在32位和64位的虚拟机中,这部分数据分别为32bit和64bit
指向这个类元数据的指针,比如,这个对象是一个Object,那么这个指针就指向Object,虚拟机通过这个指针来确定这个对象是那个类的实例;不过并不是所有的虚拟机实现都必须在对象数据上保留类型指针,因此,查询对象的元数据并不一定要经过对象本身,所以,对象的访问取决于虚拟机的实现,可以是通过句柄
的方式,也可以是通过直接指针
的方式;
真正存储有效信息的区域,也就是程序代码中所定义的各种类型的字段内容。无论是从父类中继承的,还是子类中定义的,都会在这里记录起来。这部分的数据的顺序受虚拟机参数配置和字段在java代码中定义的顺序影响。HotSpot的分配策略为:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers 普通对象指针),可以看出,相同宽度被分配在一起。在满足这个前提下,父类定义的变量会出现在子类之前。如果CompactFlashs参数值为true(默认为true),那么子类较窄的变量也可能会插入到父类变量的空隙之中
这部分数据并不是必然存在的,同时这部分数据也没有什么实质性的含义,仅仅起到了占位符的作用;是因为HotSpot虚拟机要求对象必须是8的整数倍;因此,如果不够的情况下,需要进行填充补齐。
了解了对象在HotSpot虚拟机中的的一个基本结构之后,便于下面去分析一个对象在锁的时候,数据发生了一些说明样的变化
示例代码
public class SynchronizedDemo
{
private Object object = new Object();
public void m(){
synchronized (object){
System.out.println("123");
}
}
}
基本概念:synchronized用于去锁一个对象,保证同一时间只有一个线程会拿到并持有锁,拿到锁的线程允许执行synchronized包装的代码块;记住synchronized锁的是对象,并不是锁的代码块;如上面的代码所示,锁的是object对象,执行的是synchronized后面{}内的代码,以上示例System.out.println("123");
永远只会有一个线程调用它。
上图可以看出,左侧核心的区别是多了monitorenter和monitorexit两个关键字,在这段代码直接的数据,就只能有一个线程同时访问。字节码信息查看可以下载一个插件jclasslib Bytecode viewer
// 再次拿这个图拿出来,通过这个图,来分析一下整个对象锁的演变过程
// 下图为整个锁状态变化的过程
既然有轻量级锁,为什么还要存在重量级锁呢?
原因主要是因为轻量级锁是通过自旋来实现的,当出现大量的锁竞争的时候,无任何意义的自旋操作会大量占用CPU,从而导致性能的下降。锁记录(Lock Record)
的空间,用来存储当前对象Mark Word的拷贝,同时保存当前锁记录的拥有者(owner)是谁;CAS修改
对象的Mark Word数据,将锁记录(Lock Record)的指针记录到Mark Work(while(true){} 这么个意思)"01”
当一个对象刚刚创建(new Object())出来的时候,这个对象是一个崭新的,因此他属于无锁状态;此时对象头部分保存的是对象的hashcode等信息
其他锁相关的概念
public void strAppand(String s1, String s2)
{
StringBuffer sb = new StringBuffer();
sb.append(s1).append(s2);
}
这段代码意思很简单,而且也很常用;开发过程中也经常会出现字符串拼接的操作,如:s1+s2,最终虚拟机对String拼接会按以上的方式进行优化,但是我们知道StringBuffer是线程安全的,因此他的append方法是有synchronized修饰的,那么sb的append操作难道都对StringBuffer加锁了吗?其实不然,通过上面的代码我们发现,append的作用域都限制在strAppand()的方法内部,那么方法内的所有引用永远不会“逃逸”出strAppand()内部;这样一来,虽然说sb的每个操作都加了锁,但是可以被安全的消除,在即时编译的之后,这段代码会忽略掉所有的同步直接执行。
public class SingleObj{
private volatile static SingleObj singleObj = null;
private SingleObj(){}
public static SingleObj getInstance(){
if(null == singleObj){
synchronized (SingleObj.class){
if(null == singleObj){
singleObj = new SingleObj();
}
}
}
return singleObj;
}
}
我们共享的数据只有singleObj这一个对象,因此,我们只需要将同步代码块放在singleObj是否等于空的校验上,这样,一旦这个单例对象创建成功之后,同步代码块就不会再执行,因此就可以提高整个代码的执行效率及性能;下面是简单粗暴的方式:
public class SingleObj{
private volatile static SingleObj singleObj = null;
private SingleObj(){}
public synchronized static SingleObj getInstance(){
if(null == singleObj){
singleObj = new SingleObj();
}
return singleObj;
}
}
同步代码块直接加载方法上,虽然也能确保线程安全,但是,这样的实现导致就算单例对象以及被实例化,每次通过getInstance()方法获取对象的时候,都会走加锁获取,代码性能会大大的下降。
public class NumOpe
{
private static int num = ;
public static void m()
{
for (int i = ; i < ; i++){
synchronized (NumOpe.class){
num++;
}
}
}
}
当我们对num进行循环追加的,希望能够保证num的线程安全,不会因为并发导致加错,如果按以上的方式加锁的,每一次循环都会有一个加锁及释放的过程;但是我们的目的是每次for循环追加10个数,因此,我们将锁同步的范围扩大(粗化)到整个操作序列的外部,完全可以对同步代码块进行以下的方式粗化:
public static void m()
{
synchronized (NumOpe.class){
for (int i = ; i < ; i++){
num++;
}
}
}
这样既可以线程安全,同时也不会因为粒度太细而导致性能的下降;类似于StringBuffer的appand方法亦是如此。
public class VolatileTest{
public static void main(String[] args) throws Exception
{
Mythread mythread = new Mythread();
mythread.start();
Thread.sleep();
mythread.stopMe();
}
}
class Mythread extends Thread{
private boolean label = false;
public void stopMe()
{
label = true;
}
@Override
public void run()
{
System.out.println("start");
while (!label)
{
}
System.out.println("stop");
}
}
如上的演示代码,当main方法跑起来之后,是否能够正常退出?答案是只会打印start就陷入到了while的死循环,永远不会退出。但是按代码逻辑来看,线程启动100ms之后就调用了stopMe(),为什么这个停止的没有生效呢?在了解原因之前,我们得先了解一下关于缓存方面的一些知识。
public class VolatileTest2
{
public static volatile int num = ;
public static void incr()
{
num++;
}
private static final int THREAD_NUM = ;
public static void main(String[] args)
{
// 开启20个线程,分别调用incr()对num进行追加
Thread[] threads = new Thread[THREAD_NUM];
for (int i = ; i < THREAD_NUM ; i++)
{
threads[i] = new Thread(() ->
{
for (int j = ; j < ; j++)
{
incr();
}
});
threads[i].start();
}
while (Thread.activeCount() > )
Thread.yield();
System.out.println(num);
}
}
理论上20个线程,每个调用10000次,最后追加的结果应该是20万,由上面可以看出,几乎很小概率能够正确,那么原因出现在哪里?
// 查看这个方法的字节码
public static void incr()
{
num++;
}
// 以下为字节码
getstatic #2 <VolatileTest2.num>
iconst_1
iadd
putstatic #2 <VolatileTest2.num>
return
//上面的字节码可以看出 num++行代码被编译成了4条指令
//getstatic指令将num的值取到栈顶的时候,volatile能保证拿到的值是最新的
//但是在执行iconst_1、iadd这两条指令的时候,没有办法保证其他的线程不做修改
那么可以通过synchronized解决这个问题
// 如果为了保证原子性使用synchronized,就可以不用volatile
public synchronized static void incr(){
num++;
}
public class Disorder
{
static int a = , b = ;
static int x = , y = ;
public static void main(String[] args) throws InterruptedException
{
int i = ;
for ( ; ; )
{
a = b = x = y = ;
i++;
Thread thread1 = new Thread(() ->
{
a = ;
x = b;
});
Thread thread2 = new Thread(() ->
{
b = ;
y = a;
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
if (x == && y == )
{
System.err.println("第" + i + "次异常,X=" + x + " Y=" + y);
break;
}
else
{
System.out.println("第" + i + "次,X=" + x + " Y=" + y);
}
}
}
}
// 上面的代码可以分析得出x和y值可能出现以下情况
// 第一种: x=1,y=0
// 第二种: x=1,y=1
// 第三种: x=0,y=1
// 如果出现x=0 , y=0的时候,说明发生了指令重排
// 以下的测试结果证明了确实发生了重排
// 我们再次回到双重检查的单例
public class SingleObj{
// 这里是否需要加volatile?
private volatile static SingleObj singleObj = null;
private SingleObj(){}
public synchronized static SingleObj getInstance(){
if(null == singleObj){
singleObj = new SingleObj();
}
return singleObj;
}
}
public class T
{
private int i;
public T()
{
i = ;
}
public static void main(String[] args)
{
T t = new T();
}
}
//以下是T t = new T();实例化过程的字节码指令
// 实例化一个T对象 赋初始值 i=0
new #3 <T>
dup
// 初始化变量 i=1
invokespecial #4 <T.<init>>
// 将示例对象映射到t
astore_1
return
// 第一步:实例化对象T,并将变量赋一个初值
// 第二步:初始化变量的值
// 第三步:实例化对象与栈里的引用t之间的建立关联,此时t就不为null
// 如果现在的字节码指令的第二步和第三步发送了重排,执行顺序如下:
new #3 <T>
dup
astore_1
// 假如说单例的第一个线程执行到了这里,另外的并发线程进入了
// 此时堆栈中的t对象以及不为null了,按我们上面的单例方式,就直接返回了
// 但是,这会儿这个对象的值并没有对熟悉进行初始化,对象中i的i值为0
invokespecial #4 <T.<init>>
return
以上DCL指令重排的问题实在是没办法通过测试代码测试复现出来;这种情况确实出现的概率非常的低,也只有在高并发很大的情况下,极低的可能性出现,而且出现之后也非常的难追踪;概率低不代表不会出现,因此,我们在写DCL单例的时候一定要注意这个问题。
通过上面的代码,可以看出,加了volatile和不加在JVM中体现为一个lock addl
指令,加了内存屏障多执行的一条lock addl $0x0,(%rsp)
指令;这个操作类似于一堵墙,在重排序的时候,后面的指令不能重排序到内存屏障之前;当只有一个CPU访问内存的时候,是不需要内存屏障的,但是当多个CPU同时访问同一块内存,且其中一个在观测另外一个,就需要保证一致性;其中addl $0x0,(%rsp)
把rsp寄存器中的值增加0,很显然这个是一个空操作,关键的lock
前缀会使本CPU的Cache写入到主存,该写入当做也会引起其他CPU或者内核无效化(Invalidate)其Cache,这个操作相当于对Cache中的变量做了一次Java内存模型中的“store”和“write”操作。所以通过这样的一个空操作,可以让volatile变量的修改对其他CPU可见
public class T
{
private volatile int num;
private int num2;
public int incr(int a, int b)
{
int temp = a + b;
num += temp;
return temp;
}
public int incr2(int a)
{
num2 += a;
return num2;
}
public int getNum()
{
return num;
}
public static void main(String[] args)
{
T t = new T();
int sum = ;
int sum2 = ;
for (int i = ; i < ; i++)
{
sum = t.incr(sum, );
sum2 = t.incr2(i);
}
System.out.println("num:" + t.getNum());
System.out.println("sum:" + sum);
System.out.println("sum2:" + sum2);
}
}
// 带volatile的在字节码层仅仅表现为访问标识不同