【Java多线程】基础知识二、java线程的几种状态,synchronized锁注意点以及原理详解

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状态而这个状态中还有两个状态ReadyRunning。拿到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位4bit1bit2bit
无状态锁(new)unusedhashCode(如果有调用)unused分代年龄001
锁状态54位2位1位4bit1bit2bit
偏向锁当前现在指针 JavaThread*Epochunused分代年龄101
锁状态62位2bit
轻量级锁 自旋锁 无锁指向线程栈中Lock Record的指针00
重量级锁指向互斥量(重量级锁)的指针10
GC标记信息CMS过程用到的标记信息11
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值