JUC笔记(二)

打断park的线程:

 public static void main(String[] args) throws InterruptedException {
    
    

    Thread myThread = new Thread(()->{
    
    

        System.out.println("park---->");
        LockSupport.park();
        System.out.println("unpark---->");
        System.out.println("打断状态:" + Thread.currentThread().isInterrupted());
    });

    myThread.start();
    Thread.sleep(1000);
    myThread.interrupt();
    }

输出:

park---->
unpark---->
打断状态:true

park中的线程被打断后,打断状态为true,注意:此时打断状态为true,在myThread线程中继续调用park将不会进入阻塞,如果想调用park就进入阻塞,则可以调用Thread.interrupted()清除打断标记。

public static void main(String[] args) throws InterruptedException {
    
    

    Thread myThread = new Thread(()->{
    
    

        System.out.println("park---->");
        LockSupport.park();
        System.out.println("unpark---->");
        System.out.println("打断状态:" + Thread.interrupted()); //注意此处修改为Thread.interrupted()
        LockSupport.park();
        System.out.println("我被park了吗????");
    });

    myThread.start();
    Thread.sleep(1000);
    myThread.interrupt();
}

输出:

park---->
unpark---->
打断状态:true

可以看到最后一句我被park了吗????没有输出,说明被park住了

不再建议使用的方法

  • stop(): 强制停止线程
  • suspend(): 挂起(暂停)线程的运行
  • resume(): 恢复线程运行
    以上3个方法比较暴力,均会破坏同步代码块,不建议使用

主线程和守护线程

JVM中所有非守护线程运行完成即退出,即使守护线程未运行完,未运行完的守护线程会被强制结束。守护线程例子:垃圾回收线程。Tomcat的Acceptor线程和Poller线程也是守护线程,接收到shutdown命令后会被直接结束。

线程的状态

操作系统中的状态

  • 初始状态:刚创建完成
  • 可运行状态: 等待CPU调度
  • 运行状态: 正在运行
  • 终止状态:线程代码运行完成
  • 阻塞状态:让出CPU,等待其它条件完成,如果不被唤醒,永远不会醒

JAVA中的六种状态

  • NEW:新建状态,每调用start()方法,操作系统中的初始状态
  • RUNNABLE:操作系统中的运行,可运行,阻塞(读取文件,调用线程API等)
  • BLOCKED:等待满足条件,比如锁
  • WAITING:等待其它线程运行完,比如join属于WAITING状态
  • TIMED_WAITIING:Thread.sleep(long n)
  • TERMINATED:终止状态

以下代码说明6种状态

public static void main(String[] args) throws InterruptedException {
    
    

    Thread t1 = new Thread("t1"){
    
    
        @Override
        public void run() {
    
    
            System.out.println("running...");
        }
    };

    Thread t2 = new Thread("t2"){
    
    
        @Override
        public void run() {
    
    
            int i =0;
            while (true){
    
    
                i++;
            }
        }
    };
    t2.start();

    Thread t3 = new Thread("t3"){
    
    
        @Override
        public void run() {
    
    
            System.out.println(this.getName() + " running...");
        }
    };
    t3.start();

    Thread t4 = new Thread("t4"){
    
    
        @Override
        public void run() {
    
    
            synchronized (lock){
    
    
                try {
    
    
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    };
    t4.start();

    Thread t5 = new Thread("t5"){
    
    
        @Override
        public void run() {
    
    
            try {
    
    
                t2.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    };
    t5.start();

    Thread t6 = new Thread("t6"){
    
    
        @Override
        public void run() {
    
    
            synchronized (lock){
    
    
                try {
    
    
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    };
    t6.start();

    Thread.sleep(1000);

    System.out.println("t1: " + t1.getState());
    System.out.println("t2: " + t2.getState());
    System.out.println("t3: " + t3.getState());
    System.out.println("t4: " + t4.getState());
    System.out.println("t5: " + t5.getState());
    System.out.println("t6: " + t6.getState());
}

输出:

t3 running...
t1: NEW
t2: RUNNABLE
t3: TERMINATED
t4: TIMED_WAITING
t5: WAITING
t6: BLOCKED

分析:

  1. t1线程创建了,但是没有调用start方法,因此是新建状态
  2. t2是一个无线循环,会一直循环下去,因此是运行状态
  3. t3打印完一行日志后代码运行完成,主线程睡眠1秒后t3线程执行完成了,所以是终止状态
  4. t4获取锁后sleep,是一个有限的等待,因此是TIMED_WAITING
  5. t5在等t2运行完成才会继续执行,由于t2是无线循环,因此t5一直在等t2完成,此时t5是WAITING
  6. t6和t4竞争同一个锁,t4拿到了这个锁未释放,t6在等待获取这个锁,因此是BLOCKED状态

synchronized

使用对象锁保证临界区的原子性
synchronized加在成员方法上,锁住的是this对象
synchronized加在静态方法上,锁住的是当前所在对象的类对象(Xxxx.class)

变量的线程安全

成员变量和静态变量线程安全

  • 如果它们没有共享,则线程安全
  • 如果他们共享了,则根据它们的状态是否改变分析:
    • 如果只有读操作,则线程安全
    • 如果有读写操作,这一部分是临界区,需要考虑线程安全

局部变量线程安全

  • 局部变量是线程安全的
  • 但局部变量的引用未必:
    • 如果该对象没有逃离方法的作用域访问,则线程安全
    • 如果该对象逃离了方法的作用域访问,则需要考虑线程安全

常见的线程安全类

  • String 内部无法修改,属于不可变类
  • Integer等包装类 内部无法修改,属于不可变类
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent包下面的类

注意:以上类每个方法内部代码执行都是原子的,但多个方法组合起来不一定

Monitor 监视器或者管程

java对象头

以32位虚拟机为例:

普通对象:
|--------------------------------------------|
|       Object Header (64 bits)              |
|--------------------------------------------|
| Mark Word(32 bit2)| Class Word (32 bits)   |
|--------------------------------------------|

数组对象:
|---------------------------------------------------------------|
|                 Object Header (64 bits)                       |
|---------------------------------------------------------------|
| Mark Word(32 bit2)|Class Word(32 bits)|array length(32 bits)  |
|---------------------------------------------------------------|

其中Mark Word结构为:
在这里插入图片描述
Mark Word:

  1. 第二行hashcode是对象hash码,age为分代年龄,biased代表是否是偏向锁,最后两位是加锁状态,Normal是对象的正常状态
  2. 第三行是偏向锁时的Mark word
  3. 第四行是轻量级锁时的Mark word
  4. 第五行是重量级锁时的Mark word,当一个object关联到一个monitor(操作系统持有)时,ptr_to_heavyweight_monitor会存储一个关联到的Monitor的指针,然后后面两位会修改为10,此时的hashcode和分代年龄等数据放入了Monitor中,解锁后再把数据还原
  5. 第六行是当前对象被标记为垃圾回收时的Mark word
    用一个图说明一下:
    在这里插入图片描述

上图中Owner存储的是线程ID,哪个线程获取到了锁,Owner就存储这个线程的ID。

锁分类

  1. 轻量级锁:多个线程会访问同一代码块,但运行时间是错开的,即使加了synchronized但不存在锁竞争,可以用轻量级锁来优化此种情形。
    在这里插入图片描述

加轻量级锁时,会先在线程栈中生成一个Lock Record,让Lock Record中的对象引用(Object reference)指向对象,然后尝试使用CAS替换Object对象的Mark Word,将Mark Word的值存入锁记录(此时需要存入的值中最后两位就是00)。加锁成功后:
在这里插入图片描述

如果加锁不成功,可能有两种情况,一种是重入锁,当前线程本次操作不是第一次对这个对象加锁,此时也会生成一个新的Lock Record,数据不再存储Mark Word,而是null,此时有多少个Lock Record就代表重入了几次。另一种情况是出现了锁竞争,那么就进入锁膨胀流程。
解锁过程:如果Lock Record数据区有为null的记录,那么存在重入锁,直接删除一条Lock Record即可,如果没有了数据为null的Lock Record,说明是最初的那条加锁记录,此时使用CAS将Mark Word写回Object Header,删除锁记录,即可解锁。如果CAS失败了,说明由其它线程进入了锁膨胀流程,那么进入重量级锁解锁流程。
2. 锁膨胀
Thread-1持有Object的轻量级锁,此时Thread-2也对Object加锁,发现Object已经是轻量级锁了,这时出现的锁竞争,然后进行锁膨胀,过程如下
a. 先申请一个Monitor,然后锁,然后Object的Mark Word指向Monitor地址,最后两位改为01,Monitor的Owner地址设置为Thread-1的线程id
b. Thread-2自己加入到Monitor的EntryList进行等待
膨胀前:
在这里插入图片描述

膨胀后:
在这里插入图片描述
3. 重量级锁
重量级锁是有了竞争,并且有线程进入entrylist等待唤醒。
当Thread-1解锁时使用CAS还原Lock Record到Object的Mark Word,此时会失败,因为Object的Mark Word此时已经是Monitor的地址了,最后两位也修改成01了,这时就Thread-1就进入重量解锁流程,即将Ower清空,将EntryList中的线程唤醒。

  1. 自旋优化
    重量级锁在获取Monitor不成功之后会进行自旋重试n次,如果此时其它线程释放锁,当前线程就能不进入阻塞队列获取到锁,避免2次上下文切换。这种优化适合多核CPU条件下,自旋也会占用CPU,单核CPU纯属浪费资源。如果自旋没有成功获取锁,就会进入EntryList,Java6后自适应,如果自旋能获取到锁的概率较大,会增加自旋次数,反之降低。Java7以后无法控制是否启用自旋功能。

  2. 偏向锁
    轻量级锁每次发生锁重入的时候,都会使用CAS操作(检查),针对这种情况,可以使用偏向锁优化。第一次加锁时使用ThreadId替换Mark Word,后续都是直接检查是否是自己的ThreadId,不使用CAS操作。是否启用偏向锁在Mark Word的倒数第三位存储,0代表无,1代表使用了了偏向锁。默认偏向锁开启,但不会再程序启动时立即生效,要程序启动后一段时间后才会生效,若想立即生效,需要启动时加参数。
    撤销偏向状态:
    a. 当一个已经加了偏向锁的对象调用了了hasCode()方法后,会撤销偏向锁,原因是加偏向锁时Mark Word存入了线程Id用掉了23位,无法存储下25位的hashCode,因此智能撤销偏向锁,还原HashCode。
    b. 当有其它线程也加偏向锁时,这时会升级成轻量级锁,偏向锁被撤销
    a. 当一个已经加了偏向锁的对象调用了了hasCode()方法后,会撤销偏向锁,原因是加偏向锁时Mark Word存入了线程Id用掉了23位,无法存储下25位的hashCode,因此智能撤销偏向锁,还原HashCode。
    c. 调用wait()/notify(),这两个是重量级锁用的,用到的话会把锁升级为重量级锁,自然就撤销了偏向锁
    批量重偏向:
    虽然被多个线程访问,但是没有竞争,这时偏向Thread-1的线程仍然有机会偏向Thread-2,重偏向会修改偏向锁中的ThreadId,默认情况下,当撤销偏向锁20次以后会发生重偏向
    批量撤销:
    当撤销偏向锁40次以后JVM认为不应该使用偏向锁,会将所有偏向锁撤回,不可偏向

  3. 锁消除

    当一个锁不会逃逸出当前方法时,JVM会将其消除,如以下代码,两个方法运行1000w次,时间相差无几,就是JVM将method2的synchronized优化掉了。

    public int x;
    
    public void method1(){
          
          
      x++;
    }
    
    public void method2(){
          
          
      Object o = new Object();
      synchronized (o){
          
          
        x++;
      }
    }
    

猜你喜欢

转载自blog.csdn.net/u013014691/article/details/122654597