Java 线程

在说多线程的时候,总会想起来以前读书的时候有那么一句话:进程是cpu资源分配的最小单位,线程是cpu调度的最小单位。至于进程和线程的关系本文不作分析。主要分析一下线程。

线程状态分析

在JVM中,把线程分为几种不同的状态,比如线程创建完毕后的new 状态,还有调用了start方法后线程进入Runnable状态,Runnable状态的线程会被放到JVM的科运行线程队列中等待获取CPU的执行权,此外JVM按照现成的优先级及时间分片,轮询的方式来执行Runnable状态的线程。当线程进入start的代码段,开始执行时,其线程状态转变为Running;线程如果执行过程中如果执行了sleep,wait,join,或者进入IO阻塞,锁等待时,则进入Wait或Blocked状态,在这种状态下线程会放弃CPU的使用权。等到线程wait结束,线程被唤醒或获取到锁,在这些情况下线程可以再次进入Runnable状态,在线程执行完毕之后,线程就可从运行线程的队列中删除了。JVM会根据线程的不同状态把线程放入到不同的sets中进行调度。
在这里插入图片描述
状态分析:

  1. 初始状态(New):刚刚实例化,还没有调用start方法。
  2. 可运行状态(Runnable):大致可以细分为两种,一种是就绪状态(ready),另一种是运行种状态(running)。就绪状态是放入JVM的可运行线程对列等待获取CPU的执行权,运行状态是从就绪队列中获取到了CPU执行权,开始执行代码段。
  3. 等待(WAITING)处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态,调用下面任何一种方法都会使线程进入到这种状态:
    * Object.wait with no timeout
    * Thread.join with no timeout
    * LockSupport.park
  4. 超时等待(TIMED_WAITING):处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒,调用一下方法会使线程进入到
    * Thread.sleep
    * Object.wait with timeout
    * Thread.join with timeout
    * LockSupport.parkNanos
    * LockSupport.parkUntil
  5. 终止(TERMINALED):当程序运行结束时。

线程资源同步

看线程同步之前先看下下面的代码

    int i = 0;
    public int getNextId() {
        return i++;
    }

如果只是单线程访问这段代码肯定是不会有问题的,得到的数字依次会递增,但是同时存在多个线程同时访问这一段代码时就会出现问题,线来看下这段代码的执行过程:
1)JVM首先在堆中给i分配一个内存存储场所,并存储其值为0;
2)线程启动后,会自动分配一片working memory区(通常是操作数栈),当线程执行到return i++时,JVM中并不是简单的一个步骤就可以完成了。而是要分成五个步骤:i++动作在JVM中分为装载i,读取i,进行i+1操作,存储i以及写入i五个步骤才得以完成。

  1. 装载i:线程发起一个装载i的请求给JVM线程执行引擎,引擎接收请求后会向main memory发起一个read i的指令。当read i执行完毕后,一段时间线程会将i的值从main memory区复制到working memory区中。
  2. 读取i:此步骤负责从main memory中读取i。
  3. 进行i+1操作:此步骤由线程完成。
  4. 存储i:将i+1的值赋值给i,然后存储到working memory中。
  5. 写入i:一段时间后i的值会写回到main memory中。
    看完以上步骤,关键问题有两点:一是working memory中的i值与main memory中的i值的同步是需要时间的;二是i++由多个操作组成。多线程情况下在这个时间段内执行了操作,可能会获取i值相同的现象。举个例子:假设A已经执行到i+1操作,但尚未执行写入i操作。线程B就完成了装载i的过程,那么当线程B执行完毕时,其得到的值和A就是一样的了。

在JVM中把对于working memory的操作分为了use、assign、load、store、lock、unlock,对于working memory的操作的指令由线程发出,对于main memory的操作分为了read、write、lock、unlock;对于main memory的操作指令由线程执行引擎发出,下面为具体含义:

  • use
    use由线程发起,需要完成将变量的值从working memory中复制到线程执行引擎中。
  • assign
    assign由线程发起,需要完成将变量值复制到线程的working memory中,例如a=i,这时线程就会发起一个assign动作。
  • load
    load由线程发起,需要完成将main memory中read到的值复制到working memory中。
  • store
    store由线程发起,负责将变量的值从working memory中复制到main memory中,并等待main memory通过write动作写入此值。
  • read
    read由main memory发起,负责从main memory中读取变量的值。
  • write
    write由main memory发起,负责将working memory的值写入到main memory中。
  • lock
    lock动作由线程发起,同步操作main memory,给对象加锁。
  • unlock
    unlock动作由线程发起,同步操作main memory,去除对象的锁。

在JVM中以下的操作还是顺序的:
1)同一个线程上的操作一定是顺序执行的。
2)对于 main memory 上的同一个变量的操作一定是顺序执行的,也就是不可能两个请求同时读取变量值;
3)对于加了锁的main memory上的对象操作,一定是顺序执行的,也就是两个以上加了lock的操作,同时肯定只有一个是在执行的。

那么如何保证上诉的代码安全的执行呢?JVM提供了synchronized,volatile,lock/unlock机制。
首先来看下synchronized关键字来改造上面的用法:

public synchronized int getNextId() {
        return i++;
    }

当多线程执行此代码时,线程A执行到getNextId()方法,JVM知道方法上由synchronized关键字,于是在执行其他动作前首先按照对象的的实例ID加上一个lock。然后再继续执行return i++,而此时如线程B并发访问getNextId()方法,JVM观察到这个对象的实例ID上有lock,于是将线程B放入等待队列中,只有当线程A执行完毕后,JVM才会释放对象实例ID上的lock,重新标记为unlock。这时当线程调度到线程B时,线程B才得以执行getNextId()方法,由于这个过程是串行的,因此可以保证每个线程getNextId都是不一样的值。
那么synchronized的用法其实有四种:

public synchronized int getNextId() {
        return i++;
}
public int getNextId() {
        synchronized (this) {
            return i++;
        }
 }
public int getNextId() {
        synchronized (TestThread.class) {
            return i++;
        }
 }
public static synchronized int getNextId() {
        return i++;
 }

对于第一种和第二种都是只能防止多个线程同时执行同一个对象(这里敲黑板划重点)的同步代码段。对于同一个类的不同对象是不起任何作用的。比如下面的代码:

public synchronized void sayHello() {
        System.out.println("hello, 我开始喽");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello, 我结束喽");
    }

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    new TestThread().sayHello();
                }
            }).start();
        }
  }

输出结果:

hello, 我开始喽
hello, 我开始喽
hello, 我开始喽
hello, 我结束喽
hello, 我结束喽
hello, 我结束喽

对于这种同一个类不同对象完全没有起作用,对于非static的synchronized方法,锁的就是对象本身也就是this。另外三四两种方法,当前锁的粒度是当前的Class稍微改造了一下前面的代码:

public  void sayHello() {
        synchronized (TestThread.class) {
            System.out.println("hello, 我开始喽" + i);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hello, 我结束喽");
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    new TestThread().sayHello();
                }
            }).start();
        }
   }

结果:

hello, 我开始喽
hello, 我结束喽
hello, 我开始喽
hello, 我结束喽
hello, 我开始喽
hello, 我结束喽

对于同一个类的不同实例来进行控制的话就需要采取这种全局锁。当然锁的用法也要十分的小心,一不小心可能造成效率极大的降低,另外还有可能造成死锁,比如下面两个例子:

public  void sayHello() {
        synchronized (this) {
            System.out.println("hello, 我开始喽");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hello, 我结束喽");
        }
    }

    public void sayHi() {
        synchronized (this) {
            System.out.println("hi, 我开始喽");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hi, 我结束喽");
       }
    }


    public static void main(String[] args) {
        System.currentTimeMillis();
        final TestThread testThread = new TestThread();
        final long startMill = System.currentTimeMillis();
        new Thread(new Runnable() {
            @Override
            public void run() {
                testThread.sayHello();
                System.out.println("1用时时间 " + (System.currentTimeMillis() - startMill));
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testThread.sayHi();
                long endMill = System.currentTimeMillis();
                System.out.println("2用时时间 " + (endMill - startMill));
            }
        }).start();
   }

同一个对象的不同方法上如果加了对同一个对象的synchronized锁,因为锁住了同一个对象,随意sayHi的执行必须要等待sayHello的执行结束才可以,明明可以并行的方法,最后成了串行造成了效率极大的下降。

public  void sayHello() {
        synchronized (a) {
            synchronized (b) {
                System.out.println("hello, 我开始喽");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("hello, 我结束喽");
            }
        }
    }

    public void sayHi() {
        synchronized (b) {
            synchronized (a) {
                System.out.println("hi, 我开始喽");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("hi, 我结束喽");   
            }
       }
    }

当上面的sayHello和sayHi同时执行就会产生死锁现象,导致系统挂起。而对于使用lock和unlock来编写多线程程序而言,一定要保证lock和unlock 成对出现的,并且要保证lock后程序执行完毕时一定要unlock,否则就有线程出现锁饿死的现象。
volatile的机制有所不同,它仅用于控制线程中对象的可见性,但并不能保证在此对象上操作的原子性。就像上面场景中的i++,即使把i定义为volatile也是没用的,对于定义为volatile的变量,线程不会将其从main memory复制到work memory中,而是直接在main memory中进行操作。它的代价虽然比synchronized、lock/unlock低,但用起来要非常小心,毕竟不能保证操作的原子性。

线程交互机制

多线程既然有了资源上的竞争,必然就有交互上需求。例如最典型的连接池,连接池中通常都会有get和return两个方法,return的时候需要将连接返回到缓存队列中,并将可使用的连接数加1,而get方法在判断可使用的连接数已经到了0后,需要进入一个等待状态。当有连接返回连接池时通知get方法有新的连接,不需要再等待了。如果没有这个机制,就只有在get方法中不断轮询判断可使用的连接数值。当然JVM提供wait、notify、notifyAll方式来支持这类需求,在基于Object的wait、notify、notifyAll实现连接池时大致如下:

public Connection get() {
        synchronized (this) {
            if (free > 0) {
                free--;
                return cacheConnections.poll();
            } else {
                this.wait();
            }
        }
 }
    
public void close(Connection conn) {
        synchronized (this) {
            free++;
            cacheConnections.offer(conn);
            this.notifyAll();
        }
}

调用Object的wait方法可以让当前线程进入等待状态,只有当其他线程调用了此Object的notify、或notifyAll方法,或者wait(毫秒数)到达了指定的时间后,才会被激活继续执行。另外注意的是notify只是随机找wait此Object的一个线程,而notifyAll则是通知wait此Object的所有线程。此外注意一点,在Sun JDK中,object.wait还有可能被假唤醒,因此通过object.wait被唤醒后,应再次确认需要等待的状态是否变更了。如果未变更,则继续进入wait状态,这种做法通称为double check,或者将wait加入到loop中,真正被唤醒时才从循环中跳出。
当线程调用了对象的wait方法后,JVM线程执行引擎会将此线程放入到一个wait sets中,并释放此对象的锁,在wait sets中的线程将不会被JVM线程执行引擎调度执行;当其他线程调用了此对象的notify方法时,会从wait sets中随机找一个等待在此对象的上的线程(1.8-中调用notify() 唤醒的是等待队列中的头节点(等待时间最长的那个线程)),并将其从wait sets中剔除,这样JVM线程调度引擎就可以再次调度执行此线程了;当调用notifyAll方法时,则会删除wait sets上所有等待此对象上的线程,删除完毕之后释放对象锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三寸花笺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值