Java并发编程基础----线程

一、线程介绍

       现代操作系统运行一个程序时,会为其创建一个进程。现代操作系统的最新调度单位就是线程,线程也称为轻量级进程。一个进程中可以包含多个线程,这些线程有自己的计数器、堆栈、和局部变量属性,并且能够访问共享的内存变量。java程序天生就是一个多线程,一个普通的java程序不仅仅只有main()方法的运行,而且还有其他多个线程在运行。

二、为什么要使用多线程?

       正确使用多线程,总能给开发人员带来显著的好处,使用多线程的原因如下:

       1)更多处理器核心:

    一个线程在同一时刻只能运行在一个处理器上,使用多线程技术,将逻辑分配到多个核心处理器上更加有效率。

  2)更快的响应时间:

    将数据一致性不强的操作分派给其他线程,使响应用户请求的线程尽快完成,缩短响应时间。

  3)更好的编程模型:

    Java为多线程编程提供了良好、考究并且一致的编程模型,使得开发人员能够更加专注于问题的解决。

三、线程的优先级

       现代操作系统基本采用时分的形式调度运行的线程。操作系统分出一个个时间片,线程会分配到若干时间片,时间片使用完就会发生线程调度,等待下次分配。线程分配的时间片多少也决定了线程使用处理器资源的多少。而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

     在java线程中,有一个整型成员变量priority来控制优先级,范围从1~10,超出就会报出异常。构建线程时,默认优先级是5。优先级高则分配到的时间片数量多于低的。可以通过setPriority(int x)来设置

     常量:Thread.MIN_PRIORITY = 1   Thread.NORM_PRIORITY = 5   Thread.MAX_PRIORITY = 10

     在设置优先级时候:

    针对频繁阻塞(休眠/IO操作)的线程需要设置较高的优先级,针对计算的(占用较多CPU时间或者偏向运算)的设置较低的优先级,这样会避免线程被独占。

    注意:线程的优先级不能作为程序正确性的依赖,因为操作系统可以完全不理会这个设定。针对不同的系统而言,优先级这个概念可能就不存在。

四、线程的状态

       Daemon线程 java线程在运行的生命周期中可能处于下表6中不同的状态,在某一特定时刻,线程只能处于其中的一个状态。

       事实上,线程在整个运行过程中是会随着代码的运行而不断变化状态的。在线程创建后,调用start( )方法开始运行。当线程执行到wait()方法时,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能返回运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,当超时时间到达时,线程将返回运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入阻塞状态。最后线程执行完run()方法之后将进入终止状态。

五、Daemon线程(守护线程 )

       在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。 

  Daemon线程是一种支持性线程,主要是用在后台程序做一些后台调度与支持性工作。这意味着当JVM中当所有的非守护线程结束时,即只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。JVM将自动退出。

  可以通过调用Thread.setDaemon(true)方法将线程设为Daemon线程。(注:该方法必须在start()或者run()方法前执行,也就是说必须在线程启动前执行)

  Daemon线程被用作,完成支持性工作,但是在java虚拟机退出时,Daemon线程中的finally块并不一定会执行。

  注:在构建Daemon时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

六、启动和终止线程

 1、构造线程:在运行线程之前需要先构造线程对象,线程对象的构造需要指定线程所需要的属性,比如:所属线程组、线程优先级、是否为Daemon线程等信息。下面我们看一下,java.lang.Thread中对线程初始化的方法:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        //当前线程就是该线程的父线程
        Thread parent = currentThread();
        this.group = g;
        //使用父线程的daemon、priority属性
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        setPriority(priority);
        //将父线程的inheritableThreadLocal复制过来
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

        /* 分配一个线程ID */
        tid = nextThreadID();
}

从这里也能看到父线程与子线程的关系:

  父线程就是当前线程(开启多线程的线程),子线程会具有与父线程一致的优先级, 守护线程,线程组,还会有父线程的可继承ThreadLocal。还会分配给一个唯一的ID。

  init()运行完毕,线程对象就初始化好了,在堆内存中等待运行

2、启动线程

      线程完成初始化后,调用start()方法就可以启动这个线程,

   线程start()的含义:当前线程同步告知JVM,只要线程规划器空闲,应立即启动调用start()方法的线程。

      注:作为一个习惯,最好为自定义线程起一个好名字。根据构造方法,为自定义线程取个好名字吧。

3、理解中断

     中断:可以理解为线程的一个标识位属性,其他线程中通过该线程对象调用interrupt()方法使其进入中断状态。

     线程通过方法isInterrupted()来判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断进行复位。

    1) this.interrupted(): 判断线程终止的状态, 执行后具有将状态标志置为false的功能

    2) this.isInterrupted():判断线程终止的状态,不具有清除状态标志的功能

六、安全地终止线程

   前面提到中断状态是一个线程的标识位。中断操作最适合用来取消或停止任务,也可以利用一个boolean变量来控制是否需要停止任务并终止该线程,这种通过标识位或者中断操作的方式能够是线程在终止时有机会去清理资源,显得安全和优雅。

public class Shutdown{
    public static void main(String[] args){
      Runner one = new Runner();
      Thread countThread = new Thread(one,"CountThread");
      countThread.start();
      //睡眠一秒,main线程对CountThread进行中断,使CountThread能够感知并结束。
      Thread.sleep(1000);
      countThread.interrupt();
      Runner two = new Runner();
      countThread = new Thread(two,"CountThread");
      countThread.start();
      //睡眠一秒,main线程对two进行cancel,使得CountThread能够感知并结束
      Thread.sleep(1000);
      two.cancel();
  }


private static class Runner implements Runnable{
    private long i;
    private volatile boolean on = true;
    @Override
    public void run() {
       while (on && !Thread.currentThread().isInterrupted()) {
           i++;
        }
        System.out.println("Count i = " + i);
     }
     public void cancel() {
            on = false;
    }
}

}

七、线程之间的通信

      多线程的目的是多条线程执行不同的逻辑业务从而能够提升业务整体的响应速度,如果线程仅仅是孤零零的执行,这些不同的逻辑业务就不能最终汇聚成一个完整的业务那么多线程也就失去了意义,这就是为什么要有线程间通信的存在。

1、volatile 和 synchronized 关键字

(1)关键字volatile可以修饰字段(成员变量),就是告知程序,任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

      但是:过多地使用volatile是不必要的,因为它会降低程序执行的效率。

(2)关键字synchronized可修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。实现细节是通过:monitorenter 和 monitorexit指令。

     本质是对一个对象的监视器(monitor)的获取,而且这个获取过程是排他的,也就是说同一时刻只有一个线程获取由synchronized所保护对象的监视器。任何对象都有自己的监视器,当对象由同步块或者对象的同步方法调用时,执行方法的线程必须先获取对象的监视器才能进入同步块或者同步方法,而没有获取监视器的线程会阻塞在同步块与同步方法的入口,进入BLOCKED状态。

      从图可以知道,任何对象对受synchronized保护的对象Object访问时,首先要获取Object监听器,获取监视器的线程会失败,进入同步队列,状态进入BLOCKED状态。当获得锁的线程释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监听器的获取。

八、等待/通知机制

    等待/通知的相关方法是任意java对象都具备的,因为该方法被定义在所有对象的超类上java.lang.Object

      等待通知机制:线程A调用了对象O的wait()方法进入了等待状态,而线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

  注:上述两个线程通过对象O来完成交互,而对象的wait()与notify()或notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

使用注意:

    1)使用wait()、notify()、notifyAll()方法都需要先对调用对象加锁。(即锁对象应该为调用对象)

    2)调用wait()方法后,线程状态由RUNNING变为WAITTING,将锁释放,并将当前线程放到对象的等待队列。

    3)notify()或notifyAll()方法调用后,不会立刻释放锁,需要等待调用notify()、notifyAll()的线程释放锁之后,等待线程才可能会拿到锁。

    4)notify()将对象的等待队列中的一个线程随机地移到同步对象,notifyAll()将等待队列中的全部线程都移到同步队列,然后使它们争抢锁,被移动的状态由WAITING变为BLOCKED。

    5)从wait()返回的前提是获取调用对象的锁。

等待/通知机制依托于同步机制,其目的是确保等待线程从wait()方法返回时能够感知通知线程对共享变量做出的修改。

九、管道输入/输出流

       管道输入/输出流和普通的文件输入/输出流不同的就是:它主要用于线程之间的数据传输,而传输的媒介是内存。

       主要有4种实现:PipedOutputStream/PipedIntputStream(面向字节)、PipedReader/PipedWriter(面向字符)

public class Piped{
    public static void main(String[] args){
        PipedWriter out = new PipedWriter();
        PipedReader in  = new PipedReader();
        //输出输入流必须连接起来,否则跑IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in),"PrintThread");
        printThread.start();
        int receive = 0;
        while ((receive = System.in.read()) != -1) {
            out.write(receive);
        }
        out.close();
    }

     static class Print implements Runnable {
        private PipedReader in;
        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.println((char)receive);
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 注:在使用管道流的时候要注意,一定要进行绑定,也就是调用connect()方法,否则会出异常。

十、Thread.join()的使用

  如果一个线程A执行了thread.join(),含义:当前线程A等待thread线程终止后才从thread.join()返回。

  除了join()外,还有join(long millis)和join(long millis,int nanos)两个具备超时的方法,这两个方法表示:如果线程thread没有在指定时间内停止,那么线程A会从该超时方法返回。

十一、ThreadLocal的使用

  ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

  可以通过set(T)来设置值,然后在当前线程下使用get()来获取原先设置的值。

  不过有点遗憾的是只能放一个值。对你没有看错只能放一个值,再次调用set设置值,会覆盖前一次set的值。

借鉴博客:https://www.cnblogs.com/lilinzhiyu/p/8086235.html

书籍:《Java并发编程的艺术》

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值