Java多线程——线程

来源:互联网 发布:Linux kill掉某个进程 编辑:程序博客网 时间:2024/06/05 03:09

多线程编程的利弊

其实我们的程序在运行的时候,CPU很多时候都是空闲的状态,因为程序不光有CPU调度,而大多数耗时的操作都在于IO上,因此要合理利用CPU的空闲时间,来提高程序的性能。

多线程也会带来一些负面的东西,比如如何的解决并发带来的线程安全问题。这个问题很不好解决,一般解决线程安全常会用到同步,同步或者过多线程间上下文的切换又会带来性能的下降。

进程和线程的关系

进程是资源分配的基本单位,是一个程序或者服务的基本单位。可以说进程就是程序的执行过程,这个过程包括很多东西,如CPU执行时间、运行内存、数据等,而且是一个动态的过程。线程是轻量级的进程,它们共享在父进程拥有的资源下,每个线程在父进程的环境中顺序的独立的执行一个活动,每个CPU核心在同一时刻只能执行一个线程。

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。

  3. 处理机分给线程,即真正在处理机上运行的是线程。

  4. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

线程状态

线程的状态有: 新建,就绪,运行,阻塞,死亡;状态间转换如下图。

这里写图片描述
图片来自:https://my.oschina.net/mingdongcheng/blog/139263

线程同步

同步问题的经典例子,对于value++来说,相当于value=value+1,过程分为三步:1、获得value的值。2、value的值加1。3、给value赋值。
一般情况下,如果一个对象的状态是可变的,同时它又是共享的(即至少可被多于一个线程同时访问),则它存在线程安全问题。
value++是一种“读-写-改”的操作,原子操作需要保证,在对对象进行修改的过程中,对象的状态不能被改变。这个现象我们用一个名词:竞争条件来描述。换句话说,当计算结果的正确性依赖于运行时中相关的时序或者多线程的交替时,会产生竞争条件。

同步方式

为了多线程操作的安全性,java提供了一些同步方式: synchronized修饰的同步方法和同步代码块,volatile,重入锁,threadlocal,在java.util.concurrent.atomic 包下有一些将数字和对象引用进行原始状态转换的类,将有状态变换的操作转化为原子操作。

Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操作的原子性,我们可以认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。

Java线程实现

通过Thread类或实现Runnable接口都能创建个新线程,下面是简单的实现代码。

public class SimpleThreadPractice {    public static void main(String[] args){        Mythread mythread = new Mythread();        mythread.start();        new Thread(new TestThread()).start();    }}class Mythread extends Thread {    public void run(){        System.out.println(Thread.currentThread() + " extends Thread");    }}class TestThread implements Runnable{    public void run(){        System.out.println(Thread.currentThread() + " implements Runnable");    }}

运行结果:

Thread[Thread-0,5,main] extends ThreadThread[Thread-1,5,main] implements Runnable

不管是用哪种方法,实际上都是要实现一个 run 方法的。 该方法本质是上一个回调方法。由 start 方法新创建的线程会调用这个方法从而执行需要的代码。run 方法并不是真正的线程函数,只是被线程函数调用的一个 Java 方法而已(这个回调是在jvm源码中完成的)。创建线程的实际方法是start() 中调用的 native 方法 start0(),在jvm中实现的本地方法会回调run方法。直接调用 run 方法不会报错,但是却是在当前线程执行,而不会创建一个新的线程。

Thread类和Runnable接口使得多线程编程简单直接,但有一个缺陷就是:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。因此从Jdk1.5开始,有了一系列的类的出现来解决这些问题,如Callable和Future,FutureTask,这个可以再单独说下。

线程常用方法和属性

优先级(priority)

每个类都有自己的优先级,一般property用1-10的整数表示,默认优先级是5,优先级最高是10;优先级高的线程并不一定比优先级低的线程执行的机会高,只是执行的机率高;默认一个线程的优先级和创建他的线程优先级相同;

Thread.sleep(long millis)

当前线程睡眠millis的时间(millis指定睡眠时间是其最小的不执行时间,因为sleep(millis)休眠到达后,无法保证会被JVM立即调度);线程sleep()时不会失去拥有的对象锁。 作用:保持对象锁,让出CPU,调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留一定的时间给其他线程执行的机会;

Thread.yield()

让出CPU的使用权,给其他线程执行机会、让同等优先权的线程运行(但并不保证当前线程会被JVM再次调度、使该线程重新进入Running状态),如果没有同等优先权的线程,那么yield()方法将不会起作用。而执行 yield() 方法后转入就绪(ready)状态,yield() 方法(跟操作系统相关),移植性差一些

thread.join()

使用该方法的线程会在此之间执行完毕后再往下继续执行。

object.wait()

当一个线程执行到wait()方法时,他就进入到一个和该对象相关的等待池(Waiting Pool)中,同时失去了对象的机锁—暂时的,wait后还要返还对象锁。当前线程必须拥有当前对象的锁,如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常,所以wait()必须在synchronized block中调用。

object.notify()/notifyAll()

唤醒在当前对象等待池中等待的第一个线程/所有线程。notify()/notifyAll() 也必须拥有相同对象锁,否则也会抛出 IllegalMonitorStateException异常。

如何关闭线程

为什么要废弃 stop 方法


主要 stop 本身就是不安全的,stop 一个线程会导致在该线程上所有锁定的 monitor(管程) 都会被解锁,如果之前被这些 monitor 保护的对象之前处于不一致状态,其他的线程看到这些对象也会处于不一致的状态,这种对象称为 damaged object, 如果线程在这些受损的对象上操作的时候,可能会导致任意行为,而且这种行为难以检测,不像 unchecked 异常,这样容易发现。ThreadDead 异常会杀死其他线程,导致用户可能收不到警告,只有在正在崩坏的时候才能发现。

如何关闭线程


我们应该通过共享变量标志来表明是否线程需要停止运行的代码,而且目标线程应该有规律的去检测变量,如果变量指示线程需要停止的时候,目标线程应该有序地从它 run 方法中返回,同时,由于要保证变量的线程可见性,变量应该修饰为 volatile 或者进行同步访问。

线程正确关闭方式:
正确的方法—设置退出标志 或者 退出标志+中断
使用volatile 定义boolean running=true,通过设置标志变量running,来结束线程。

这样做的好处是:使得线程有机会使得一个完整的业务步骤被完整地执行,在执行完业务步骤后有充分的时间去做代码的清理工作,使得线程代码在实际中更安全。

使用violate boolean变量来标识线程是否停止。
停止线程时,需要调用停止线程的interrupt()方法,因为线程有可能在wait()或sleep(), 提高停止线程的即时性。

代码实现和文章参考:http://blog.xianyijun.cn/2016/05/23/Java如何关闭一个线程/ 和参考资料中的 如何停止一个正在运行的java线程.

参考资料

Java 中的进程与线程
Java之美[从菜鸟到高手演变]之多线程简介
如何停止一个正在运行的java线程

1 0
原创粉丝点击