java中的线程

536 阅读7分钟

java语言里的线程本质上就是操作系统的线程,他们是一 一对应的

线程生命周期

线程状态转换图—— 五态模型

  1. 初始状态: 线程已经被创建,但是还没有分配CPU执行。 这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅时在编程语言层面被创建,而在操作系统层面,真正的线程还没创建。
  2. 可运行状态:初始状态线程执行start()方法,线程具备CPU的执行资格,没有CPU的执行权。 这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配CPU执行了。
  3. 运行状态: 处于可运行状态的线程得到CUP执行权。 当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程状态就转换成了运行状态。
  4. 休眠状态:运行状态的线程释放CPU的执行权。 运行状态的线程如果调用一个阻塞的API(sleep,wait)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得 CPU使用权。当等待的事件出现了(sleep到期,wait执行notif),线程就会从休眠状态转换到可运行状态。
  5. 终止状态: 线程执行完或者出现异常或者调用API强制结束。

java中的线程周期

Java语言中线程共有六种状态,分别是:

  1. NEW(初始化状态) : Java刚创建出来的Thread对象。
  2. RUNNABLE(可运行/运行状态) : Thread执行start()方法。
  3. BLOCKED(阻塞状态),WAITING(无时限等待),TIMED_WAITING(有时限等待) : RUNNABLE状态的线程调用wait()、join()、sleep()方法。
  4. TERMINATED(终止状态): run()方法执行完,或者意外中断。

BLOCKED,WAITING,TIMED_WAITING 是上面提到的休眠状态。Java线程处于这些状态那么这个线程就永远没有CPU的使用权。

线程可以通过isInterrupted()方法,检 测是不是自己被中断了。

创建一个线程

Java刚创建出来的Thread对象就是NEW状态,而创建Thread对象主要有两种方法。一种是继承Thread对 象,重写run()方法。示例代码如下:

方式一: 继承Thread

//    ⾃定义线程对象 
class MyThread extends Thread{
    public void run() {                
        //    线程需要执⾏的代码    
    } 
} 
//    创建线程对象 
MyThread myThread =    new    MyThread();

方式二: 实现Runnable接口

//实现Runnable接⼝ 
class Runner implements Runnable {        
    @Override        
    public void run(){                
        //线程需要执⾏的代码        
    } 
} 
//创建线程对象 
Thread thread = new Thread(new Runner());

方式三:实现Callable接口

//实现Runnable接⼝ 
class Runner implements Callable<String> {        
    @Override
     public String call() throws Exception {
          //线程需要执⾏的代码        
         return null;
     }
}

//创建 FutureTask
 FutureTask<String> ft1 = new FutureTask(new Runner());
//执行这个任务
Thread t1 = new Thread(ft1);
t1.start();
//获取返回值
t1.get();

方法三实质上也是实现了Runnable接口,因为FutureTask实现了Runnable接口

Future接口提供的方法:

//    取消任务 
boolean    cancel(boolean    mayInterruptIfRunning); 
//    判断任务是否已取消        
boolean    isCancelled(); 
//    判断任务是否已结束 
boolean    isDone(); 
//    获得任务执⾏结果 
get(); 
//    获得任务执⾏结果,⽀持超时 
get(long timeout, TimeUnit unit);

这两个get()方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。

  • Java刚创建出来的Thread对象就是NEW状态
  • NEW状态的线程不会被操作系统调度,因此不会执行
  • NEW状态的线程调用start()会进入RUNNABLE状态

stop()与interrupt()的区别

stop()会杀死线程,如果线程持有ReentrantLock锁,被stop()的线程并不会自动调用ReentrantLock的unlock()去释放锁,那其他线程就再也没机会获得ReentrantLock锁。所以该方法就不建议使用了,类似的方法还有suspend()和resume()方法,这两个方法同样也都不建议使用。

interrupt()仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。

为什么要使用多线程

提高程序的性能: 降低延迟,提高吞吐量。

提高性能的方式:1优化算法;2将硬件的性能发挥到极致

在并发编程领域,提升性能本质上就是提升硬件的利用率,具体来说就是提升I/O的利用率和CPU的利用率。

如果CPU和I/O设备的利用率都很低,那么可以尝试通过增加线程提高吞吐量。

创建多少线程合适?

我们的成语一般都是CPU计算和I/O操作交叉执行的,由于I/O设备的速度相对于CPU来说都是很慢的,所以大部分情况下,I/O操作的执行时间相对于CPU计算来说都非常长,这种场景我们一般都成为I/O密集型程序和CPU密集型程序,计算最近线程数的方法是不同的。

对于CPU密集型的计算场景,理论上“线程的数量-CPU核数”就是最合适的。不过在工程上,线程的数量一般会设置为"CPU核数+1" ,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证CPU的利用率。

对于I/O密集型的计算场景,比如前面我们的例子中,如果CPU计算和I/O操作的耗时是1:1,那么2个线程是 最合适的。如果CPU计算和I/O操作的耗时是1:2,那多少个线程合适呢?是3个线程,如下图所示:CPU在 A、B、C三个线程之间切换,对于线程A,当CPU从B、C切换回来时,线程A正好执行完I/O操作。这样CPU 和I/O设备的利用率都达到了100%。

更多的精力其实应该放在算法的优化上,线程池的配置,按照经验配置一个,随时关注线程池大小对程序 的影响即可,具体做法:可以为你的程序配置一个全局的线程池,需要异步执行的任务,扔到这个全局线 程池处理,线程池大小按照经验设置,每隔一段时间打印一下线程池的利用率,做到心里有数。

设置线程数的原则: 将硬件的性能发挥到极致。

为什么局部变量是线程安全的?

调用栈结构

线程与调用栈的关系图

每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。

局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就没用了,局部变量和方法同生共死。

局部变量是和方法同生共死的,一个变量如果想跨越方法的边界,就必须创建在堆里。

调用栈与线程

两个线程可以同时用不同的参数调用相同的方法。

每个线程都有自己独立的调用栈。

线程封闭 : 仅在单线程内访问数据。不存在多线程的数据共享。

栈溢出的原因:

因为每调用一个方法就会在栈上创建一个栈帧,方法调用结束后就会弹出该栈帧,而栈的大小不是无限的 ,所以递归调用次数过多的话就会导致栈溢出。而递归调用的特点是每递归一次,就要创建一个新的栈帧 ,而且还要保留之前的环境(栈帧),直到遇到结束条件。所以递归调用一定要明确好结束条件,不要出现死循环,而且要避免栈太深。

解决方法:

  1. 简单粗暴,不要使用递归,使用循环替代。缺点:代码逻辑不够清晰;
  2. 限制递归次数;
  3. 使用尾递归,尾递归是指在方法返回时只调用自己本身,且不能包含表达式。编译器或解释器会把尾递归做优化,使递归方法不论调用多少次,都只占用一个栈帧,所以不会出现栈溢出。然而,Java没有尾递归优化。

------ 码字不易如果对你有帮助请给个关注

爱技术爱生活 QQ群: 894109590