并发核心理论
1.共享性: 数据共享是线程安全问题的主要问题之一
2.互斥性: 资源互斥指同一时间内值允许一个访问者访问.通常情况修改数据是互斥性,读数据不要求.因此我们有共享锁和互斥锁.也叫读锁和写锁.
-
3.原子性:指操作是一个独立,不可分割的整体.操作不会中断,数据不会执行一半的时候被其他操作修改.例如一条指令就是最基本的原子.但是 i++,就分了好几次操作:
读取i=1值 --> 2.i=1+1 --->3.存储i=2
-
4.可见性:
每个线程都有自己的工作内存,对于共享变量,先拿到变量的副本,对副本进行操作,在某一个时间在把副本的值同步到共享变量.这样导致,有可能线程1修改了共享变量的值,某一时间线程2可能拿不到最新的值.
5.有序性:为了提高性能,编译器和处理器可能会对指令进行重排.
synchronized及其实现原理
synchronized 作用:
- 1.确保线程互斥的访问同步代码
- 2.保证共享变量能及时可见
- 3.解决重排序问题
synchronize 的一般用法,对象锁,类锁.(synchronized Object,synchronized 方法)
对class文件反编译后的汇编语言如下:
说明synchronized 是在对象前面加了 monitor .
Monitorenter : 对象都享有一个monitor
1.线程一旦进入monitor,monitor值1,线程为monitor拥有者.
2.其他线程到了这里,会进行阻塞,直到monitor的值=0.
Monitorexit:monitor 变回0.说明其他线程可以拥有.
对方法加synchronized
Synchronized底层优化(偏向锁、轻量级锁)
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行时间较长。 |
锁的状态: 无锁状态,偏向锁 ,轻量级锁,重量级锁
随着锁的竞争,锁的状态可以升级,但是是单向的.从低到高,不会出现锁的降级.
偏向锁<轻量级锁<重量级锁
轻量级锁:本意使用操作系统互斥量解决传统的重量级锁的性能问题.
使用场景:线程交替执行同步块.如果同一时刻访问同一锁的情况,轻量级锁会升级重量级锁.
偏向锁:用于一个线程执行同步块是提高性能.一旦出现多线程竞争,升级为轻量级锁.
总结 :JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行时间较长。 |
Java并发编程:线程间的协作(wait/notify/sleep/yield/join)
线程有5中状态:
新建(New)-->准备状态(Runnable)-->运行状态(Running)-->阻塞(Blocking)-->死亡(Dead)
new :创建 new Thread();
Runnable:调用start(),进入就绪状态,等待cpu分配资源,有系统运行时线程来调度
Running:开始执行Run方法
Blocking: 阻塞
Dead
Wait/notify/notifyAll()
wait() 当前线程挂起,直到有其他线程调用notify(), notifyAll().
wait(long timeout) 当前线程挂起,直到有其他线程调用notify(), notifyAll();或者等到timeout
wait(long timeout,int nanos)
notify() 唤醒指定线程 , notifyAll() 唤醒所有的线程
注意:wait 必须在synchronized 块之内.
Thread.sleep(long sleeptime)
当前线程暂停指定的时间(毫秒),而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁.而wait方法则需要释放锁,(意味其他线程可以拿到锁,进行操作)
package jni.test.leon.javatest;
/**
* Created by leon on 17-12-11.
*/
public class SleepTest {
public synchronized void sleepMethod() {
System.out.println("Sleep start-----");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Sleep end-----");
}
public void waitMethod() {
System.out.println("Wait start-----");
synchronized (this) {
try {
wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Wait end-----");
}
public static void main(String[] args) {
final SleepTest test1 = new SleepTest();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
test1.sleepMethod();
}
}).start();
}
try {
Thread.sleep(10000);//暂停十秒,等上面程序执行完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----分割线-----");
final SleepTest test2 = new SleepTest();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
test2.waitMethod();
}
}).start();
}
}
}
//output-----
Connected to the target VM, address: '127.0.0.1:38691', transport: 'socket'
Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
-----分割线-----
Wait start-----
Wait start-----
Wait start-----
Wait end-----
Disconnected from the target VM, address: '127.0.0.1:38691', transport: 'socket'
Wait end-----
Wait end-----
Process finished with exit code 0
Thread.yeild()
当前线程暂停,让其他线程有机会执行.(用的场景比较少,主要是调试)
package jni.test.leon.javatest;
/**
* Created by leon on 17-12-11.
*/
public class YieldTest implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
Thread.yield();
}
}
public static void main(String[] args) {
YieldTest runn = new YieldTest();
Thread t1 = new Thread(runn, "FirstThread");
Thread t2 = new Thread(runn, "SecondThread");
t1.start();
t2.start();
}
}
//--output
FirstThread: 0
FirstThread: 1
SecondThread: 0
FirstThread: 2
SecondThread: 1
FirstThread: 3
SecondThread: 2
FirstThread: 4
SecondThread: 3
SecondThread: 4
Thread.join()/Thread.join(long waittime)/Thread.join(long waittime,int nano)
作用:父线程等待子线程执行完之后在执行,可以达到异步子线程,最后的同步.
package jni.test.leon.javatest;
/**
* Created by leon on 17-12-11.
*/
public class JoinTest implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " start-----");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " end------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread test = null;
for (int i = 0; i < 5; i++) {
test = new Thread(new JoinTest());
test.start();
}
//这里是阻塞
try {
test.join(); //调用join方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Finished~~~");
}
}
//output------
Thread-0 start-----
Thread-1 start-----
Thread-2 start-----
Thread-3 start-----
Thread-4 start-----
Thread-0 end------
Thread-1 end------
Thread-2 end------
Thread-3 end------
Thread-4 end------
Finished~~~
总结:
因为wait() /notify () 是对象monitor,所以当wait的时候就monitorEnter,必须等待有monitorExit 才能释放对象的拥有权.所以,一旦wait 了,其他的线程是无法获得拥有权,也就不能进入.
而sleep, yield这些是Thread级别的方法,只是让出cpu执行权join,调用的是wait 方法,所以是会进行对象级别的monitor.
volatile的使用及其原理
我们知道 可见性,有序性,原子性 的问题.synchronized 就是用来解决这些问题的,但是synchronized是比较重量级,volatile是一个轻量级的解决有序性,可见性,原子性的方案.
原理:
1.对有序性:
在解释这个问题前,我们先来了解一下Java中的happen-before规则,JSR 133中对Happen-before的定义如下:
Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.
通俗一点说就是如果a happen-before b,则a所做的任何操作对b是可见的。(这一点大家务必记住,因为happen-before这个词容易被误解为是时间的前后)。我们再来看看JSR 133中定义了哪些happen-before规则:
- Each action in a thread happens before every subsequent action in that thread.
- An unlock on a monitor happens before every subsequent lock on that monitor.
- A write to a volatile field happens before every subsequent read of that volatile.
- A call to start() on a thread happens before any actions in the started thread.
- All actions in a thread happen before any other thread successfully returns from a join() on that thread.
- If an action a happens before an action b, and b happens before an action c, then a happens before c.
翻译过来为:
- 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
- 监视器上的解锁操作 happen-before 其后续的加锁操作.(Synchronized 规则)
- 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
- 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
- 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
- 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。
这里我们主要看下第三条:volatile变量的保证有序性的规则<<Java并发编程:核心理论>>
2.可见性
使用 Volatile 关键字
1.修改时会强制修改主内存的数据
2.修改变量后导致其他线程工作中内存的值失效,所以需要重新读取主内存的数据
volatile 的使用场景比较有限:
1.该变量写操作不依赖当前值
2.该变量没有包含在具有其他变量的不变式中(这个变量只有原子性操作,简单说来就是 进行赋值)
常用:
1.状态标记量
A:
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
B:
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2.double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}