一、多线程
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++; } }</div>
对于IdGenerator的getNext()方法,在多线程下不能保证返回值是不重复的:各个线程之间相互竞争CPU时间来获取运行机会,CPU切换可能发生在执行间隙。
以上代码getNext()的指令序列:CPU切换可能发生在7条指令之间,多个getNext的指令交织在一起。
aload_0 dup getfield #12 dup_x1 iconst_1 iadd putfield #12</div>
b、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){ //执行任务; } } }</div>
例子:错误使用。因为没有锁的支持,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() { @Override public void run() { Counter.inc(); } }).start(); } //这里每次运行的值都有可能不同,可能不为1000 System.out.println("运行结果:Counter.count=" + Counter.count); } }</div>
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++; } } }</div>
2、Object类的wait、notify和notifyAll方法
生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。
wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。
notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。
notifyAll:将对象中等待池中的线程,全部放入锁池。
(notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,不过能保证程序的正确性)
工作流程:
a、Consumer线程A 来 看产品,发现产品为空,调用产品对象的wait(),线程A进入产品对象的等待池并释放产品的锁。
b、Producer线程B获得产品的锁,执行产品的notifyAll(),Consumer线程A从产品的等待池进入锁池,Producer线程B生产产品,然后退出释放锁。
c、Consumer线程A获得产品锁,进入执行,发现有产品,消费产品,然后退出。
例子:
public synchronized String pop(){ this.notifyAll();// 唤醒对象等待池中的所有线程,可能唤醒的就是 生产者(当生产者发现产品满,就会进入对象的等待池,这里代码省略,基本略同) while(index == -1){//如果发现没产品,就释放锁,进入对象等待池 this.wait(); }//当生产者生产完后,消费者从this.wait()方法再开始执行,第一次还会执行循环,万一产品还是为空,则再等待,所以这里必须用while循环,不能用if String good = buffer[index]; buffer[index] = null; index--; return good;// 消费完产品,退出。 }</div>
注:wait()方法有超时和不超时之分,超时的在经过一段时间,线程还在对象的等待池中,那么线程也会推出等待状态。
3、线程状态转换: