目录
Sleep,Yield,Join
先来以系统层面来解释一下这个几个方法是什么意思。
Sleep: 线程休眠500毫秒, 让出cpu并且500毫秒内不会获取cpu, 500毫秒后有机会得到cpu
Thread.sleep(500);
Yield: 线程让出CPU,让其他线程有机会运行,当然也有可能是当前线程又拿到了cpu 。
或者说线程返回到就绪状态,与其他线程一起再次竞争cpu
Thread.yield();
Join: 让其他进程先执行完成,然后再执行。
// t1中调用 t1.join() 是没有意义的
Thread t1 = new Thread(()->{
System.out.println("A");
})
Thread t2 = new Thread(()->{
try{
t1.join(); // 让t1先运行, t2等待t1运行结束
}catch (Exception e){
e.printStackTrace();
}
System.out.println("B");
})
线程的几种状态
new Thread().start()
之后线程会进入到Runnable
状态而这个状态中还有两个状态Ready
和Running
。拿到cpu资源开始执行则就是Running。在Java线程中将这两个状态都合并成Runnable,因为Java是将线程交给系统去执行的,具体有没有执行什么时候执行是不可知的。当程序执行完成之后就会结束Teminated
状态。
当线程未得到锁而无法执行时就会进入Blocked
阻塞态。
synchronized
首先需要明确synchronized
锁的到底是什么。其实锁的是一个对象。JVM对synchronized
的底层实现其实并没有一个明确的规范, HotSpot VM
的实现就是在对象的头部(64位),记录线程的ID。而前两位是用来记录对象是否被锁定,前两位的组合就是锁的类型(偏向锁,自旋锁,系统锁)。后面会详细介绍
原理大概了解了,先来看一下synchronized的使用
使用方式一:
public class T{
private int count = 10;
private Object o = new Object();
public void m(){
synchronized(o){ // 任何线程要执行下面的代码,必须先拿到o的锁
count--;
System.out.println(Thread.currentThread().getName()+"count="+count);
}
}
}
使用方式二:
下面两种写法是等值的。所以直接直接在方法上加synchronized
其实就是对当前对象加锁!
public class T1{
private int count = 10;
public void m(){
synchronized(this){
count--;
System.out.println(Thread.currentThread().getName()+"count="+count);
}
}
public void n(){count++;}
}
public class T{
private int count = 10;
public synchronized void m(){
count--;
System.out.println(Thread.currentThread().getName()+"count="+count);
}
public void n(){count++;}
}
与static配合使用
这种方式其实就是锁整个类。等同于synchronized(T.class)
public class T{
private int count = 10;
public synchronized static void m(){ // 这里等同于synchronized(T.class)
count--;
System.out.println(Thread.currentThread().getName()+"count="+count);
}
public void n(){count++;}
}
synchronized是可重入锁
synchronized是可重入锁,也必须是可重入锁。在继承中使用super.m1()
,如果synchronized不是重入锁,那么继承就直接死锁了这是不允许的。
/**
在m1()中调用m2(),发现m2()也加锁了且和m1()是相同的锁。所以依然可以调用m2()
*/
public class T {
synchronized void m1(){
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
new T().m1();
}
}
synchronized 异常
如果线程出现异常,默认情况下锁是会被释放的。
/**
* 程序在执行过程中,如果出现异常,默认情况下锁是会被释放的
* 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况
* 比如,在一个web app处理过程中,多个servlet线程共同访问一个资源,这时如果异常处理不合适
* 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问异常产生的数据
* 因此要非常小心的处理同步业务逻辑中的异常
*/
public class T {
int count = 0;
synchronized void m(){
System.out.println(Thread.currentThread().getName()+" start");
while (true){
count++;
System.out.println(Thread.currentThread().getName()+" count="+count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 5){
int i = 1/0; // 此处抛出异常,锁将被释放,想要被不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
T t = new T();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r,"t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r,"t2").start();
}
}
synchronized 的底层实现
JDK早期的实现就是直接使用操作系统的,导致效率非常低。后来进行了改进。
下只讨论HotSpot VM
的实现。
synchronized (Object obj)
锁升级的概念
- 当第一个线程访问该对象时,会在对象头上使用
markword
记录这个线程ID,该线程再次访问该对象时发现,对象头上的线程ID与自己相同则就可以进入。此时,这个锁是自旋锁,如果这个线程的临界区的执行时间超过4秒,且没有其他线程来竞争,那么就会启用偏向锁,这种锁认为是没有其他线程会与其进行竞争锁的。在JVM调优中可以使用-XX:BiasedLockingStartupDelay=4
来设置偏向锁的延时启动时间,默认是4秒。 - 这时有其他线程需要这个对象的锁时,这时锁升级为自旋锁。自旋锁类似于使用一个
while(true)
去循环判断这个线程是否获取到了锁。自旋锁是需要占用cpu资源的 - 如果10次以后还没有获取到锁,再次升级为重量级锁-OS锁。线程进入等待态,不占用cpu资源,等待系统唤醒。
那么效率优先的时候,什么情况下使用自旋锁,什么情况下使用OS锁?
执行时间长,线程数多,使用OS锁,
执行时间短,线程数比较少,用自旋锁
附录
java对象在内存中的存储结构
markword(8bytes) | Class pointer(4bytes) | instance data(可变长度) | padding |
---|---|---|---|
存放3部分信息: 锁、GC、HashCode | 对象类的指针 | 实例数据 | 整体长度要是8bytes的倍数,不足在此处补齐 |
markword结构详解
Hotsport实现markword
中的结构,可以查看当前处于哪种锁的状态。
锁状态 | 25位 | 31位 | 1位 | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
无状态锁(new) | unused | hashCode(如果有调用) | unused | 分代年龄 | 0 | 01 |
锁状态 | 54位 | 2位 | 1位 | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
偏向锁 | 当前现在指针 JavaThread* | Epoch | unused | 分代年龄 | 1 | 01 |
锁状态 | 62位 | 2bit |
---|---|---|
轻量级锁 自旋锁 无锁 | 指向线程栈中Lock Record的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
GC标记信息 | CMS过程用到的标记信息 | 11 |