- 并发编程三要素
- 原子性:即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
- 可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获 取到最新的值。
- 悲观锁与乐观锁
- 悲观锁:每次操作都会加锁,会造成线程阻塞。
- 乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
- Thread.States类中定义线程的状态如下:
- NEW:Thread对象已经创建,但是还没有开始执行。
- RUNNABLE:Thread对象正在Java虚拟机中运行。
- BLOCKED : Thread对象正在等待锁定。
- WAITING:Thread 对象正在等待另一个线程的动作。
- TIME_WAITING:Thread对象正在等待另一个线程的操作,但是有时间限制。
- TERMINATED:Thread对象已经完成了执行。
- 线程的状态
- 创建状态:准备好了一个多线程的对象,叫做Treade t = new Treade()
- 就绪状态:调用了start()方法,等待CPU进行调度
- 运行状态:执行run()方法
- 阻塞状态:暂时停止执行,可能将资源交给其它线程使用
- 终止状态(死亡状态):线程执行完毕(run方法结束),不再使用了
- 同步代码块和同步方法有什么区别?
- 同步方法使用的锁是固定的this ;同步代码块使用的锁可以是任意对象
- 如果同步方法被static修饰了,使用的锁就不是this了,而是字节码文件对象(字节码文件对象:类名.class)因为静态方法和类同时加载,此时没有创建类的对象(没有new),只有Class对象(Ticket.class)
- wait()和notify()
- wait()会让线程处于等待状态,其实就是把线程临时存储到等待池中。
- notify()会唤醒等待池中的任意一个等待的线程,唤醒之后就具备了执行资格。
- notifyAll()会唤醒等待池中所以的等待的线程
- 唤醒之后并不能立即运行,还需要拿到同步的锁
- 这些方法使用在同步中,因为必须要标识wait()、notify()所属的锁。
- 同一个锁上的notify,只能唤醒该锁上被wait的线程
- 为什么这些方法定义在Object类中?因为这些方法必须标识所属的锁,而锁可以是任意对象,任意对象可以调用的方法必须是Object中的方法
- sleep() 和 wait() 的异同点?
相同点:可以让线程处于冻结状态
不同点:
- sleep()必须指定时间,wait()可以指定时间,也可以不指定时间
- sleep()不一定非要定义在同步中;wait()必须定义在同步中
- 类的不同:sleep() 来自 Thread,wait() 来自 Object。
- 释放锁:sleep() 不释放锁;wait() 释放锁。
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
- JMM(Java内存模型)
线程之间的共享变量存在主内存中,每个线程都有⼀个私有的本地内存,存储了该线程以读、写共享变量的副本。本地内存是Java内存模型的⼀个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。
从图中可以看出:
- 所有的共享变量都存在主内存中。
- 每个线程都保存了⼀份该线程使⽤到的共享变量的副本。
- 如果线程A与线程B之间要通信的话,必须经历下⾯2个步骤:
- 线程A将本地内存A中更新过的共享变量刷新到主内存中去。
- 线程B到主内存中去读取线程A之前已经更新过的共享变量。
所以,线程A⽆法直接访问线程B的⼯作内存,线程间通信必须经过主内存。
注意,根据JMM的规定,线程对共享变量的所有操作都必须在⾃⼰的本地内存中进⾏,不能直接从主内存中读取。
所以线程B并不是直接去主内存中读取共享变量的值,⽽是先在本地内存B中找到这个共享变量,发现这个共享变量已经被更新了,然后本地内存B去主内存中读取这个共享变量的新值,并拷⻉到本地内存B中,最后线程B再读取本地内存B中的新值。
那么怎么知道这个共享变量的被其他线程更新了呢?
这就是JMM的功劳了,也是JMM存在的必要性之⼀。JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可⻅性保证。
JMM和Java运⾏时内存区域的划分,这两者既有差别⼜有联系:
区别:
两者是不同的概念层次。JMM是抽象的,他是⽤来描述⼀组规则,通过这个规则来控制各个变量的访问⽅式,围绕原⼦性、有序性、可⻅性等展开的。⽽Java运⾏时内存的划分是具体的,是JVM运⾏Java程序时必要的内存划分。
- 联系
都存在私有数据区域和共享数据区域。⼀般来说,JMM中的主内存属于共享数据区域,他是包含了堆和⽅法区;同样,JMM中的本地内存属于私有数据区域,包含了程序计数器、本地⽅法栈、虚拟机栈。
- 内存可⻅性
- JMM有⼀个主内存,每个线程有⾃⼰私有的⼯作内存,⼯作内存中保存了⼀些变量在主内存的拷⻉。
- 内存可⻅性,指的是线程之间的可⻅性,当⼀个线程修改了共享变量时,另⼀个线程可以读取到这个修改后的值。
- 重排序
为优化程序性能,对原有的指令执⾏顺序进⾏优化重新排序。重排序可能发⽣在多个阶段,⽐如编译重排序、CPU重排序等。
- happens-before规则
是⼀个给程序员使⽤的规则,只要程序员在写代码的时候遵循happens-before规则,JVM就能保证指令在多线程之间的顺序性符合程序员的预期。
- happens-before关系的定义如下:
- 如果⼀个操作happens-before另⼀个操作,那么第⼀个操作的执⾏结果将对第⼆个操作可⻅,⽽且第⼀个操作的执⾏顺序排在第⼆个操作之前。
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执⾏。如果重排序之后的执⾏结果,与按happens-before关系来执⾏的结果⼀致,那么JMM也允许这样的重排序。
- happen-before规则总结
- 单线程中的每个操作,happen-before于该线程中任意后续操作。
- 对volatile变量的写,happen-before于后续对这个变量的读。
- 对synchronized的解锁,happen-before于后续对这个锁的加锁。
- 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的 读。
四个基本规则再加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外的部分,程序都可能被重排序,都需要开发者小心地处理内存可见性问题。
- 缓存一致性问题
java i = i + 1;
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
- volatile的内存语义
在Java中,volatile关键字有特殊的内存语义。volatile主要有以下两个功能:
- 保证可见性;
- 64位写入的原子性,不保证复合操作的原子性;(在Java中,对基本数据类型的变量和赋值操作都是原子性操作;)
- 禁止指令重排。
所谓内存可⻅性,指的是当⼀个线程对 volatile 修饰的变量进⾏写操作时,JMM会⽴即把该线程对应的本地内存中的共享变量的值刷新到主内存;当⼀个线程对 volatile 修饰的变量进⾏读操作时,JMM会把⽴即该线程对应的本地内存置为⽆效,从主内存中读取共享变量的值。
- 说说 synchronized 关键字和 volatile 关键字的区别?
- volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile仅仅保证对单个volatile变量的读/写具有原⼦性,⽽锁可以保证整个临界区代码的执⾏具有原⼦性。所以在功能上,锁⽐volatile更强⼤;在性能上,volatile更有优势。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
在保证内存可⻅性这⼀点上,volatile有着与锁相同的内存语义,所以可以作为⼀个“轻量级”的锁来使⽤。