并发线程基础第二篇

目录

线程六种状态

线程间同步与互斥

共享问题

共享带来的问题(java体现)

问题分析

解决方案

synchronized

概念

语法

注意点

方法上的synchronized

线程安全分析

成员变量和静态变量是否线程安全?

局部变量是否线程安全的呢?

常见的线程安全类

Monitor

概念

加锁流程

synchronized原理进阶

轻量级锁

锁膨胀

自旋优化 

偏向锁

锁消除


线程六种状态

  • NEW
  • RUNNABLE
  • TERMINATED
  • TIMED_WAITING
  • WAITING
  • BLOCKED

示例

public class Test5 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> log.debug("我是线程一"));//未调用start,一直处于NEW状态

        Thread t2 = new Thread(() ->{ while(true){} });//RUNNABLE状态,就绪状态,可能被分到时间片,也可能分不到
        t2.start();

        Thread t3 = new Thread(()-> log.debug("我是线程三"));//TERMINATED,终止状态
        t3.start();

        Thread t4 = new Thread(() ->{
                synchronized (Test5.class){
                    try {
                        Thread.sleep(100000);//timed_waiting(有时限的等待)
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        t4.start();

        Thread t5 = new Thread(()-> {
                try {
                    t2.join();//waiting(没有时限的等待)
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        t5.start();

        Thread t6 = new Thread(()-> {
                synchronized (Test5.class){
                    try {
                        Thread.sleep(100000);//拿不到锁,blocked阻塞状态
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        t6.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("t1 state{}",t1.getState());
        log.debug("t2 state{}",t2.getState());
        log.debug("t3 state{}",t3.getState());
        log.debug("t4 state{}",t4.getState());
        log.debug("t5 state{}",t5.getState());
        log.debug("t6 state{}",t6.getState());
    }
}

09:35:09.699 [main] DEBUG com.example.com.yunlong.test1.Test5 - t1 state是NEW
09:35:09.700 [main] DEBUG com.example.com.yunlong.test1.Test5 - t2 state是RUNNABLE
09:35:09.700 [main] DEBUG com.example.com.yunlong.test1.Test5 - t3 state是TERMINATED
09:35:09.700 [main] DEBUG com.example.com.yunlong.test1.Test5 - t4 state是TIMED_WAITING
09:35:09.700 [main] DEBUG com.example.com.yunlong.test1.Test5 - t5 state是WAITING
09:35:09.700 [main] DEBUG com.example.com.yunlong.test1.Test5 - t6 state是BLOCKED

线程间同步与互斥

共享问题

共享带来的问题(java体现)

两个线程对初始值为0的静态变量,一个做自增2000次,一个做自减2000次,结果为0吗?

public class Test6 {
    static int ans =0;

    public static void main(String[] args) throws InterruptedException {
        Thread add = new Thread(()-> {
            for (int i = 0; i < 2000; i++) {
                ans+=1;
            }
        });
        Thread remove = new Thread(()-> {
            for (int i = 0; i < 2000; i++) {
                ans-=1;
            }
        });
        add.start();
        remove.start();
        add.join();
        remove.join();
        System.out.println(ans);
    }
}

经过多次运行,我们发现结果并不是我们期望中的0,反而是什么样的数值都有。 

问题分析

  • 一个程序运行多个线程本身没有问题
  • 问题出在多个线程访问共享资源        
  1. 多个线程读共享资源其实也没有什么问题
  2. 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

解决方案

为了避免临界区竞态条件发生,有多种手段可以可以采取。分为两种:

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized

概念

synchronized是 Java 中用于实现线程同步的关键字,它可以修饰代码块或方法,用于控制对共享资源的访问,保证多个线程之间的安全性和正确性。

synchronized俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获得这个对象锁时就会被阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

语法

synchronized (obj) {
    // 访问共享资源的代码
}

注意点

两个线程t1和t2,如果t1 synchronized(obj1),t2 synchronized(obj2),会怎么运行呢?把锁想象称一个房子,锁对象就是一把钥匙,钥匙不同,房子不同,如果操作同一静态变量,还是会造成线程不安全的问题。

如果t1 synchronized(obj) 而t2没有加锁,此时t2不会被锁住,t2线程不会进入blocked状态。也会造成线程不安全的问题。

方法上的synchronized

class Test{
    public synchronized void test(){}
}
//等价
class T{
    public void test(){
        synchronized (this){

        }
    }
}

方法上的 synchronized并不是锁方法,也是锁对象,只是锁住this对象。上面代码中两个类是等价的。

class Test{
    public synchronized static void test(){}
}
//等价
class T{
    public void test(){
        synchronized (T.class){

        }
    }
}

 静态方法上的synchronized并不是锁方法,也是锁对象,只是锁住类对象。上面代码中两个类是等价的。

对应的练习可以去搜索一下线程八锁。加深一下对synchronized的理解。

线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则是线程安全的
  • 如果它们被共享了,根据它们的状态是否能够改变,又分为两种情况
  •         如果只有读操作,则线程安全
  •         如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全的呢?

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

常见的线程安全类

String,Integer(等一些包装类)

StringBuffer(拼接字符串)

Random(获取随机数)

Vector(线程安全的list集合)

HashTable(线程安全的map集合)

java.util.concurrent包下的类

这里的线程安全说的是,多个线程调用它们同一个实例的某个方法时,是线程安全的。

注意:它们的每个方法是原子的,但是它们多个方法的组合不是原子的

 Hashtable hashtable = new Hashtable();

        if(hashtable.get("key")==null){
            hashtable.put("key","value");
        }//线程不安全

Monitor

概念

monitor被翻译成监视器或者管程

加锁流程

每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的Mark Word(可以去搜一下具体的java对象头底层是什么)中就被设置指向Monitor对象的指针。

  •  刚开始Monitor对象中Owner为null
  • 当Thread-2执行synchronized(obj)就会将Monitor的Owner设置为Thread-2,一个Monitor对象中只能有一个Owner
  • 在Thread-2上锁的过程中,如果有后续的线程也来执行synchronized(obj) ,就会进入Monitor对象中的EntryList Blocked
  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的

注意:

  • synchronized必须是进入同一对象的monitor才有上述的效果,不同的锁对象对应不同的monitor
  • 不加synchronized的对象不会关联监视器,不遵从以上原则   

synchronized原理进阶

轻量级锁

使用场景:如果一个对象如果有多线程访问,但是访问时间是错开的(没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然还是synchronized。

加锁解锁内部流程

static final Object obj = new Object();
    public static void method1(){
        synchronized (obj){
            //同步代码块一
            method2();
        }
    }
    public static void method2(){
        synchronized (obj){
            //同步代码块二
        }
    }

 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

让锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录中

 

如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,图示如下

 

 如果cas失败,有两种情况

  •         如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
  •         如果是自己执行了synchronized锁重入,那么再添加一条Lock Recode作为冲入的计数

当退出synchronized代码块(解锁)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

 

 当退出ynchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

  •         成功,则解锁成功
  •         失败,说明轻量级锁进行了锁膨胀或者已经升级为重量级锁,进入重量级锁解锁流程。

锁膨胀

概念:如果在尝试加轻量级锁的过程中,cas操作无法成功,这时一种情况就是有其他线程已经为此对象加上了轻量级锁(表明有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

过程

当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

这时Thread-1加轻量级锁失败,进入锁膨胀过程

        即为Object对象申请Monitor锁,让Object指向重量级锁地址

        然后自己进入Monitor的EntryList BLOCKED 

当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,然后唤醒EntryList中BLOCKED线程。、

自旋优化 

重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

偏向锁

针对轻量级锁,在没有发生竞争的时候(就自己一个线程),每次重入仍然需要执行cas操作。

对此,java6中引入了偏向锁来做进一步优化:只有第一次使用cas将线程ID设置到对象的mark word头,之后发现这个线程id是自己的就表示没有竞争,不用重新cas。以后只要不发生竞争,这个对象就归该线程所有。

锁消除

锁消除(Lock Elimination)是编译器或运行时系统在代码中自动消除不必要的锁操作的优化技术。在某些情况下,编译器或运行时系统可以分析代码并确定某些锁操作是不必要的,然后将它们移除,以提高程序的性能和效率。

锁消除通常发生在以下情况下:

  1. 逃逸分析: 编译器通过逃逸分析(Escape Analysis)来判断某个对象的引用是否会逃逸到方法外部,如果对象的引用不会逃逸,则可以安全地消除相关的同步操作。

  2. 循环不变式: 如果某个锁操作位于循环内部,但实际上被锁保护的资源在循环执行过程中并没有发生变化,编译器可以将锁操作移出循环外部,避免重复获取和释放锁。

  3. 线程私有对象: 如果某个对象只在单个线程中使用,那么在该线程中的同步操作是不必要的,因为不存在竞争。编译器可以消除这些不必要的同步操作。

  4. 字符串常量池: 字符串常量池中的字符串是不可变的,因此不需要进行同步操作来保护它们的访问。

通过锁消除技术,可以减少程序中不必要的同步操作,从而减少锁竞争、提高并发性能和降低系统开销。然而,锁消除并不是适用于所有情况的通用优化技术,它需要编译器或运行时系统对代码进行静态或动态分析,才能确定哪些锁操作是安全的可以消除的。

  • 46
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值