精华内容
下载资源
问答
  • java多线程

    多人点赞 2012-06-15 15:12:30
    Java多线程 目录 1 线程的概念 1 1.1 线程的概念 1 1.2 进程与线程 1 2 线程的创建 2 2.1 JAVA创建线程 2 2.1.1 实现Runnable接口 2 2.1.2 扩展Thread类 4 2.2 JAVA线程的实例化,启动,执行 6 ...
    Java多线程


    目录
    1 线程的概念 1
    1.1 线程的概念 1
    1.2 进程与线程 1
    2 线程的创建 2
    2.1 JAVA创建线程 2
    2.1.1 实现Runnable接口 2
    2.1.2 扩展Thread类 4
    2.2 JAVA线程的实例化,启动,执行 6
    2.2.1 Java线程的实例化 6
    2.2.1 Java线程的启动 7
    2.2.2 Java线程的执行 7
    3 线程的状态 7
    3.1 JAVA线程的状态 7
    3.2 改变线程状态的方法 8
    3.2.1 sleep() 9
    3.2.2 wait(),notify(),notifyAll() 9
    3.2.3 yield() 9
    3.2.4 join() 10
    3.2.5 interrupt() 10




    1 线程的概念
    1.1 线程的概念
    线程:是"进程"中某个单一顺序的执行流。是程序执行流的最小单元,一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
    线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
    Java中,每个线程都有一个调用栈,即使不在程序中创建任何新的线程,线程也在后台运行着。一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。一旦创建一个新的线程,就产生一个新的调用栈。

    1.2 进程与线程
    现代操作系统都支持多任务处理,多任务处理有两种截然不同的类型:基于进程和基于线程的。
    基于进程的多任务处理:进程(Process)本质上是一个运行的程序。因此。基于进程的多任务处理就是系统可以同时运行多个应用程序。如系统上可以同时运行文本编辑器和word文档编辑器。
    基于线程的多任务处理:线程(thread)本质是进程的一个执行流,是最小的执行单位。这就意味着一个程序可以同时执行多个任务功能。如一个文本编辑器可以在打印的同时格式化文本。
    在多任务处理中多线程与多进程相比:进程是重量级任务,需要分配自己独立的地址空间和资源,进程之间的通信是昂贵的,受限的。线程是轻量级任务,线程之间共享相同的地址空间并其共同分享一个进程,线程间的通信时容易的,成本比较低。
    多线程可以提高程序对CPU最大利用率:多线程可以高效使用CPU空闲时间,使得CPU空闲时间保持最低。如网络的数据传输速率远低于计算机的处理能力,本地文件系统资源的读写速度远低于CPU的处理能力,在这些数据传输和文件读写的过程中CPU空闲时间能够让其他线程利用起来。多线程能够使得CPU空闲时间得到充分的利用。

    2 线程的创建
    2.1 Java创建线程
    Java提供了两种方法用来创建线程:
    (1)实现Runnable接口
    (2)扩展Tread类

    2.1.1 实现Runnable接口
    (1) 创建线程类
    package com.thread;
    /**
    * 实现Runnable接口创建线程类
    * @author xmong
    *
    */
    public class TestThreadImpl implements Runnable{

    /**
    * 重写Runnable接口的run方法
    * 当线程启动后会执行该方法
    */
    @Override
    public void run() {
    for (int i = 0; i < 5; i++) {
    try {
    Thread.sleep((long)(Math.random()*1000));
    System.out.println(Thread.currentThread().getName()+" : "+i);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }


    (2) 测试线程
    package com.thread;
    /**
    * 测试多线程
    * @author xmong
    *
    */
    public class TestThread {

    /**
    * 创建两个线程实例
    * 测试实现Runnable接口的线程
    */
    public void testThreadImpl(){
    TestThreadImpl tl1 = new TestThreadImpl();
    TestThreadImpl tl2 = new TestThreadImpl();

    Thread t1 = new Thread(tl1,"tl1");
    Thread t2 = new Thread(tl2,"tl2");

    t1.start();
    t2.start();
    }

    /**
    * main方法测试
    * @param args
    */
    public static void main(String[] args) {
    TestThread tt = new TestThread();
    tt.testThreadImpl();
    }
    }


    (3) 测试结果
    tl2 : 0
    tl2 : 1
    tl1 : 0
    tl1 : 1
    tl2 : 2
    tl1 : 2
    tl1 : 3
    tl2 : 3
    tl1 : 4
    tl2 : 4

    2.1.2 扩展Thread类
    (1) 创建线程类
    package com.thread;
    /**
    * 通过扩展Thread实现线程
    * @author xmong
    *
    */
    public class TestThreadExt extends Thread{
    /**
    * 构造线程
    * @param name
    */
    public TestThreadExt(String name){
    super(name);
    }

    /**
    * 重写Thread类的run方法
    * 当线程启动后会执行该方法
    */
    @Override
    public void run() {
    for (int i = 0; i < 5; i++) {
    try {
    Thread.sleep((long)(Math.random()*1000));
    System.out.println(this.getName()+" : "+i);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }


    (2) 测试线程
    package com.thread;
    /**
    * 测试多线程
    * @author xmong
    *
    */
    public class TestThread {
    /**
    * 创建两个线程实例
    * 测试实现Runnable接口的线程
    */
    public void testThreadImpl(){
    TestThreadImpl tl1 = new TestThreadImpl("tl1");
    TestThreadImpl tl2 = new TestThreadImpl("tl2");

    Thread t1 = new Thread(tl1);
    Thread t2 = new Thread(tl2);

    t1.start();
    t2.start();
    }

    /**
    * 创建两个线程实例
    * 测试扩展Thread类的线程
    */
    public void testThreadExt(){
    TestThreadExt t1 = new TestThreadExt("te1");
    TestThreadExt t2 = new TestThreadExt("te2");

    t1.start();
    t2.start();
    }

    /**
    * main方法测试
    * @param args
    */
    public static void main(String[] args) {
    TestThread tt = new TestThread();
    //tt.testThreadImpl();
    tt.testThreadExt();
    }
    }


    (3) 测试结果
    te2 : 0
    te1 : 0
    te1 : 1
    te2 : 1
    te1 : 2
    te1 : 3
    te2 : 2
    te2 : 3
    te1 : 4
    te2 : 4

    2.2 Java线程的实例化,启动,执行
    2.2.1 Java线程的实例化
    (1)如果是实现了java.lang.Runnable接口的类,则调用Thread的构造方法:
    Thread(Runnable target)
    Thread(Runnable target, String name)
    Thread(ThreadGroup group, Runnable target)
    Thread(ThreadGroup group, Runnable target, String name)
    Thread(ThreadGroup group, Runnable target, String name, long stackSize)

    分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,作为 group 所引用的线程组的一员,并具有指定的堆栈尺寸stackSize。
    (2)如果是扩展java.lang.Thread类的线程,则直接new即可。

    2.2.1 Java线程的启动
    在线程的Thread对象上调用start()方法,而不要直接调用run()或者别的方法。
    在调用start()方法之后,发生了一系列动作:
    1、启动新的执行线程(具有新的调用栈);
    2、该线程从新状态转移到可运行状态(可见线程状态图);
    3、当该线程获得机会执行时,其目标run()方法将运行。
    注意:对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。

    2.2.2 Java线程的执行
    运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是mian,非主线程的名字不确定。线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。
    一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。当线程目标run()方法结束时该线程完成,线程转为死状态。一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。
    线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。


    3 线程的状态
    public static enum Thread.State
    extends Enum<Thread.State>
    线程状态。线程可以处于下列状态之一:
    • NEW
    至今尚未启动的线程处于这种状态。

    • RUNNABLE
    正在 Java 虚拟机中执行的线程处于这种状态。处于可运行状态的某一线程正在 Java 虚拟机中运行,但它可能正在等待操作系统中的其他资源,比如处理器。

    • BLOCKED
    受阻塞并等待某个监视器锁的线程处于这种状态。处于受阻塞状态的某一线程正在等待监视器锁,以便进入一个同步的块/方法,或者在调用 Object.wait 之后再次进入同步的块/方法。

    • WAITING
    无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。某一线程因为调用下列方法之一而处于等待状态:
    1、不带超时值的 Object.wait
    2、不带超时值的 Thread.join
    3、LockSupport.park
     处于等待状态的线程正等待另一个线程,以执行特定操作。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。

    • TIMED_WAITING
    等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态:
    1、Thread.sleep
    2、带有超时值的 Object.wait
    3、带有超时值的 Thread.join
    4、LockSupport.parkNanos
    5、LockSupport.parkUntil

    • TERMINATED
    已退出的线程处于这种状态。
    在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。
    获取java线程状态,可以通过Thread的getState()方法
    Thread.State getState() 返回该线程的状态。

    3.1 Java中线程的状态
    Java中线程的状态如下图:

    [img]http://dl.iteye.com/upload/attachment/0069/6020/2a3955f5-ac62-3c60-8f12-94567049dbcf.jpg[/img]

    • 新状态:线程对象已经创建,还没有在其上调用start()方法。此时它和其它的java对象一样,仅仅在堆中被分配了内存。
    • 可运行状态:当一个线程创建了以后,其他的线程调用了它的start()方法,但调度程序还没有把它选定为运行线程时线程所处的状态。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也可以返回到可运行状态。
    • 运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。处于这个状态的线程占用CPU,执行程序run()方法的代码。
    • 等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。
    阻塞状态分为三种情况:
    (1) 等待状态的线程位于对象等待池中:当线程运行时,如果执行了某个对象的wait()方法,JVM就会把线程放到这个对象的等待池中。
    (2) 阻塞状态的线程位于对象锁池中:当线程处于运行状态时,试图获取某个对象的同步锁,如果该对象的同步锁已经被其他的线程占用,JVM就会把这个线程放到对象的锁池中。
    (3) 睡眠状态的线程位于对象锁池中:当前线程执行了sleep()方法,当前线程就会让出CUP的使用,目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。
    线程阻塞的意思是:“雇主对雇工说:你守在这里,其它人做完到你的时候你就接着干活”。
    线程等待的意思是:“雇主对雇工说:你去休息,用的着你的时候我会叫你,你在接着干活”。
    线程睡眠的意思是:“雇主对雇工说:你去睡觉,某个时刻过来报到,然活接着干活”。
    • 死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

    3.2 改变线程状态的方法
    从操作系统的角度讲,OS会维护一个ready queue(就绪的线程队列)。并且在某一时刻cpu只为ready queue中位于队列头部的线程服务。
    在java中提供Thread类的一些方法来控制线程,常用的有sleep(),wait(),yield(),jion()等方法。
    当前正在被服务的线程需要睡一会,醒来后继续被服务,这就是sleep。
    当前正在被服务的线程可能觉得cpu的服务不够好,于是推出了,到等待池中休息,这就是wait。
    当前正在被服务的线程可能觉得cpu的服务不够好,于是提前退出,到ready queue队列中继续排队,这就是yield。
    当前正在被服务的线程被cpu服务完后需要cpu接着服务另一个线程,与是把另一个线程归并到该线程的后面候着一起服务,当然服务这个线程也要经过ready queue队列,这就是join。
    线程退出最好自己实现,在运行状态中一直检验一个状态,如果这个状态为真,就一直运行,如果外界更改了这个状态变量,那么线程就停止运行。
    sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
    sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield()只能使同优先级的线程有执行的机会。
    wait()会使线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。
    wait() 和notify()因为会对对象的“锁标志”进行操作,所以它们必须在 synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non- synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

    3.2.1 sleep()
    Thread中的sleep方法:
    static void sleep(long millis)
    在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
    static void sleep(long millis, int nanos)
    在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

    在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
    线程睡眠是帮助所有线程获得运行机会的最好方法。线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。sleep()是静态方法,只能控制当前正在运行的线程。
    由于sleep()方法是Thread类的方法,因此它不能改变对象的机锁。所以当在一个Synchronized方法中调用sleep()时,线程虽然休眠了,但是对象的机锁 没有被释放,其他线程仍然无法访问这个对象。sleep()方法不需要在同步的代码块中执行。但是sleep()可以通过interrupt()方法打断 线程的暂停状态,从而使线程立刻抛出InterruptedException。

    3.2.2 wait(),notify(),notifyAll()
    Thread从Object对象中继续了wait(),notify(),notifyAll()方法。
    void wait()
    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
    void wait(long timeout)
    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
    void wait(long timeout, int nanos)
    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。
    void notify()
    唤醒在此对象监视器上等待的单个线程。
    void notifyAll()
    唤醒在此对象监视器上等待的所有线程。

    wait()方法则会在线程休眠的同时释放掉机锁,其他线程可以访问该对象。wait()必须在同步的代码块中执行。 当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去了对象的机锁,可以允许其它的线程执行一些同步操作。但是 wait()可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
    wait 可以让同步方法或者同步块暂时放弃对象锁,而将它暂时让给其它需要对象锁的人(这里应该是程序块,或线程)用,这意味着可在执行wait()期间调用线程 对象中的其他同步方法!在其它情况下(sleep啊,suspend啊),这是不可能的.但是注意我前面说的,只是暂时放弃对象锁,暂时给其它线程使用, 我wait所在的线程还是要把这个对象锁收回来的呀.wait什么?就是wait别人用完了还给我啊!
    好,那怎么把对象锁收回来呢?
    第一种方法,限定借出去的时间.在wait()中设置参数,比如wait(1000),以毫秒为单位,就表明我只借出去1秒中,一秒钟之后,我自动收回.
    第二种方法,让借出去的人通知我,他用完了,要还给我了.这时,我马上就收回来.哎,假如我设了1小时之后收回,别人只用了半小时就完了,那怎么办呢?靠!当然用完了就收回了,还管我设的是多长时间啊.
    那么别人怎么通知我呢?相信大家都可以想到了,notify(),这就是最后一句话"而且只有在一个notify()或notifyAll()发生变化的时候,线程才会被唤醒"的意思了. notify()唤醒在此对象监视器上等待的单个线程。当它被一个notify()方法唤醒时,等待池中的线程就被放到了锁池中。该线程将等待从锁池中获得机锁,然后回到wait()前的中断现场。 notifyAll()唤醒在此对象监视器上等待的所有线程。

    3.2.3 yield()
    Thread中的yield方法
    static void yield()
    暂停当前正在执行的线程对象,并执行其他线程。

    Yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么Yield()方法将不会起作用。
    yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果,因为让步的线程还有可能被线程调度程序再次选中

    3.2.4 join()
    Thread中的join方法
    void join()
    等待该线程终止。
    void join(long millis)
    等待该线程终止的时间最长为 millis 毫秒。
    void join(long millis, int nanos)
    等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

    join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作,实现线程串行化执行。
    join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。值得注意的是,线程的在被激活后不一定马上就运行,而是进入到可运行线程的 队列中。但是join()可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
    如t.join(5000);当前线程运行完后,则让t线程等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态

    3.2.5 interrupt()
    Thread中的interrupt(),interrupted()方法
    void interrupt()
    中断线程。
    static boolean interrupted()
    测试当前线程是否已经中断。

    interrupt()中断线程。需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛 出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但 是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。

    3.3 Java线程的优先级
    线程总是存在优先级,优先级范围在1~10之间。JVM线程调度程序是基于优先级的抢先调度机制。在大多数情况下,高优先级的线程比低优先级的线程有更高的几率得到执行。但这仅仅是大多数情况。
    当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。

    3.3.1 线程优先级的值
    线程默认优先级是5,Thread类中有三个常量,定义线程优先级范围:
    static int MAX_PRIORITY 线程可以具有的最高优先级。
    static int MIN_PRIORITY 线程可以具有的最低优先级。
    static int NORM_PRIORITY 分配给线程的默认优先级。

    3.3.2 设置线程优先级
    Thread中获取线程优先级getPriority(),和设置线程优先级setPriority()。
    int getPriority()
    返回线程的优先级。
    void setPriority(int newPriority)
    更改线程的优先级。

    可以通过setPriority方法更改优先级。优先级不能超出1-10的取值范围,否则抛出 IllegalArgumentException。另外如果该线程已经属于一个线程组(ThreadGroup),该线程的优先级不能超过该线程组的优先级。


    4 线程的同步

    5 线程的通信


    /*********************

    未完待续
    展开全文
  • Java多线程代码编写

    千次阅读 2019-08-19 14:27:58
    Java多线程代码编写 什么是多线程 并发和并行  并行:指两个或多个时间在同一时刻发生(同时发生);  并发:指两个或多个事件在一个时间段内发生。 在操作系统中,安装了多个程序,并发指的是在一段时间内...

    Java多线程代码编写

    什么是多线程

    并发和并行

     并行:指两个或多个时间在同一时刻发生(同时发生);

     并发:指两个或多个事件在一个时间段内发生。

    在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

      而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。

      目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

    注意:单核处理器的计算机肯定不能并行的处理多个任务,只能是多个任务交替的在单个 CPU 上运行。

    进程和线程

      进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

      线程:进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。

    我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程和线程

    线程选项:

    进程和线程的区别

      进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

      线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

    注意:1、因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

       2、Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

       3、由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

    线程理解:线程是一个程序里面不同的执行路径

     每一个分支都叫做一个线程,main()叫做主分支,也叫主线程。

      程只是一个静态的概念,机器上的一个.class文件,机器上的一个.exe文件,这个叫做一个进程。程序的执行过程都是这样的:首先把程序的代码放到内存的代码区里面,代码放到代码区后并没有马上开始执行,但这时候说明了一个进程准备开始,进程已经产生了,但还没有开始执行,这就是进程,所以进程其实是一个静态的概念,它本身就不能动。平常所说的进程的执行指的是进程里面主线程开始执行了,也就是main()方法开始执行了。进程是一个静态的概念,在我们机器里面实际上运行的都是线程。

      Windows操作系统是支持多线程的,它可以同时执行很多个线程,也支持多进程,因此Windows操作系统是支持多线程多进程的操作系统。Linux和Uinux也是支持多线程和多进程的操作系统。DOS就不是支持多线程和多进程了,它只支持单进程,在同一个时间点只能有一个进程在执行,这就叫单线程。

      CPU难道真的很神通广大,能够同时执行那么多程序吗?不是的,CPU的执行是这样的:CPU的速度很快,一秒钟可以算好几亿次,因此CPU把自己的时间分成一个个小时间片,我这个时间片执行你一会,下一个时间片执行他一会,再下一个时间片又执行其他人一会,虽然有几十个线程,但一样可以在很短的时间内把他们通通都执行一遍,但对我们人来说,CPU的执行速度太快了,因此看起来就像是在同时执行一样,但实际上在一个时间点上,CPU只有一个线程在运行。

     

    多线程的优势

      1、进程之间不能共享内存,而线程之间可以共享内存。

      2、系统创建进程需要为该进程重新分配系统资源,创建线程的代价则小的多,因此多任务并发时,多线程效率高。

      3、Java 语言本身内置多线程功能的支持,而不是单纯作为底层系统的调度方式,从而简化了多线程编程。

      注意:多线程是为了同步完成多个任务,不是为了提高程序运行效率,而是通过提高资源使用效率来提高系统的效率。

    什么才是真正的多线程?如果你的机器是双CPU,或者是双核,这确确实实是多线程。

     

    创建进程和线程

      在 windows 操作系统中,我们创建一个进程通常就是打开某个应用软件,这便在电脑中创建了一个进程。更原始一点的,我们在命令提示符中来做(我们以打开记事本这个进程为例):

      第一步:windows+R,输入cmd,打开 cmd 命令提示符

       

      第二步:在命令提示符中输入 notepad,按 Enter 键便会弹出记事本应用软件

       

     PS:常用的windows 应用软件命令

        1、regedit:打开注册表编辑器

        2、control:打开控制面板

        3、msconfig:打开系统配置

        4、gpedit.msc:打开本地组策略

        5、explorer:打开资源管理器

        6、taskmgr:任务管理器

        7、logoff:直接注销计算机

        8、osk:打开屏幕键盘

        9、calc:打开计算器

        10、mspaint:调出画图软件

        11、dxdiag:查看电脑详细配置信息

        12、mstsc:打开远程桌面连接

        13、systeminfo:查看计算机基本信息

        14、notepad:打开记事本

    在java中创建进程

    第一种方法:通过 Runtime 类的 exec() 方法来创建进程

    public class Runtime
    extends Object
    ①、表示当前进程所在的虚拟机实例,每个Java应用程序都有一个Runtime类的Runtime ,允许应用程序与运行应用程序的环境进行接口。
    ②、由于任何进程只会运行与一个虚拟机实例当中,即只会产生一个虚拟机实例(底层源码采用 单例模式)
    ③、当前运行时可以从getRuntime方法获得。

      由上面源码可以看到,构造器私有化了,即外部我们不能 new 一个新的 Runtime 实例,而内部给了我们一个获取 Runtime 实例的方法 getRuntime() 。

    通过 Runtime 类创建一个 记事本的 进程

    public class ProcessTest {
         
        public static void main(String[] args) throws Exception {
            Runtime run = Runtime.getRuntime();
            //打开记事本
            run.exec("notepad");
        }
     
    }

    第二种方法:通过 ProcessBuilder 创建进程

    public final class ProcessBuilder
    extends Object<br>①、此类用于创建操作系统进程。
    ②、每个ProcessBuilder实例管理进程属性的集合。 start()方法使用这些属性创建一个新的Process实例。 start()方法可以从同一实例重复调用,以创建具有相同或相关属性的新子进程。

    public class ProcessTest {
         
        public static void main(String[] args) throws Exception {
            //打开记事本
            ProcessBuilder pBuilder = new ProcessBuilder("notepad");
            pBuilder.start();
        }
     
    } 

    在java中创建进程

    第一种方法:继承 Thread 类

    public class Thread
    extends Object
    implements Runnable  

    步骤:1、定义一个线程类 A 继承于 java.lang.Thread 类

       2、在 A 类中覆盖 Thread 类的 run() 方法

       3、在 run() 方法中编写需要执行的操作

       4、在 main 方法(线程)中,创建线程对象,并启动线程

          创建线程类:A类 a = new A()类;

          调用 start() 方法启动线程:a.start();

    package com.ys.thread;
     
    class Thread1 extends Thread{
        @Override
        public void run() {
            for(int i = 0 ; i < 10 ;i++){
                System.out.println("播放音乐"+i);
            }
        }
    }
     
    public class ThreadTest {
        public static void main(String[] args) {
            for(int i = 0 ; i < 10 ; i++){
                System.out.println("玩游戏"+i);
                if(i==5){
                    Thread1 th1 = new Thread1();
                    th1.start();
                }
            }
        }
     
    }

     结果:

    玩游戏0
    玩游戏1
    玩游戏2
    玩游戏3
    玩游戏4
    玩游戏5
    玩游戏6
    玩游戏7
    玩游戏8
    玩游戏9
    播放音乐0
    播放音乐1
    播放音乐2
    播放音乐3
    播放音乐4
    播放音乐5
    播放音乐6
    播放音乐7
    播放音乐8
    播放音乐9

      注意:我们看结果,并不是出现 5 个先打游戏,然后在播放音乐,这是线程调度的结果,两个线程同时在争抢 CPU 的资源,即最后的结果,前面5个打游戏的必然先出现的,后面的啥时候出现播放音乐就看 CPU 怎么调度了,这是随机的。我们不能干涉。

    第二种方法:实现 Runnable 接口

    @FunctionalInterface
    public interface Runnable

    1、Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。 
    2、该接口旨在为希望在活动时执行代码的对象提供一个通用协议。此类整个只有一个 run() 抽象方法

     

    步骤:1、定义一个线程类 A 实现于 java.lang.Runnable 接口(注意:A类不是线程类,没有 start()方法,不能直接 new A 的实例启动线程)

       2、在 A 类中覆盖 Runnable 接口的 run() 方法

       3、在 run() 方法中编写需要执行的操作

       4、在 main 方法(线程)中,创建线程对象,并启动线程

          创建线程类:Thread t = new Thread( new A类() ) ;

          调用 start() 方法启动线程:t.start();

    class Runnable1 implements Runnable{
        @Override
        public void run() {
            for(int i = 0 ; i < 10 ;i++){
                System.out.println("播放音乐"+i);
            }
        }
    }
     
    public class RunnableTest {
        public static void main(String[] args) {
            for(int i = 0 ; i < 10 ; i++){
                System.out.println("玩游戏"+i);
                if(i==5){
                    Thread th1 = new Thread(new Runnable1());
                    th1.start();
                }
            }
        }
     
    }

    第三种方法:使用匿名内部类创建线程

    public static void main(String[] args) {
            for(int i = 0 ; i < 10 ; i++){
                System.out.println("玩游戏"+i);
                if(i==5){
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            for(int i = 0 ; i < 10 ;i++){
                                System.out.println("播放音乐"+i);
                            }
                        }
                    }).start();
                }
            }
        }

    注意:

    1、启动线程是调用 start() 方法,而不是 调用 run() 方法。

      解析:run()方法:在本线程内调用run()方法,和其他方法没有什么区别,可以重复多次调用;

         start()方法:启动一个线程,实际上还是调用该Runnable对象的run()方法。

         打开 Thread 类的源码,start()方法里面有一句:

            private native void start0();  //native 关键字:本地程序调用    

        native关键字指的是Java本地接口调用,即是使用Java调用本地操作系统的函数功能完成一些特殊的操作,而这样的代码开发在Java中几乎很少出现,因为Java的最大特点是可移植性,如果一个程序 只能在固定的操作系统上使用,那么可移植性就将彻底丧失,多线程的实现一定需要操作系统的支持,那么start0()方法实际上就和抽象方法很类似,没有方法体,而是交给JVM 去实现,即在windows下的JVM可能使用A方法实现start0(),在linux下的JVM可能使用B方法实现start0(),在调用时并不会关心具体是何方式实现了start0()方法,只会关心最终的操作结果,交给 JVM去匹配了不同的操作系统。

    2、不能多次启动同一个线程,即多次调用 start() 方法,只能调用一次,否则报错:

     

    线程的同步

    利用多线程模拟 3 个窗口卖票

    第一种方法:继承 Thread 类

     创建窗口类 TicketSell 

    public class TicketSell extends Thread{
        //定义一共有 50 张票,注意声明为 static,表示几个窗口共享
        private static int num = 50;
         
        //调用父类构造方法,给线程命名
        public TicketSell(String string) {
            super(string);
        }
        @Override
        public void run() {
            //票分 50 次卖完
            for(int i = 0 ; i < 50 ;i ++){
                if(num > 0){
                    try {
                        sleep(10);//模拟卖票需要一定的时间
                    } catch (InterruptedException e) {
                        // 由于父类的 run()方法没有抛出任何异常,根据继承的原则,子类抛出的异常不能大于父类, 故我们这里也不能抛出异常
                        e.printStackTrace();
                    }
                    System.out.println(this.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                }
            }
        }
         
     
    }

     创建主线程测试:

    public class TestTicket {
     
        public static void main(String[] args) {
            //创建 3 个窗口
            TicketSell t1 = new TicketSell("A窗口");
            TicketSell t2 = new TicketSell("B窗口");
            TicketSell t3 = new TicketSell("C窗口");
             
            //启动 3 个窗口进行买票
            t1.start();
            t2.start();
            t3.start();
        }
    }

      结果:这里我们省略了一些,根据电脑配置,结果会随机出现不同

    B窗口卖出一张票,剩余48张
    A窗口卖出一张票,剩余47张
    C窗口卖出一张票,剩余49张
    C窗口卖出一张票,剩余46张
    B窗口卖出一张票,剩余44张
    A窗口卖出一张票,剩余45张
    A窗口卖出一张票,剩余43张
    ...
    C窗口卖出一张票,剩余5张
    A窗口卖出一张票,剩余4张
    B窗口卖出一张票,剩余3张
    A窗口卖出一张票,剩余2张
    C窗口卖出一张票,剩余3张
    B窗口卖出一张票,剩余1张
    C窗口卖出一张票,剩余0张
    A窗口卖出一张票,剩余-1张

    第二种方法:实现 Runnable 接口

      创建窗口类 TicketSellRunnable

    public class TicketSellRunnable implements Runnable{
     
        //定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
        private int num = 50;
         
        @Override
        public void run() {
            //票分 50 次卖完
            for(int i = 0 ; i < 50 ;i ++){
                if(num > 0){
                    try {
                        //模拟卖一次票所需时间
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                }
            }
        }
     
    }

      创建主线程测试:

    public class TicketSellRunnableTest {
        public static void main(String[] args) {
            TicketSellRunnable t = new TicketSellRunnable();
             
            Thread t1 = new Thread(t,"A窗口");
            Thread t2 = new Thread(t,"B窗口");
            Thread t3 = new Thread(t,"C窗口");
             
            t1.start();
            t2.start();
            t3.start();
        }
     
    }

     结果:同理为了篇幅我们也省略了中间的一些结果

    B窗口卖出一张票,剩余49张
    C窗口卖出一张票,剩余48张
    A窗口卖出一张票,剩余49张
    B窗口卖出一张票,剩余47张
    A窗口卖出一张票,剩余45张
    ......
    A窗口卖出一张票,剩余4张
    C窗口卖出一张票,剩余5张
    A窗口卖出一张票,剩余3张
    B窗口卖出一张票,剩余2张
    C窗口卖出一张票,剩余1张
    B窗口卖出一张票,剩余0张
    A窗口卖出一张票,剩余-2张
    C窗口卖出一张票,剩余-1张

    结果分析:这里出现了票数为 负数的情况,这在现实生活中肯定是不存在的,那么为什么会出现这样的情况呢?

      

    解决办法分析:即我们不能同时让超过两个以上的线程进入到 if(num>0)的代码块中,不然就会出现上述的错误。我们可以通过以下三个办法来解决:

    1、使用 同步代码块

    2、使用 同步方法

    3、使用 锁机制

    ①、使用同步代码块

    语法:
    synchronized (同步锁) {
        //需要同步操作的代码         
    }
     
    同步锁:为了保证每个线程都能正常的执行原子操作,Java 线程引进了同步机制;同步锁也叫同步监听对象、同步监听器、互斥锁;
    Java程序运行使用的任何对象都可以作为同步监听对象,但是一般我们把当前并发访问的共同资源作为同步监听对象
     
    注意:同步锁一定要保证是确定的,不能相对于线程是变化的对象;任何时候,最多允许一个线程拿到同步锁,谁拿到锁谁进入代码块,而其他的线程只能在外面等着
    public void run() {
            //票分 50 次卖完
            for(int i = 0 ; i < 50 ;i ++){
                //这里我们使用当前对象的字节码对象作为同步锁
                synchronized (this.getClass()) {
                    if(num > 0){
                        try {
                            //模拟卖一次票所需时间
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                    }
                }
                 
            }
        }

    ②、使用 同步方法

    语法:即用  synchronized  关键字修饰方法

    @Override
        public void run() {
            //票分 50 次卖完
            for(int i = 0 ; i < 50 ;i ++){
                sell();
                 
            }
        }
        private synchronized void sell(){
            if(num > 0){
                try {
                    //模拟卖一次票所需时间
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
            }
        }

      注意:不能直接用 synchronized 来修饰 run() 方法,因为如果这样做,那么就会总是第一个线程进入其中,而这个线程执行完所有操作,即卖完所有票了才会出来。

    死锁问题

    解决线程死锁的问题最好只锁定一个对象,不要同时锁定两个对象

     /*这个小程序模拟的是线程死锁的问题*/
     public class TestDeadLock implements Runnable {
         public int flag = 1;
         static Object o1 = new Object(), o2 = new Object();
     
         public void run() {
            System.out.println(Thread.currentThread().getName() + "的flag=" + flag);
             /*
              * 运行程序后发现程序执行到这里打印出flag以后就再也不往下执行后面的if语句了 
              * 程序也就死在了这里,既不往下执行也不退出
              */
     
             /* 这是flag=1这个线程 */
             if (flag == 1) {
                 synchronized (o1) {
                    /* 使用synchronized关键字把对象01锁定了 */                try {
                         Thread.sleep(500);
                     } catch (InterruptedException e) {
                         e.printStackTrace();                 }
                     synchronized (o2) {
                         /*
                          * 前面已经锁住了对象o1,只要再能锁住o2,那么就能执行打印出1的操作了 
                          * 可是这里无法锁定对象o2,因为在另外一个flag=0这个线程里面已经把对象o1给锁住了 
                          * 尽管锁住o2这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o2不放的
                          */
                         System.out.println("1");
                     }
                 }
             }
            /*
              * 这里的两个if语句都将无法执行,因为已经造成了线程死锁的问题 
             * flag=1这个线程在等待flag=0这个线程把对象o2的锁解开, 
              * 而flag=0这个线程也在等待flag=1这个线程把对象o1的锁解开           * 然而这两个线程都不愿意解开锁住的对象,所以就造成了线程死锁的问题
              */
     
             /* 这是flag=0这个线程 */
             if (flag == 0) {
                 synchronized (o2) {
                     /* 这里先使用synchronized锁住对象o2 */
                     try {
                         Thread.sleep(500);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     synchronized (o1) {
                         /*
                          * 前面已经锁住了对象o2,只要再能锁住o1,那么就能执行打印出0的操作了 可是这里无法锁定对象o1,因为在另外一个flag=1这个线程里面已经把对象o1给锁住了 尽管锁住o1这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o1不放的
                          */
                         System.out.println("0");
                     }
                 }
             }
         }
     
         public static void main(String args[]) {
             TestDeadLock td1 = new TestDeadLock();
             TestDeadLock td2 = new TestDeadLock();
             td1.flag = 1;
             td2.flag = 0;
             Thread t1 = new Thread(td1);
             Thread t2 = new Thread(td2);
             t1.setName("线程td1");
             t2.setName("线程td2");
             t1.start();
             t2.start();
         }
     }

    ③、使用 锁机制

    public interface Lock

     主要方法:

    常用实现类:

    public class ReentrantLock
    extends Object
    implements Lock, Serializable<br>//一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。

    例子:

    package com.ys.thread;
     
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    public class TicketSellRunnable implements Runnable{
     
        //定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
        private int num = 50;
        //创建一个锁对象
        Lock l = new ReentrantLock();
         
        @Override
        public void run() {
            //票分 50 次卖完
            for(int i = 0 ; i < 50 ;i ++){
                //获取锁
                l.lock();
                try {
                    if(num > 0){
                    //模拟卖一次票所需时间
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }finally{
                    //释放锁
                    l.unlock();
                }
                 
                 
            }
        }
        private void sell(){
             
        }
     
    }

     

    生产者和消费者

    生产者和消费者模型为:生产者Producer 生产某个对象(共享资源),放在缓冲池中,然后消费者从缓冲池中取出这个对象。也就是生产者生产一个,消费者取出一个。这样进行循环。

    产者--消费者问题问题:共享数据的不一致性/临界资源的保护

     第一步:我们先创建共享资源的类 Person,它有两个方法,一个生产对象,一个消费对象

    public class Person {
        private String name;
        private int age;
         
        /**
         * 生产数据
         * @param name
         * @param age
         */
        public void push(String name,int age){
            this.name = name;
            this.age = age;
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public void pop(){
            System.out.println(this.name+"---"+this.age);
        }
    }

    第二步:创建生产者线程,并在 run() 方法中生产50个对象

    ublic class Producer implements Runnable{
        //共享资源对象
        Person p = null;
        public Producer(Person p){
            this.p = p;
        }
        @Override
        public void run() {
            //生产对象
            for(int i = 0 ; i < 50 ; i++){
                //如果是偶数,那么生产对象 Tom--11;如果是奇数,则生产对象 Marry--21
                if(i%2==0){
                    p.push("Tom", 11);
                }else{
                    p.push("Marry", 21);
                }
            }
        }
    }

    第三步:创建消费者线程,并在 run() 方法中消费50个对象

    public class Consumer implements Runnable{
        //共享资源对象
        Person p = null;
        public Consumer(Person p) {
            this.p = p;
        }
         
        @Override
        public void run() {
            for(int i = 0 ; i < 50 ; i++){
                //消费对象
                p.pop();
            }
        }
    }

      由于我们的模型是生产一个,马上消费一个,那期望的结果便是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次

    但是结果却是:

    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    ......
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Marry---21

    为了让结果产生的更加明显,我们在共享资源的 pop() 和 push() 方法中添加一段延时代码

    /**
         * 生产数据
         * @param name
         * @param age
         */
        public void push(String name,int age){
            this.name = name;
            try {
                //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.age = age;
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public void pop(){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.name+"---"+this.age);
        }  

    这个时候,结果如下:

    Marry---11
    Tom---21
    Marry---11
    Tom---21
    Marry---11
    Tom---21
    Marry---11
    Tom---21
    ......
    Tom---11
    Tom---21
    Marry---11
    Tom---21
    Marry---11
    Marry---21

    结果分析:这时候我们发现结果全乱套了,Marry--21是固定的,Tom--11是固定的,但是上面的结果全部乱了,那这又是为什么呢?而且有很多重复的数据连续出现,那这又是为什么呢?

    原因1:出现错乱数据,是因为先生产出Tom--11,但是消费者没有消费,然后生产者继续生产出name为Marry,但是age还没有生产,而消费者这个时候拿去消费了,那么便出现 Marry--11。同理也会出现 Tom--21

    原因2:出现重复数据,是因为生产者生产一份数据了,消费者拿去消费了,但是第二次生产者生产数据了,但是消费者没有去消费;而第三次生产者继续生产数据,消费者才开始消费,这便会产生重复

    解决办法1:生产者生产name和age必须要是一个整体一起完成,即同步。生产的中间不能让消费者来消费即可。便不会产生错乱的数据。这里我们选择同步方法(在方法前面加上 synchronized)

    public class Person {
        private String name;
        private int age;
         
        /**
         * 生产数据
         * @param name
         * @param age
         */
        public synchronized void push(String name,int age){
            this.name = name;
            try {
                //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.age = age;
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public synchronized void pop(){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.name+"---"+this.age);
        }
    }

     结果如下:

    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Tom---11
    Tom---11
    ......
    Tom---11
    Tom---11
    Tom---11
    Tom---11
    Tom---11

    问题:还是没有解决上面的问题2,出现重复的问题。期望的结果是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次。那如何解决呢?

    解决办法:生产者生产一次数据了,就暂停生产者线程,等待消费者消费;消费者消费完了,消费者线程暂停,等待生产者生产数据,这样来进行。

     

    同步锁池的概念:

      同步锁池:同步锁必须选择多个线程共同的资源对象,而一个线程获得锁的时候,别的线程都在同步锁池等待获取锁;当那个线程释放同步锁了,其他线程便开始由CPU调度分配锁

    关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)

     

    wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程

    notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)

    notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。

    注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错:        java.lang.IllegalMonitorStateException

    假设 A 线程和 B 线程同时操作一个 X 对象,A,B 线程可以通过 X 对象的 wait() 和 notify() 方法来进行通信,流程如下:

    ①、当线程 A 执行 X 对象的同步方法时,A 线程持有 X 对象的 锁,B线程在 X 对象的锁池中等待

    ②、A线程在同步方法中执行 X.wait() 方法时,A线程释放 X 对象的锁,进入 X 对象的等待池中

    ③、在 X 对象的锁池中等待锁的 B 线程获得 X 对象的锁,执行 X 的另一个同步方法

    ④、B 线程在同步方法中执行 X.notify() 方法,JVM 把 A 线程从等待池中移动到 X 对象的锁池中,等待获取锁

    ⑤、B 线程执行完同步方法,释放锁,等待获取锁的 A 线程获得锁,继续执行同步方法

    那么为了解决上面重复的问题,修改代码如下:

    public class Person {
        private String name;
        private int age;
         
        //表示共享资源对象是否为空,如果为 true,表示需要生产,如果为 false,则有数据了,不要生产
        private boolean isEmpty = true;
        /**
         * 生产数据
         * @param name
         * @param age
         */
        public synchronized void push(String name,int age){
            try {
                //不能用 if,因为可能有多个线程
                while(!isEmpty){//进入到while语句内,说明 isEmpty==false,那么表示有数据了,不能生产,必须要等待消费者消费
                    this.wait();//导致当前线程等待,进入等待池中,只能被其他线程唤醒
                }
                 
                //-------生产数据开始-------
                this.name = name;
                //延时代码
                Thread.sleep(10);
                this.age = age;
                //-------生产数据结束-------
                isEmpty = false;//设置 isEmpty 为 false,表示已经有数据了
                this.notifyAll();//生产完毕,唤醒所有消费者
            } catch (Exception e) {
                e.printStackTrace();
            }
             
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public synchronized void pop(){
            try {
                //不能用 if,因为可能有多个线程
                while(isEmpty){//进入 while 代码块,表示 isEmpty==true,表示为空,等待生产者生产数据,消费者要进入等待池中
                    this.wait();//消费者线程等待
                }
                //-------消费开始-------
                Thread.sleep(10);
                System.out.println(this.name+"---"+this.age);
                //-------消费结束------
                isEmpty = true;//设置 isEmpty为true,表示需要生产者生产对象
                this.notifyAll();//消费完毕,唤醒所有生产者
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    结果:

    Tom---11
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    ......
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    Marry---21  

    那么这便是我们期待的结果,交替出现。

     

    死锁:

    ①、多线程通信的时候,很容易造成死锁,死锁无法解决,只能避免

    ②、当 A 线程等待由 B 线程持有的锁,而 B 线程正在等待由 A 线程持有的锁时发生死锁现象(比如A拿着铅笔,B拿着圆珠笔,A说你先给我圆珠笔,我就把铅笔给你,而B说你先给我铅笔,我就把圆珠笔给你,这就造成了死锁,A和B永远不能进行交换)

    ③、JVM 既不检测也不避免这种现象,所以程序员必须保证不能出现这样的情况

    Thread 类中容易造成死锁的方法(这两个方法都已经过时了,不建议使用):

    suspend():使正在运行的线程放弃 CPU,暂停运行(不释放锁)

    resume():使暂停的线程恢复运行

    情景:A 线程获得对象锁,正在执行一个同步方法,如果 B线程调用 A 线程的 suspend() 方法,此时A 暂停运行,放弃 CPU 资源,但是不放弃同步锁,那么B也不能获得锁,A又暂停,那么便造成死锁。

    解决死锁法则:当多个线程需要访问 共同的资源A,B,C时,必须保证每一个线程按照一定的顺序去访问,比如都先访问A,然后B,最后C。就像我们这里的生产者---消费者模型,制定了必须生产者先生产一个对象,然后消费者去消费,消费完毕,生产者才能在开始生产,然后消费者在消费。这样的顺序便不会造成死锁。

     

    线程的生命周期

    线程是一个动态执行的过程,它也有从创建到死亡的过程。

    在 Thread 类中,有一个枚举内部类:

      

    上面的信息以图片表示如下:

      第一张图:

      

    第二张图:把等待、计时等待、阻塞看成阻塞一个状态了

      

    1、新建状态(new):使用 new 创建一个线程,仅仅只是在堆中分配了内存空间

               新建状态下,线程还没有调用 start()方法启动,只是存在一个线程对象而已

              Thread t = new Thread();//这就是t线程的新建状态

    2、可运行状态(runnable):新建状态调用 start() 方法,进入可运行状态。而这个又分成两种状态,ready 和 running,分别表示就绪状态和运行状态

        就绪状态:线程对象调用了 start() 方法,等待 JVM 的调度,(此时该线程并没有运行)

        运行状态:线程对象获得 JVM 调度,如果存在多个 CPU,那么运行多个线程并行运行

      注意:线程对象只能调用一次 start() 方法,否则报错:illegaThreadStateExecptiong

    3、阻塞状态(blocked):正在运行的线程因为某种原因放弃 CPU,暂时停止运行,就会进入阻塞状态。此时 JVM 不会给线程分配 CPU,知道线程重新进入就绪状态,才有机会转到 运行状态。

      注意:阻塞状态只能先进入就绪状态,不能直接进入运行状态

      阻塞状态分为两种情况:

        ①、当线程 A 处于可运行状态中,试图获取同步锁时,却被 B 线程获取,此时 JVM 把当前 A 线程放入锁池中,A线程进入阻塞状态

        ②、当线程处于运行状态时,发出了 IO 请求,此时进入阻塞状态

    4、等待状态(waiting):等待状态只能被其他线程唤醒,此时使用的是无参数的 wait() 方法

      ①、当线程处于运行状态时,调用了 wait() 方法,此时 JVM 把该线程放入等待池中

    5、计时等待(timed waiting):调用了带参数的 wait(long time)或 sleep(long time) 方法

      ①、当线程处于运行状态时,调用了带参数 wait 方法,此时 JVM 把该线程放入等待池中

      ②、当前线程调用了 sleep(long time) 方法

    6、终止状态(terminated):通常称为死亡状态,表示线程终止

      ①、正常终止,执行完 run() 方法,正常结束

      ②、强制终止,如调用 stop() 方法或 destory() 方法

      ③、异常终止,执行过程中发生异常

    下面详细介绍线程的几种方法:

      1、sleep(long millis)线程休眠:让执行的线程暂停一段时间,进入计时等待状态。

        static void sleep(long millis):调用此方法后,当前线程放弃 CPU 资源,在指定的时间内,sleep 所在的线程不会获得可运行的机会,此状态下的线程不会释放同步锁(注意和 wait() 的区别,wait 会放弃 CPU 资源,同时也会放弃 同步锁)

        该方法更多的是用来模拟网络延迟,让多线程并发访问同一资源时的错误效果更加明显。

       2、join()联合线程:表示这个线程等待另一个线程完成后(死亡)才执行,join 方法被调用之后,线程对象处于阻塞状态。写在哪个线程中,哪个线程阻塞

        这种也称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程

    class Join extends Thread{
        @Override
        public void run() {
            for(int i = 0 ; i < 10 ;i++){
                System.out.println("播放音乐"+i);
            }
        }
    }
     
    public class ThreadTest {
        public static void main(String[] args) {
            //创建 join 线程对象
            Join joinThread = new Join();
            for(int i = 0 ; i < 10 ; i++){
                System.out.println("玩游戏"+i);
                if(i==3){
                    joinThread.start();
                }
                if(i==5){
                    try {
                        joinThread.join();//强制运行 join 线程,知道 join 运行完毕了,main 才有机会运行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
     
    }

    结果:

    玩游戏0
    玩游戏1
    玩游戏2
    玩游戏3
    玩游戏4
    玩游戏5
    播放音乐0
    播放音乐1
    播放音乐2
    播放音乐3
    播放音乐4
    播放音乐5
    播放音乐6
    播放音乐7
    播放音乐8
    播放音乐9
    玩游戏6
    玩游戏7
    玩游戏8
    玩游戏9

     

    后台线程(守护线程):在后台运行的线程,其目的是为其他线程提供服务,也称为“守护线程”。

    ①、JVM 的垃圾回收线程就是守护线程。

    ②、main 方法是前台线程,不是后台线程

      

    public static void main(String[] args) {
            String mainThreadName = Thread.currentThread().getName();
            System.out.println(mainThreadName);  //main
             
            System.out.println(Thread.currentThread().isDaemon());//false
             
        }

    特点:

    ①、若所有的前台线程都死亡,则后台线程自动死亡;

    ②、前台线程没有结束,后台线程是不会结束的;

    ③、前台线程创建的线程是前台线程,后台线程创建的线程是后台线程。

      Thread.setDaemon(Boolean on)必须在 start() 的方法前调用。否则会报错。

     

    线程的优先级:

      每个线程都有一个优先级,这有助于 系统确定线程的调动顺序。

      Java 线程的优先级是一个整数,取值范围是:1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )

      默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

      具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

    public class TestThread6 {
        public static void main(String args[]) {
            MyThread4 t4 = new MyThread4();
            MyThread5 t5 = new MyThread5();
            Thread t1 = new Thread(t4);
            Thread t2 = new Thread(t5);
            t1.setPriority(Thread.NORM_PRIORITY + 3);// 使用setPriority()方法设置线程的优先级别,这里把t1线程的优先级别进行设置
            /*
             * 把线程t1的优先级(priority)在正常优先级(NORM_PRIORITY)的基础上再提高3级 
             * 这样t1的执行一次的时间就会比t2的多很多     
             * 默认情况下NORM_PRIORITY的值为5
             */
            t1.start();
            t2.start();
            System.out.println("t1线程的优先级是:" + t1.getPriority());
            // 使用getPriority()方法取得线程的优先级别,打印出t1的优先级别为8
        }
    }
    
    class MyThread4 implements Runnable {
        public void run() {
            for (int i = 0; i <= 1000; i++) {
                System.out.println("T1:" + i);
            }
        }
    }
    
    class MyThread5 implements Runnable {
        public void run() {
            for (int i = 0; i <= 1000; i++) {
                System.out.println("===============T2:" + i);
            }
        }
    }

    run()方法一结束,线程也就结束了。

     

    线程礼让:yield()方法:表示当前线程对象提示调度器自己愿意让出 CPU 资源,但是调度器可以自由的忽略该提示。

    调用该方法后,线程对象进入就绪状态,所以完全有可能:某个线程调用了 yield() 方法,但是线程调度器又把它调度出来重新执行。

     package cn.galc.test;
     
     public class TestThread5 {
         public static void main(String args[]) {
             MyThread3 t1 = new MyThread3("t1");
             /* 同时开辟了两条子线程t1和t2,t1和t2执行的都是run()方法 */
             /* 这个程序的执行过程中总共有3个线程在并行执行,分别为子线程t1和t2以及主线程 */
             MyThread3 t2 = new MyThread3("t2");
             t1.start();// 启动子线程t1
             t2.start();// 启动子线程t2
             for (int i = 0; i <= 5; i++) {
                 System.out.println("I am main Thread");
             }
         }
     }
     
     class MyThread3 extends Thread {
         MyThread3(String s) {
             super(s);
         }
     
         public void run() {
             for (int i = 1; i <= 5; i++) {
                 System.out.println(getName() + ":" + i);
                 if (i % 2 == 0) {
                     yield();// 当执行到i能被2整除时当前执行的线程就让出来让另一个在执行run()方法的线程来优先执行
                     /*
                      * 在程序的运行的过程中可以看到,
                      * 线程t1执行到(i%2==0)次时就会让出线程让t2线程来优先执行 
                      * 而线程t2执行到(i%2==0)次时也会让出线程给t1线程优先执行
                      */
                 }
             }
         }
     }

    运行结果如下:

    从 Java7 提供的文档上可以清楚的看出,开发中会很少使用该方法,该方法主要运用于调试或测试,它可能有助于多线程竞争条件下的错误重现现象。

    sleep() 和 yield() 方法的区别:

      ①、都能使当前处于运行状态的线程放弃 CPU资源,把运行的机会给其他线程

      ②、sleep 方法会给其他线程运行的机会,但是不考虑其他线程优先级的问题;yield 方法会优先给更高优先级的线程运行机会

      ③、调用 sleep 方法后,线程进入计时等待状态,调用 yield 方法后,线程进入就绪状态。

     

     

     

     

     

     

     

    展开全文
  • 比如当用户编写Word文档时,不断进行拼写检查的线程就是个守护线程,它不会影响用户编辑文件。典型的守护线程例子是JVM中的系统资源自动回收线程,它始终在低级别的状态中运行,不需要占用大量的系统资源,用于...

    1、  java中用两种线程:

     

    1  守护线程

     

          守护线程(Daemon) 是比较特殊的一种低级别线程,一般被用于在后台为其它线程提供服务。比如当用户编写Word文档时,不断进行拼写检查的线程就是个守护线程,它不会影响用户编辑文件。典型的守护线程例子是JVM中的系统资源自动回收线程,它始终在低级别的状态中运行,不需要占用大量的系统资源,多用于实时监控和管理系统中的可回收资源。

     

     

           

    可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,而将一个用户线程设置为守护线程的方法是在线程对象创建之前调用线程对象的setDaemon(boolean on)方法。true为守护线程,false为用户线程。

     

    public class Daemons extends Thread {
    
    	public Daemons(){
    		this.setDaemon(true);//设置线程为守护线程
    		/*setDaemon必须在线程启动之前调用,当线程运行时调用会发生异常
    		如果要在一个守护线程中产生其他线程,那么新产生的线程都是守护线程
    		 */
    		this.start();
    	}
    		public void run(){
    			while(true){
    				System.out.println("daemon "+Thread.currentThread().getName()+" is running");
    				try {
    					sleep(500);
    				} catch (InterruptedException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    			}
    		}
    		
    	
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		for(int i=0;i<5;i++){
    			new Daemons();
    		}
    	}
    
    }
    


    运行结果:

     

    2)  用户线程

    守护线程和普通线程的区别就在于守护线程不是应用程序的核心部分。当一个应用程序的所有非守护线程终止运行时,及时仍然有守护线程运行,应用程序也将终止。反之,只要有一个非守护线程运行,应用程序就不会终止。

     

     

     

     

    展开全文
  • 工程师面试/工作必知必会:Java 多线程与并发编程」,「阅读原文」查看交流实录 「文末高能」 编辑 | 雷诺 一、Java-Thread 概念 我们想搞懂多线程必须先明白以下几个重要概念。 什么是进程...

    640.gif?wxfrom=5&wx_lazy=1

    本文来自作者 张振华 在 GitChat 上分享「Java 工程师面试/工作必知必会:Java 多线程与并发编程」,阅读原文」查看交流实录

    文末高能

    编辑 | 雷诺

    一、Java-Thread 概念

    我们想搞懂多线程必须先明白以下几个重要概念。

    1. 什么是进程

      是资源分配的最小单位;(资源,包括各种表格、内存空间、磁盘空间) 同一进程中的多条线程将共享该进程中的全部系统资源。

    2. 什么是线程

      线程是CPU调度的最小单位。线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表组成。 而寄存器可被用来存储线程内的局部变量。

    3. 什么是并行和并发

    • 并行运行:总线程数<=CPU数量*核心数。

    • 并发运行:总线程数>CPU数量*核心数。 (如:有的操作系统CPU线程切换之间用的时间片轮转进程调度算法)

    线程创建的4个方法大家想一下。

    二、安全和锁

    Java 里面如果谈到线程,最核心要搞明白的就是线程安全和线程锁的问题。

    1. 何为安全

    我先问一下各位小伙伴什么叫线程安全或者是不安全的?思考一下:何为安全???思考2分钟。

    我总结出来的一个定理啊,一定要铭记:

    Jack定理1:

    离开单例、全局共享变量来谈线程安全问题都是耍流氓。

    那么问题来了?单例的一定是线程不安全的吗?答案是否定的,只要你单例的类里面没有全局变量那一定是线程安全的。

    所以只有单例模式下共享全局变量的时候才会有线程不安全的问题,这个时候我们就要引入锁的概念了。

    2. 锁

    经常在工作中听到我们的小伙伴们谈论什么乐观锁,悲观锁,排它锁,共享锁等等,但记住这些只是结果。在我们Java中我认为只有两种锁:隐式锁和显示锁两种实现手段。

    隐式锁: synchronized

    1. 同一个对象锁下面的, synchronized 区域是互斥的

    2. 方法锁(默认是当前对象的锁)

    3. 代码快锁(性能高于方法锁,可以指定哪个对象的锁)

    Jack 定理2:

    离开对象来谈 synchronized,也是耍流氓。synchronized 一定是加在对象上的切记。

    使用方法案例:

    public synchronized void updateUser(UserInfo userInfo){    。。。。//共享数据操作 } public  void updateUser(UserInfo userInfo){    synchronized(this) {    。。。。//共享数据操作    } }

    显示锁:java.util.concurrent.lock  

    1. 需要手动关/开

    2. 注意自己的代码逻辑不要产生死锁了

    使用案例:

    public void updateUser(UserInfo userInfo){    Lock lock = new ReentrantLock();    lock.lock();//加锁    try {        。。。。//共享数据操作    } finally {        lock.unlock();//释放锁,一定要释放    } }

    3. synchronized 与 lock 区别

    • lock更灵活,方法更多,能实现各种锁的场景。

    • 性能上如果都指定锁都是一个对象,那基本上没什么差别。

    • 默认情况下synchronized锁是当前对象,而lock是不一样的。

    三、Concurrent 包

    java.util.concurrent 包是必须要了解的,如果你不知道有这个包的存在就别谈多线程。

    我们可以把这个包下面的内容分成四部分

    1. 原子性操作类

    原子操作(atomic operation)是不需要 synchronized,也可以实现多线程的安全,效率要比 lock 高很多。

    底层是通过一定的算法将内存中分割了一个独立排它的内存空间,来做单线程操作。目前只有一些AtomicBoolean、AtomicInteger、AtomicLong等一些基本类型。

    2. 线程队列

    我们学习数据结构的时候都知道有栈和队列两种结构,而Java给我提供了一些线程安全的队列操作的类。

    0?wx_fmt=jpeg

    而其中关键的几个类,我们大概介绍一下:

    0?wx_fmt=jpeg

    • BlockingQueue 很好的解决了多线程中高效安全“传输”数据的问题;基于 java.util.Queue 的基础上做了一些线程安全的封装;

    • ArrayBlockingQueue 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

    • LinkedBlockingQueue 基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列。

    • DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。

      DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

      使用场景:DelayQueue 使用场景较少,但都相当巧妙,常见的例子比如使用一个 DelayQueue 来管理一个超时未响应的连接队列。

    • PriorityBlockingQueue 基于优先级的阻塞队列(优先级的判断通过构造函数传入的 Compator 对象来决定)。

      但需要注意的是 PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

    • SynchronousQueue 一种无缓冲的等待队列,同步队列没有任何内部容量,甚至连一个队列的容量都没有;

      其中每个 put 必须等待一个 take,反之亦然。无锁的机制实现(可想而知高并发的时候性能肯定是最高的)。

    关于队列只介绍个大概,大家知道有这么回事,具体使用可以查询相关的API文档。为什么要提一下呢,因为我们在说明线程池的时候有用到安全队列。

    3. 线程阀

    线程阀:控制线程的开(开始)与关(结束)。如果用Queue来管理线程的队列即开始,那么用线程阀管理整体线程的调配工作,即线程结束之后的开与关。我们这里大概介绍4个类:

    1. CountDownLatch 是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。

      当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

    2. CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。

      在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。

      因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

    3. Semaphore:一个计数信号量。从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。就像一个排队进入上海博物馆一样,放几个人等一下,有几个人走了然后再放几个人进入,像是一种排队机制。

    4. Future->FutureTask:一般FutureTask多用与耗时的计算,主线程再完成自己的任务后,再去获取结果。只有在计算完成时获取,否则会一直阻塞直到任务完成状态。

    具体语法和使用可以查询相关文档。

    4.  Java 提供的线程安排工具类

    java.util.concurrent.ConcurrentHashMap   java.util.concurrent.ConcurrentLinkedQueue   java.util.concurrent.ConcurrentMap   java.util.concurrent.ConcurrentNavigableMap   java.util.concurrent.ConcurrentSkipListMap   java.util.concurrent.ConcurrentSkipListSet

    ……..等等基于lock的算法实现

    5.  volatile关键字

    我们通过查看源码,会发现 java 的另外一个关键字volatile,线程在每次使用变量的时候,都会读取变量修改后的最的值。(其实是有风险的,并行情况下不一定正确,有可能两个线程同时取到最后修改的值)

    四、线程池

    1. 线程池要解决的问题:

    我们掌握线程池必须要明白线程池要接解决的两个问题:

    • 解决频繁创建线程所产生的开销。减少在创建和销毁线程上所花的时间以及系统资源的开销。

    • 解决无限制的创建线程引起的系统崩溃。如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及“过度切换”  

    2. Executors给我们提供的四种创建线程池的方法

    创建一个可重用固定线程数的线程池  

    ExecutorService pool = Executors.newFixedThreadPool(5);

    newFixedThreadPool的参数指定了可以运行的线程的最大数目,超过这个数目的线程加进去以后,不会立马运行,会做队列等待。其次,加入线程池的线程属于托管状态,线程的运行不受加入顺序的影响

    单任务线程池

    ExecutorService pool = Executors.newSingleThreadExecutor();

    一个一个执行,这种基本上很少用到。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    可变尺寸的线程池

    ExecutorService pool = Executors.newCachedThreadPool();

    如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

    定时以及周期性执行任务的线程池  

    ScheduledThreadPoolExecutor exec = Executors.ScheduledThreadPoolExecutor(1);        exec.scheduleAtFixedRate(new Runnable() {                      publicvoid run() { .....每隔一段时间就触发的线程内容                      }                  }, 1000, 5000,TimeUnit.MILLISECONDS);

    3. 自定义线程池

    我们查看Executors的源码发现底层都是调用ThreadPoolExecutor来实现的,里面有几个重要参数我们一定要记一下:

    public static ExecutorService newCachedThreadPool() {        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                      60L, TimeUnit.SECONDS,                                      new SynchronousQueue<Runnable>());    }    public static ExecutorService newFixedThreadPool(int nThreads) {        return new ThreadPoolExecutor(nThreads, nThreads,                                      0L, TimeUnit.MILLISECONDS,                                      new LinkedBlockingQueue<Runnable>());    }      //ThreadPoolExecutor构造器    public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {        if (corePoolSize < 0 ||            maximumPoolSize <= 0 ||            maximumPoolSize < corePoolSize ||            keepAliveTime < 0)            throw new IllegalArgumentException();        if (workQueue == null || threadFactory == null || handler == null)            throw new NullPointerException();        this.corePoolSize = corePoolSize;        this.maximumPoolSize = maximumPoolSize;        this.workQueue = workQueue;        this.keepAliveTime = unit.toNanos(keepAliveTime);        this.threadFactory = threadFactory;        this.handler = handler;    }

    • corePoolSize:池中所保存的线程数,包括空闲线程。

    • maximumPoolSize:池中允许的最大线程数。

    • keepAliveTime: 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

    • unit: keepAliveTime参数的时间单位。

    • BlockingQueue: 执行前用于保持任务的队列。此队列仅保持由execute方法提交的Runnable任务。常见的三种队列:

      • 直接提交。SynchronousQueue。

      • 无界队列。使用无界队列(例如,不具有预定义容量的LinkedBlockingQueue)

      • 有界队列。当使用有限maximumPoolSizes时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽。

    • ThreadFactory: 执行程序创建新线程时使用的工厂。默认情况下为Executors.defaultThreadFactory():我们可以采用自定义的ThreadFactory工厂,增加对线程创建与销毁等更多的控制。

    • RejectedExecutionHandler: (拒绝策略)由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

      • AbortPolicy(默认):这种策略直接抛出异常,丢弃任务。

      • DiscardPolicy:不能执行的任务将被删除;这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。

      • DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务被删除,然后重试执行程序(如果再次失败,则重复此过程)。

      • CallerRunsPolicy: 使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。就像是个急脾气的人,我等不到别人来做这件事就干脆自己干。

      • 当然也可以自定义。

    Jack定理3:

    离开全局和单例谈论线程池那也是耍流氓。工作中看到有人把线程池写在方法里面的局部变量,那有用吗?

    4. 线程的监控和分析方法

    VisualVM的使用

    VisualVM 是 JDK 的一个集成的分析工具,自从JDK 6 Update 7以后已经作为 Sun 的 JDK 的一部分。

    VisualVM 可以做的:监控应用程序的性能和内存占用情况、监控应用程序的线程、进行线程转储(Thread Dump)或堆转储(Heap Dump)、跟踪内存泄漏、监控垃圾回收器、执行内存和CPU分析,保存快照以便脱机分析应用程序。

    0?wx_fmt=png

    Jconsole的使用

    JConsole 是一个内置 Java 性能分析器,可以从命令行或在 GUI shell 中运行。

    0?wx_fmt=png

    在Java Visualvm工具里面安装JTA插件

    0?wx_fmt=png

    利用linux的top&jstack命令

    例如:top先找到Java进程,top -p 8442 -H 找到哪个线程,jstack 8442> ./8442_dump.txt输出thread的demp文件。

    在实际生产环境,一般我们都是自己公司的监控平台的,只需要到各大监控平台开 thread 即可,内容基本上一样。

    Jack 定理4:

    任何 Java 执行的类和相关信息都在堆栈里面,就是我们如何想办法看到他们的问题,万变不离其宗。

    五、线程和线程池工作中的应用场景:

    1. ervlet 我们java开发最基本的东西,其启动的时候其实是开辟了一个main线程的。

      而其中 servlet 类是单例的所以它是线程不安全的,但是在没有共享全局变量的情况,而 reqest 和 response 是一个请求是一个实例,而其本身的数据设计又是线程安全的。

    2. Tomcat Servlet 的容器 tomcat 其实是对线程的线程池做了控制的。提高请求处理效率和避免请求太多把容器弄挂。

    3. Spring 默认加载 bean 的方式是单例的,所以其是线程不安全的。

    4. 数据库连接池,其实也是多线程。

    5. nginx 前端网关请求,也是利用了线程池的原理。

    6. 而我们的客户端ios,android其实也都是有主线程和子线程的说法,如果你能很好的将器里面的线程掌握基本上此种客户端开发就能掌握一半。

    Jack定理5:

    线程无处不在,线程池也无处不在,只不过是换不同的马甲,不通形式存在就看你知道不知道。

    Jack一句话总结:

    Java线程是围绕着java的进程的共享内存的管理和数据访问,而围绕线程本身的管理产生了通讯,争抢和队列管理的线程池。

    六、开放性问题?

    1. 线程安全和数据库数据线程安全是一回事吗?

    2. 我们实际工作中服务的最大并发量是多少?为什么?

    3. 除了数据库连接池,我们还有在哪些地方用过线程池?

    4. 你面试的时候多线程你都被问到了哪些问题?

    以上内容是对《Java并发编程从入门到精通》本书的内容的高度概括。大家可以来讨论欢迎留言!

    近期热文

    沉迷前端,无法自拔的人,如何规划职业生涯?

    区块链在哪些案例上发挥着重大作用

    Java 实现 Web 应用中的定时任务

    业务团队如何高效实施自动化测试

    入行 AI,如何选个脚踏实地的岗位


    《GitChat 达人课:Gradle 从入门到实战

    0?wx_fmt=jpeg

    「阅读原文」看交流实录,你想知道的都在这里


    展开全文
  • JAVA_API1.6文档(中文)

    万次下载 热门讨论 2010-04-12 13:31:34
    java.util.concurrent.atomic 类的小工具包,支持在单个变量上解除锁的线程安全编程。 java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 ...
  • 部分主要阐述 Thread 的基础知识,详细介绍线程的 API 使用、线程安全、线程间数据通信,以及如何保护共享资源等内容,它是深入学习多线程内容的基础。 第二部分引入了 ClassLoader,这是因为 ClassLoader 与线程...
  • Java-Thread00之多线程知识准备

    万次阅读 2019-10-12 17:02:21
    之前对多线程一直有排斥心理,每一次学一点点就放弃了,最近狠下心来从新学习,顺便做一个笔记,供大家参考一下。 demo:https://github.com/LiJinHongPassion/ThreadTest 后期学习目录 thread01:创建线程thread02...
  • Java多线程的监控分析工具(VisualVM)

    千次阅读 2012-11-23 20:24:06
    Java多线程程序运行时,多数情况下我们不知道到底发生了什么,只有出了错误的日志的时候,我们才知道原来代码中有死锁。撇开代码检查工具,我们先讨论一下利用VisualVM监控,分析我们的多线程的运行情况。(注:...
  • Email多线程异步发送设计文档 1 概述  系统中经常需要发送邮件通知用户,比较常见的是同步发送邮件,即在执行用户业务逻辑的线程中执行发送邮件动作;这样会造成发送邮件动作堵塞正常的业务逻辑执行,影响系统...
  • java基础(五):谈谈java中的多线程

    千次阅读 2019-02-26 02:16:09
    1.多线程 1.1.多线程介绍   学习多线程之前,我们先要了解几个关于多线程有关的概念。   进程:正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有...
  •  数据量很大百万条记录,因此考虑到要用多线程并发执行,在写的过程中又遇到问题,我想统计所有子进程执行完毕总共的耗时,在第一个子进程创建前记录当前时间用System.currentTimeMillis()在最后一个子进程结束后记录...
  • java守护线程

    2016-07-07 16:59:39
    比如你正在 用 Java 写成的编辑器 写 Word 文档,你一边敲键盘,这是个 非守护线程, 后台还有一个 拼写检查 线程,它是个守护线程,他尽量不打扰你写稿子, 你们可以同时进行,他发现有拼写错误时在状态...
  • c#与java多线程技术比较

    千次阅读 2005-02-01 09:36:00
    C#的多线程能力www.chinacs.net 2001-08-25 中文C#技术站 <!--google_ad_client = "pub-2021287483230946";google_ad_width = 160;google_ad_height = 600;google_ad_format = "160x600_as";google_ad_ch
  • 朋友让我帮忙写个程序从文本文档中导入数据到oracle... 数据量很大百万条记录,因此考虑到要用多线程并发执行,在写的过程中又遇到问题,我想统计所有子进程执行完毕总共的耗时,在第一个子进程创建前记录当前时间用Sy
  • Java 7之多线程第5篇 - 原子类

    千次阅读 2014-02-03 09:50:51
    java.util.concurrent包里包含的主要就是一些与并发实现相关的类,首先来看一下最为基础的原子类(java.util.concurrent.atomic)和和线程锁(java.utl.concurrent.locks)。 1、原子类 在java.util.concurrent....
  • 概述 浏览网页时,当你等待加载图片时可以阅读和...通过允许一个多线程程序在同一时间做多个任务就可以使得这些方便的功能成为可能。这本书可以帮助你学习这些必须的技巧并把此有用的功能与你的JAVA程序相结合。
  • Java线程详解

    千次阅读 2015-04-25 22:59:23
    ...Java线程详解 ...Java线程:概念与原理 ...多线程是实现多任务的一种方式。 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Wind
  •  数据量很大百万条记录,因此考虑到要用多线程并发执行,在写的过程中又遇到问题,我想统计所有子进程执行完毕总共的耗时,在第一个子进程创建前记录当前时 间用System.currentTimeMillis()在最后一个子进程结束后记录...
  • 多线程下载原理

    千次阅读 2016-02-25 11:19:20
    多线程下载原理图和java代码实现 import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.InputStream; import java.io.RandomAccessFile; import java.net....
  • Date和Long类型数据进行转换 日起在程序中主要是通过龙类型数据进行转换的 SimpleDateFormat类 对日期进行格式化 把Date类型的格式转换为字符串格式...线程的基本概念: 1 进程与线程的区别 进程:在操作系...
  • 基础与框架 1.String类能被继承吗,为什么 2.String,Stringbuffer,StringBuilder的区别? ...5.用过哪些Map,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分...
  • 学习多线程之前,我们先要了解几个关于多线程有关的概念。 进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。 线程:...
  • 多线程编程技术

    千次阅读 2008-10-27 16:35:00
    转自:...据说以前的 400 版本并不支持真正的多线程技术,在 4.2版后才从内核上提供了对多线程的支持。总之写这份文档的时候,绝大部分版本应该可以支持。主要资料来源于 IB
  • C# 多线程能力

    2007-04-28 11:47:00
    我们在这篇文章中将重点讨论Java和C#在线程方面的不同之处,并将一些Java线程的常用模式转换为C#。 从概念上讲,线程提供了一种在一个软件中并行执行代码的方式━━每个线程都“同时”在一个共享的内存空间中执行...
  • C#的多线程能力

    2007-06-27 22:56:00
    我们在这篇文章中将重点讨论Java和C#在线程方面的不同之处,并将一些Java线程的常用模式转换为C#。 从概念上讲,线程提供了一种在一个软件中并行执行代码的方式━━每个线程都“同时”在一个共享的内存空间中执行...
  • JAVA守护线程 非守护线程

    千次阅读 2017-02-21 10:35:19
    当非守护线程执行完jvm就退出,不管是否还有守护线程在执行。所以守护线程尽量不要执行逻辑代码,顶多执行一些可有可无的辅助性代码。什么东西作为守护线程,还是不太明确?第二篇举了一个实际例子,可以加深理解。 ...
  • 编程资料 -C# 多线程

    千次阅读 2011-04-19 22:57:00
    编程资料 - 多线程 C#多线程编程实例实战 作者: ...本文通过对常见的单个写入/多个阅读程序的分析来探索c# 的多线程编程。 问题的提出 所谓单个写入程序/多个阅读程序的线程同步问

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 39,374
精华内容 15,749
关键字:

多线程编辑文档java

java 订阅