escaflone的专栏

一、多线程

1、操作系统有两个容易混淆的概念,进程和线程。

进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。

线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源。

2、Java标准库提供了进程和线程相关的API,进程主要包括表示进程的java.lang.Process类和创建进程的java.lang.ProcessBuilder类;

表示线程的是java.lang.Thread类,在虚拟机启动之后,通常只有Java类的main方法这个普通线程运行,运行时可以创建和启动新的线程;还有一类守护线程(damon thread),守护线程在后台运行,提供程序运行时所需的服务。当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。

3、线程间的可见性:一个线程对进程中共享的数据的修改,是否对另一个线程可见

可见性问题:

a、CPU采用时间片轮转等不同算法来对线程进行调度

public class IdGenerator{ private int value = 0; public int getNext(){return value++; }}对于IdGenerator的getNext()方法,在多线程下不能保证返回值是不重复的:各个线程之间相互竞争CPU时间来获取运行机会,CPU切换可能发生在执行间隙。

以上代码getNext()的指令序列:CPU切换可能发生在7条指令之间,多个getNext的指令交织在一起。

aload_0dupgetfield #12dup_x1iconst_1iaddputfield #12b、CPU缓存:

目前CPU一般采用层次结构的多级缓存的架构,有的CPU提供了L1、L2和L3三级缓存。当CPU需要读取主存中某个位置的数据时,会一次检查各级缓存中是否存在对应的数据。如果有,直接从缓存中读取,这比从主存中读取速度快很多。当CPU需要写入时,数据先被写入缓存中,之后再某个时间点写回主存。所以某些时间点上,缓存中的数据与主存中的数据可能是不一致。

c、指令顺序重排

出行性能考虑,编译器在编译时可能会对字节代码的指令顺序进行重新排列,以优化指令的执行顺序,在单线程中不会有问题,但在多线程可能产生与可见性相关的问题。

二、Java内存模型(Java Memory Model)

屏蔽了CPU缓存等细节,只关注主存中的共享变量;关注对象的实例域、静态域和数组元素;关注线程间的动作。

1、volatile关键词:用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。

用来确保对一个变量的修改被正确地传播到其他线程中。

例子:A线程是Worker,一直跑循环,B线程调用setDone(true),A线程即停止任务

public class Worker{ private volatile boolean done; public void setDone(boolean done){this.done = done; } public void work(){while(!done){//执行任务;} }}例子:错误使用。因为没有锁的支持,volatile的修改不能依赖于当前值,当前值可能在其他线程中被修改。(Worker是直接赋新值与当前值无关)public class Counter {public volatile static int count = 0;public static void inc() {//这里延迟1毫秒,使得结果明显try {Thread.sleep(1);} catch (InterruptedException e) {}count++;}public static void main(String[] args) {//同时启动1000个线程,去进行i++计算,看看实际结果for (int i = 0; i < 1000; i++) {new Thread(new Runnable() {@Overridepublic void run() {Counter.inc();}}).start();}//这里每次运行的值都有可能不同,可能不为1000System.out.println("运行结果:Counter.count=" + Counter.count);}}2、final关键词final关键词声明的域的值只能被初始化一次,一般在构造方法中初始化。。(在多线程开发中,final域通常用来实现不可变对象)

当对象中的共享变量的值不可能发生变化时,在多线程中也就不需要同步机制来进行处理,故在多线程开发中应尽可能使用不可变对象。

另外,在代码执行时,final域的值可以被保存在寄存器中,而不用从主存中频繁重新读取。

3、java基本类型的原子操作

1)基本类型,引用类型的复制引用是原子操作;(即一条指令完成)

2)long与double的赋值,引用是可以分割的,非原子操作;

3)要在线程间共享long或double的字段时,必须在synchronized中操作,或是声明成volatile

三、Java提供的线程同步方式

1、synchronized关键字

方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)

所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。

a、静态方法:Java类对应的Class类的对象所关联的监视器对象。

b、实例方法:当前对象实例所关联的监视器对象。

c、代码块:代码块声明中的对象所关联的监视器对象。

注:当锁被释放,对共享变量的修改会写入主存;当活得锁,CPU缓存中的内容被置为无效。编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。

例:以下方法getNext()和getNextV2() 都获得了当前实例所关联的监视器对象

public class SynchronizedIdGenerator{ private int value = 0; public synchronized int getNext(){return value++; } public int getNextV2(){synchronized(this){return value++;} }}

2、Object类的wait、notify和notifyAll方法

生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。

在泪水中浸泡过的微笑最灿烂,从迷惘中走出来的灵魂最清醒。

escaflone的专栏

相关文章:

你感兴趣的文章:

标签云: